merge master

pull/1/head
Pierre Bondoerffer 8 years ago
commit 1092285a68
No known key found for this signature in database
GPG Key ID: C0C7C0C5063F2236

@ -0,0 +1,7 @@
[ignore]
[include]
[libs]
[options]

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": [

@ -12,7 +12,7 @@ node_js:
- "6.6.0"
before_script:
- npm run-script lint
- cp config.js.dist config.js
- cp config.example.js config.js
- npm install bower
- ./node_modules/bower/bin/bower install
- node ./server.js &

@ -76,7 +76,7 @@ Chainpad can handle out of order messages, but it performs best when its message
By architecting your system such that all clients send to a server which then relays to other clients, you guarantee that a particular chain of patches is consistent between the participants of your session.
Cryptpad is capable of using a variety of data stores.
Which data store your instance employs can be [easily configured](https://github.com/xwiki-labs/cryptpad/blob/master/config.js.dist).
Which data store your instance employs can be [easily configured](https://github.com/xwiki-labs/cryptpad/blob/master/config.example.js).
You simply need to write an adaptor which conforms to a simple API.
The documentation for writing such an adaptor, and the complete list of implemented adaptors, is available [here](https://github.com/xwiki-labs/cryptpad/tree/master/storage).
@ -243,5 +243,3 @@ A session could still have difficulty with very large chains, however, in practi
## Conclusion

@ -1,3 +1,4 @@
/*@flow*/
/*
globals module
*/
@ -38,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.
@ -58,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 *",
@ -115,6 +116,12 @@ module.exports = {
'contact',
],
/* Domain
* If you want to have enable payments on your CryptPad instance, it has to be able to tell
* our account server what is your domain
*/
// domain: 'https://cryptpad.fr',
/*
You have the option of specifying an alternative storage adaptor.
These status of these alternatives are specified in their READMEs,
@ -140,6 +147,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))
*/
@ -162,6 +186,52 @@ 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: false,
/* 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: false,
/* Default user storage limit (bytes)
* if you don't want to limit users,
* you can set this to the size of your hard disk
*/
defaultStorageLimit: 50 * 1024 * 1024,
/* Max Upload Size (bytes)
* this sets the maximum size of any one file uploaded to the server.
* anything larger than this size will be rejected
*/
maxUploadSize: 20 * 1024 * 1024,
/* clients can use the /settings/ app to opt out of usage feedback
* which informs the server of things like how much each app is being
* used, and whether certain clientside features are supported by
* the client's browser. The intent is to provide feedback to the admin
* such that the service can be improved. Enable this with `true`
* and ignore feedback with `false` or by commenting the attribute
*/
//logFeedback: true,
/* it is recommended that you serve cryptpad over https
* the filepaths below are used to configure your certificates
*/

@ -4,12 +4,12 @@
mkdir -p customize
[ -z "$(ls -A customize)" ] && echo "Creating customize folder" \
&& cp -R customize.dist/* customize/ \
&& cp config.js.dist customize/config.js
&& cp config.example.js customize/config.js
# Linking config.js
# Linking config.js
[ ! -h config.js ] && echo "Linking config.js" && ln -s customize/config.js config.js
# Configure
# Configure
[ -n "$USE_SSL" ] && echo "Using secure websockets: $USE_SSL" \
&& sed -i "s/useSecureWebsockets: .*/useSecureWebsockets: ${USE_SSL},/g" customize/config.js

@ -8,7 +8,6 @@
<link rel="stylesheet" type="text/css" href="/customize/main.css" />
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<script data-bootload="/customize/main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
@ -107,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>
@ -115,7 +114,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.4.0 (Easter-Bunny)</div>
<div class="version-footer">CryptPad v1.7.0 (Hodag)</div>
</footer>
</body>

@ -32,7 +32,24 @@ define(function() {
'#FF00C0', // hot pink
'#800080', // purple
];
config.enableTemplates = true;
config.enableHistory = true;
config.enablePinLimit = true;
/* 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;
});

@ -1,4 +1,5 @@
CKEDITOR.editorConfig = function( config ) { // jshint ignore:line
/* global CKEDITOR */
CKEDITOR.editorConfig = function( config ) {
var fixThings = false;
// https://dev.ckeditor.com/ticket/10907
config.needsBrFiller= fixThings;
@ -8,13 +9,33 @@ CKEDITOR.editorConfig = function( config ) { // jshint ignore:line
// magicline plugin inserts html crap into the document which is not part of the
// document itself and causes problems when it's sent across the wire and reflected back
config.removePlugins= 'resize';
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock';
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify';
config.toolbarGroups= [{"name":"clipboard","groups":["clipboard","undo"]},{"name":"editing","groups":["find","selection"]},{"name":"links"},{"name":"insert"},{"name":"forms"},{"name":"tools"},{"name":"document","groups":["mode","document","doctools"]},{"name":"others"},{"name":"basicstyles","groups":["basicstyles","cleanup"]},{"name":"paragraph","groups":["list","indent","blocks","align","bidi"]},{"name":"styles"},{"name":"colors"}];
config.font_defaultLabel = 'Arial';
config.fontSize_defaultLabel = '16px';
config.fontSize_defaultLabel = '16';
config.contentsCss = '/customize/ckeditor-contents.css';
config.keystrokes = [
[ CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' ],
[ CKEDITOR.ALT + 122 /*F11*/, 'elementsPathFocus' ],
[ CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' ],
[ CKEDITOR.CTRL + 90 /*Z*/, 'undo' ],
[ CKEDITOR.CTRL + 89 /*Y*/, 'redo' ],
[ CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/, 'redo' ],
[ CKEDITOR.CTRL + CKEDITOR.SHIFT + 76 /*L*/, 'link' ],
[ CKEDITOR.CTRL + 76 /*L*/, undefined ],
[ CKEDITOR.CTRL + 66 /*B*/, 'bold' ],
[ CKEDITOR.CTRL + 73 /*I*/, 'italic' ],
[ CKEDITOR.CTRL + 85 /*U*/, 'underline' ],
[ CKEDITOR.ALT + 109 /*-*/, 'toolbarCollapse' ]
];
//skin: 'moono-cryptpad,/pad/themes/moono-cryptpad/'
//skin: 'flat,/pad/themes/flat/'
//skin: 'moono-lisa,/pad/themes/moono-lisa/'

@ -8,7 +8,6 @@
<link rel="stylesheet" type="text/css" href="/customize/main.css" />
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<script data-bootload="/customize/main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
@ -104,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>
@ -112,7 +111,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.4.0 (Easter-Bunny)</div>
<div class="version-footer">CryptPad v1.7.0 (Hodag)</div>
</footer>
</body>

@ -8,7 +8,6 @@
<link rel="stylesheet" type="text/css" href="/customize/main.css" />
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<script data-bootload="/customize/main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
@ -226,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>
@ -234,7 +233,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.4.0 (Easter-Bunny)</div>
<div class="version-footer">CryptPad v1.7.0 (Hodag)</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;
@ -408,6 +413,7 @@
font-family: FontAwesome;
}
.dropdown-bar button .fa-caret-down {
margin-right: 0px;
margin-left: 5px;
}
.dropdown-bar .dropdown-bar-content {
@ -571,7 +577,7 @@ html.cp,
font-size: .875em;
background-color: #fafafa;
color: #555;
font-family: Georgia,Cambria,serif;
font-family: Ubuntu,Georgia,Cambria,serif;
height: 100%;
}
.cp {
@ -597,6 +603,14 @@ html.cp,
font-family: lato, Helvetica, sans-serif;
font-size: 1.02em;
}
.cp .unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.cp h1,
.cp h2,
.cp h3,
@ -1084,6 +1098,48 @@ html.cp,
color: #FA5858;
cursor: pointer !important;
}
/* Pin limit */
.limit-container .cryptpad-limit-bar {
display: inline-block;
height: 26px;
width: 200px;
margin: 2px;
box-sizing: border-box;
border: 1px solid #999;
background: white;
position: relative;
text-align: center;
line-height: 24px;
vertical-align: middle;
}
.limit-container .cryptpad-limit-bar .usage {
height: 24px;
display: inline-block;
background: blue;
position: absolute;
left: 0;
z-index: 1;
}
.limit-container .cryptpad-limit-bar .usage.normal {
background: #5cb85c;
}
.limit-container .cryptpad-limit-bar .usage.warning {
background: orange;
}
.limit-container .cryptpad-limit-bar .usage.above {
background: red;
}
.limit-container .cryptpad-limit-bar .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;
}
.limit-container .upgrade {
margin-left: 10px;
}
#cors-store {
display: none;
}

@ -1,11 +1,10 @@
define([
'jquery',
'/customize/application_config.js',
'/common/cryptpad-common.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Config, Cryptpad) {
var $ = window.$;
'/common/cryptpad-common.js'
], function ($, Config, Cryptpad) {
var APP = window.APP = {
window.APP = {
Cryptpad: Cryptpad,
};
@ -119,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/';
});
};
@ -191,4 +192,3 @@ define([
console.log("ready");
});
});

@ -7,7 +7,8 @@ var map = {
'es': 'Español',
'pl': 'Polski',
'de': 'Deutsch',
'pt-br': 'Português do Brasil'
'pt-br': 'Português do Brasil',
'ro': 'Română',
};
var getStoredLanguage = function () { return localStorage.getItem(LS_LANG); };
@ -23,12 +24,10 @@ var getLanguage = function () {
};
var language = getLanguage();
var req = ['/customize/translations/messages.js'];
var req = ['jquery', '/customize/translations/messages.js'];
if (language && map[language]) { req.push('/customize/translations/messages.' + language + '.js'); }
req.push('/bower_components/jquery/dist/jquery.min.js');
define(req, function(Default, Language) {
var $ = window.jQuery;
define(req, function($, Default, Language) {
var externalMap = JSON.parse(JSON.stringify(map));
@ -114,9 +113,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
@ -139,12 +136,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]);

@ -8,7 +8,6 @@
<link rel="stylesheet" type="text/css" href="/customize/main.css" />
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<script data-bootload="/customize/main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
@ -125,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>
@ -133,7 +132,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.4.0 (Easter-Bunny)</div>
<div class="version-footer">CryptPad v1.7.0 (Hodag)</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);
};
@ -163,12 +163,8 @@
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = Frame;
}
else if ((typeof(define) !== 'undefined' && define !== null) &&
(define.amd !== null)) {
define([
'/bower_components/jquery/dist/jquery.min.js',
], function () {
} else if (typeof(define) === 'function' && define.amd) {
define(['jquery'], function () {
return Frame;
});
} else {

@ -1,8 +1,7 @@
define([
'/customize/share/frame.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Frame) {
var $ = window.jQuery;
'jquery',
'/customize/share/frame.js'
], function ($, Frame) {
var domain = 'https://beta.cryptpad.fr';
@ -40,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");
@ -51,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; }
@ -77,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; }
@ -123,4 +122,3 @@ define([
}].forEach(runTest);
});
});

@ -60,7 +60,10 @@ var fragments = {};
});
// build static pages
['../www/settings/index'].forEach(function (page) {
[
'../www/settings/index',
'../www/user/index'
].forEach(function (page) {
var source = swap(template, {
topbar: fragments.topbar,
fork: fragments.fork,

@ -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.4.0 (Easter-Bunny)</div>
<div class="version-footer">CryptPad v1.7.0 (Hodag)</div>
</footer>

@ -8,12 +8,14 @@
@import "./topbar.less";
@import "./footer.less";
@toolbar-green: #5cb85c;
html.cp, .cp body {
font-size: .875em;
background-color: @page-white; //@base;
color: @fore;
font-family: Georgia,Cambria,serif;
font-family: Ubuntu,Georgia,Cambria,serif;
height: 100%;
}
@ -41,6 +43,15 @@ a.github-corner > svg {
font-size: 1.02em;
}
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
h1,h2,h3,h4,h5,h6 {
color: @fore;
@ -536,6 +547,51 @@ noscript {
}
}
/* Pin limit */
.limit-container {
.cryptpad-limit-bar {
display: inline-block;
height: 26px;
width: 200px;
margin: 2px;
box-sizing: border-box;
border: 1px solid #999;
background: white;
position: relative;
text-align: center;
line-height: 24px;
vertical-align: middle;
.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;
}
}
.upgrade {
margin-left: 10px;
}
}
// hack for our cross-origin iframe
#cors-store {
display: none;

@ -18,6 +18,7 @@
button {
.fa-caret-down{
margin-right: 0px;
margin-left: 5px;
}
}

@ -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;

@ -28,7 +28,10 @@
box-sizing: border-box;
padding: 0px 6px;
.fa {font-family: FontAwesome;}
.fa {
font: normal normal normal 14px/1 FontAwesome;
font-family: FontAwesome;
}
.unselectable;
@ -42,12 +45,17 @@
}
button {
&#shareButton {
font: @toolbar-button-font;
* {
font: @toolbar-button-font;
}
&#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 +66,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 +86,47 @@
&.hidden {
display: none;
}
// Bootstrap 4 colors (btn-secondary)
border: 1px solid transparent;
border-radius: .25rem;
color: #000;
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-limit {
box-sizing: border-box;
height: 26px;
width: 26px;
display: inline-block;
padding: 5px;
margin: 3px 0;
div {
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;
@ -105,6 +135,7 @@
margin: 3px;
vertical-align: top;
box-sizing: content-box;
text-align: center;
span {
display: inline-block;
width: 4px;
@ -172,6 +203,7 @@
padding-right: 5px;
padding-left: 5px;
margin: 3px 2px;
box-sizing: border-box;
}
.dropdown-bar-content {
@ -179,17 +211,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 +399,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 +412,11 @@
.cryptpad-toolbar-leftside {
float: left;
margin-bottom: -1px;
.cryptpad-user-list {
//float: right;
.cryptpad-dropdown-users {
pre {
/* needed for ckeditor */
white-space: pre;
margin: 0;
margin: 5px 0px;
}
}
button {
@ -409,12 +429,44 @@
.cryptpad-toolbar-rightside {
text-align: right;
}
.cryptpad-spinner {
.cryptpad-toolbar-history {
display: none;
text-align: center;
.next {
display: inline-block;
vertical-align: middle;
margin: 20px;
}
.previous {
display: inline-block;
vertical-align: middle;
margin: 20px;
}
.goto {
display: inline-block;
vertical-align: middle;
text-align: center;
input { width: 75px; }
}
.gotoInput {
vertical-align: middle;
}
}
.cke_toolbox .cryptpad-toolbar-history {
input.gotoInput {
background: white;
height: 20px;
padding: 3px 3px;
border-radius: 5px;
}
}
.cryptpad-spinner > span {
height: 16px;
width: 16px;
margin: 8px;
line-height: 16px;
font-size: 16px;
text-align: center;
}
.cryptpad-readonly {
margin-right: 5px;

@ -72,6 +72,7 @@
@toolbar-gradient-start: #f5f5f5;
@toolbar-gradient-end: #DDDDDD;
@toolbar-button-font: 12px Ubuntu, Arial, sans-serif;
@topbar-back: #fff;
@topbar-color: #000;

@ -8,7 +8,6 @@
<link rel="stylesheet" type="text/css" href="/customize/main.css" />
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
{{script}}
</head>

@ -8,7 +8,6 @@
<link rel="stylesheet" type="text/css" href="/customize/main.css" />
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<script data-bootload="/customize/main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
@ -108,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>
@ -116,7 +115,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.4.0 (Easter-Bunny)</div>
<div class="version-footer">CryptPad v1.7.0 (Hodag)</div>
</footer>
</body>

@ -7,6 +7,7 @@
font-family: FontAwesome;
}
.dropdown-bar button .fa-caret-down {
margin-right: 0px;
margin-left: 5px;
}
.dropdown-bar .dropdown-bar-content {
@ -112,56 +113,93 @@
z-index: 9001;
}
.cryptpad-toolbar .fa {
font: normal normal normal 14px/1 FontAwesome;
font-family: FontAwesome;
}
.cryptpad-toolbar a {
float: right;
}
.cryptpad-toolbar button#shareButton {
.cryptpad-toolbar button {
font: 12px Ubuntu, Arial, sans-serif;
border: 1px solid transparent;
border-radius: .25rem;
color: #000;
background-color: #fff;
border-color: #ccc;
}
.cryptpad-toolbar button * {
font: 12px Ubuntu, Arial, sans-serif;
}
.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-limit {
box-sizing: border-box;
height: 26px;
width: 26px;
display: inline-block;
padding: 5px;
margin: 3px 0;
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;
@ -170,8 +208,9 @@
margin: 3px;
vertical-align: top;
box-sizing: content-box;
text-align: center;
}
.cryptpad-toolbar #newLag span {
.cryptpad-toolbar .cryptpad-lag span {
display: inline-block;
width: 4px;
margin: 0;
@ -182,50 +221,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;
}
@ -245,22 +284,12 @@
padding-right: 5px;
padding-left: 5px;
margin: 3px 2px;
box-sizing: border-box;
}
.cryptpad-toolbar .dropdown-bar-content {
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 +478,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 +488,10 @@
float: left;
margin-bottom: -1px;
}
.cryptpad-toolbar-leftside .cryptpad-user-list pre {
.cryptpad-toolbar-leftside .cryptpad-dropdown-users pre {
/* needed for ckeditor */
white-space: pre;
margin: 0;
margin: 5px 0px;
}
.cryptpad-toolbar-leftside button {
margin: 2px 4px 2px 0px;
@ -472,12 +502,44 @@
.cryptpad-toolbar-rightside {
text-align: right;
}
.cryptpad-spinner {
.cryptpad-toolbar-history {
display: none;
text-align: center;
}
.cryptpad-toolbar-history .next {
display: inline-block;
vertical-align: middle;
margin: 20px;
}
.cryptpad-toolbar-history .previous {
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: 75px;
}
.cryptpad-toolbar-history .gotoInput {
vertical-align: middle;
}
.cke_toolbox .cryptpad-toolbar-history input.gotoInput {
background: white;
height: 20px;
padding: 3px 3px;
border-radius: 5px;
}
.cryptpad-spinner > span {
height: 16px;
width: 16px;
margin: 8px;
line-height: 16px;
font-size: 16px;
text-align: center;
}
.cryptpad-readonly {
margin-right: 5px;

@ -290,10 +290,23 @@ define(function () {
out.fm_categoryError = "No se pudo abrir la categoría seleccionada, mostrando la raíz.";
out.settings_userFeedbackHint1 = "CryptPad suministra informaciones muy básicas al servidor, para ayudarnos a mejorar vuestra experiencia.";
out.settings_userFeedbackHint2 = "El contenido de tu pad nunca será compartido con el servidor.";
out.settings_userFeedback = "Activar feedback"; // "Disable user feedback"
out.settings_userFeedback = "Activar feedback";
out.settings_anonymous = "No has iniciado sesión. Tus ajustes se aplicarán solo a este navegador.";
out.blog = "Blog";
out.initialState = "<p>Esto es <strong>CryptPad</strong>, el editor collaborativo en tiempo real zero knowledge.<br>Lo que escribes aquí es cifrado, con lo cual solo las personas con el enlace pueden accederlo.<br>Incluso el servido no puede ver lo que escribes.</p><p><small><i>Lo que ves aquí, lo que escuchas aquí, cuando sales, se queda aquí</i></small></p><p>&nbsp;<br></p>";
out.initialState = [
'<span style="font-size:18px;"><p>',
'Esto es&nbsp;<strong>CryptPad</strong>, el editor collaborativo en tiempo real Zero Knowledge. Todo está guardado cuando escribes.',
'<br>',
'Comparte el enlace a este pad para editar con amigos o utiliza el botón <span style="background-color:#5cb85c;color:#ffffff;">&nbsp;Compartir&nbsp;</span> para obtener un <em>enlace solo lectura</em>&nbsp;que permite leer pero no escribir.',
'</p>',
'<p><span style="color:#808080;"><em>',
'Vamos, solo empezia a escribir...',
'</em></span></p></span>',
'<p>&nbsp;<br></p>'
].join('');
out.codeInitialState = "/*\n Esto es CryptPad, el editor collaborativo en tiempo real zero knowledge.\n Lo que escribes aquí es cifrado, con lo cual solo las personas con el enlace pueden accederlo.\n Incluso el servidor no puede ver lo que escribes.\n Lo que ves aquí, lo que escuchas aquí, cuando sales, se queda aquí\n*/";
out.slideInitialState = "# CryptSlide\n* Esto es CryptPad, el editor collaborativo en tiempo real zero knowledge.\n* Lo que escribes aquí es cifrado, con lo cual solo las personas con el enlace pueden accederlo.\n* Incluso el servidor no puede ver lo que escribes.\n* Lo que ves aquí, lo que escuchas aquí, cuando sales, se queda aquí\n\n---\n# Como utilizarlo\n1. Escribe tu contenido en Markdown\n - Puedes aprender más sobre Markdown [aquí](http://www.markdowntutorial.com/)\n2. Separa tus slides con ---\n3. Haz clic en \"Presentar\" para ver el resultado - Tus slides se actualizan en tiempo real";
out.driveReadmeTitle = "¿Qué es CryptDrive?";
@ -356,6 +369,66 @@ define(function () {
out.register_warning = "Zero Knowledge significa que no podemos recuperar tus datos si pierdes tu contraseña.";
out.register_alreadyRegistered = "Este usuario ya existe, ¿iniciar sesión?";
// 1.4.0 - Easter Bunny
out.button_newwhiteboard = "Nueva Pizarra";
out.wrongApp = "No se pudo mostrar el contenido de la sessión en tiempo real en tu navigador. Por favor, actualiza la página.";
out.synced = "Todo está guardado.";
out.saveTemplateButton = "Guardar como plantilla";
out.saveTemplatePrompt = "Élige un título para la plantilla";
out.templateSaved = "¡Plantilla guardada!";
out.selectTemplate = "Élige una plantilla o pulsa ESC";
out.slideOptionsTitle = "Personaliza tus diapositivas";
out.slideOptionsButton = "Guardar (enter)";
out.canvas_clear = "Limpiar";
out.canvas_delete = "Borrar selección";
out.canvas_disable = "No permitir dibujos";
out.canvas_enable = "Permitir dibujos";
out.canvas_width = "Talla";
out.canvas_opacity = "Opacidad";
out.settings_publicSigningKey = "Clave de Firma Pública";
out.settings_usage = "Utilización";
out.settings_usageTitle = "Vee el uso total de tus pads en MB";
out.settings_pinningNotAvailable = "Los pads pegados solo están disponibles para usuarios registrados.";
out.settings_pinningError = "Algo salió mal";
out.settings_usageAmount = "Tus pads pegados utilizan {0}MB";
out.historyButton = "Mostrar el historial del documento";
out.history_next = "Ir a la versión anterior";
out.history_prev = "Ir a la versión posterior";
out.history_goTo = "Ir a la versión seleccionada";
out.history_close = "Volver";
out.history_closeTitle = "Cerrar el historial";
out.history_restore = "Restaurar";
out.history_restoreTitle = "Restaurar la versión seleccionada del documento";
out.history_restorePrompt = "¿Estás seguro que quieres cambiar la versión actual del documento por esta?";
out.history_restoreDone = "Documento restaurado";
out.fc_sizeInKilobytes = "Talla en Kilobytes";
// 1.5.0/1.6.0 - Fenrir/Grootslang
out.deleted = "El pad fue borrado de tu CryptDrive";
out.upgrade = "Mejorar";
out.upgradeTitle = "Mejora tu cuenta para obtener más espacio";
out.MB = "MB";
out.GB = "GB";
out.KB = "KB";
out.formattedMB = "{0} MB";
out.formattedGB = "{0} GB";
out.formattedKB = "{0} KB";
out.pinLimitReached = "Has llegado al limite de espacio";
out.pinLimitReachedAlert = "Has llegado al limite de espacio. Nuevos pads no serán movidos a tu CryptDrive.<br>Para resolver este problema, puedes quitar pads de tu CryptDrive (incluso en la papelera) o mejorar tu cuenta para obtener más espacio.";
out.pinLimitNotPinned = "Has llegado al limite de espacio.<br>Este pad no estará presente en tu CryptDrive.";
out.pinLimitDrive = "Has llegado al limite de espacio.<br>No puedes crear nuevos pads.";
out.printTransition = "Activar transiciones";
out.history_version = "Versión: ";
out.settings_logoutEverywhereTitle = "Cerrar sessión en todas partes";
out.settings_logoutEverywhere = "Cerrar todas las otras sessiones";
out.settings_logoutEverywhereConfirm = "¿Estás seguro? Tendrás que volver a iniciar sessión con todos tus dispositivos.";
out.upload_serverError = "Error: no pudimos subir tu archivo.";
out.upload_uploadPending = "Ya tienes una subida en progreso. ¿Cancelar y subir el nuevo archivo?";
out.upload_success = "Tu archivo ({0}) ha sido subido con éxito y fue añadido a tu drive.";
out.poll_remove = "Quitar";
out.poll_edit = "Editar";
out.poll_locked = "Cerrado";

@ -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';
@ -30,6 +32,7 @@ define(function () {
out.error = "Erreur";
out.saved = "Enregistré";
out.synced = "Tout est enregistré";
out.deleted = "Pad supprimé de votre CryptDrive";
out.disconnected = 'Déconnecté';
out.synchronizing = 'Synchronisation';
@ -49,10 +52,36 @@ define(function () {
out.language = "Langue";
out.comingSoon = "Bientôt disponible...";
out.newVersion = '<b>CryptPad a été mis à jour !</b><br>' +
'Découvrez les nouveautés de la dernière version :<br>'+
'<a href="https://github.com/xwiki-labs/cryptpad/releases/tag/{0}" target="_blank">Notes de version pour CryptPad {0}</a>';
out.upgrade = "Augmenter votre limite";
out.upgradeTitle = "Améliorer votre compte pour augmenter la limite de stockage";
out.MB = "Mo";
out.GB = "Go";
out.KB = "Ko";
out.formattedMB = "{0} Mo";
out.formattedGB = "{0} Go";
out.formattedKB = "{0} Ko";
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.updated_0_pinLimitReachedAlert = "Vous avez atteint votre limite de stockage. Les nouveaux pads ne seront pas enregistrés dans votre CryptDrive.<br>" +
'Vous pouvez soit supprimer des pads de votre CryptDrive, soit vous <a href="https://accounts.cryptpad.fr/#!on={0}" target="_blank">abonner à une offre premium</a> pour augmenter la limite maximale.';
out.pinLimitReachedAlert = out.updated_0_pinLimitReachedAlert;
out.pinAboveLimitAlert = 'Depuis la dernière version, nous imposons désormais une limite de 50 Mo de stockage gratuit et vous utilisez actuellement {0}. You devriez soit supprimer certains pads ou soit vous abonner sur <a href="https://accounts.cryptpad.fr/#!on={1}" target="_blank">accounts.cryptpad.fr</a>. Votre contribution nous aidera à améliorer CryptPad et à répandre le Zero Knowledge. Vous pouvez contacter le <a href="https://accounts.cryptpad.fr/#/support" target="_blank">support</a> pour tout problème ou question concernant ces changements.';
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';
@ -80,6 +109,8 @@ define(function () {
out.templateSaved = "Modèle enregistré !";
out.selectTemplate = "Sélectionner un modèle ou appuyer sur Échap";
out.previewButtonTitle = "Afficher ou cacher la prévisualisation de Markdown";
out.presentButtonTitle = "Entrer en mode présentation";
out.presentSuccess = 'Appuyer sur Échap pour quitter le mode présentation';
@ -93,6 +124,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)";
@ -115,6 +147,18 @@ define(function () {
out.cancel = "Annuler";
out.cancelButton = 'Annuler (Echap)';
out.historyButton = "Afficher l'historique du document";
out.history_next = "Voir la version suivante";
out.history_prev = "Voir la version précédente";
out.history_goTo = "Voir la version sélectionnée";
out.history_close = "Retour";
out.history_closeTitle = "Fermer l'historique";
out.history_restore = "Restaurer";
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
out.poll_title = "Sélecteur de date Zero Knowledge";
@ -196,14 +240,22 @@ define(function () {
out.fm_info_root = "Créez ici autant de dossiers que vous le souhaitez pour trier vos fichiers.";
out.fm_info_unsorted = 'Contient tous les pads que vous avez ouvert et qui ne sont pas triés dans "Documents" ou déplacés vers la "Corbeille".'; // "My Documents" should match with the "out.fm_rootName" key, and "Trash" with "out.fm_trashName"
out.fm_info_template = "Contient tous les fichiers que vous avez sauvés en tant que modèle afin de les réutiliser lors de la création d'un nouveau pad.";
out.fm_info_trash = 'Les fichiers supprimés dans la corbeille sont également enlevés de "Tous les fichiers" et il est impossible de les récupérer depuis l\'explorateur de fichiers.'; // Same here for "All files" and "out.fm_filesDataName"
out.updated_0_fm_info_trash = "Vider la corbeille permet de libérer de l'espace dans votre CryptDrive";
out.fm_info_trash = out.updated_0_fm_info_trash;
out.fm_info_allFiles = 'Contient tous les fichiers de "Documents", "Fichiers non triés" et "Corbeille". Vous ne pouvez pas supprimer ou déplacer des fichiers depuis cet endroit.'; // Same here
out.fm_info_anonymous = 'Vous n\'êtes pas connectés, ces pads risquent donc d\'être supprimés (<a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">découvrez pourquoi</a>). ' +
'<a href="/register/">Inscrivez-vous</a> ou <a href="/login/">connectez-vous</a> pour les maintenir en vie.';
out.fm_alert_backupUrl = "Lien de secours pour ce disque.<br>" +
"Il est <strong>fortement recommandé</strong> de garder ce lien pour vous-même.<br>" +
"Elle vous servira en cas de perte des données de votre navigateur afin de retrouver vos fichiers.<br>" +
"Quiconque se trouve en possession de celle-ci peut modifier ou supprimer tous les fichiers de ce gestionnaire.<br>";
out.fm_alert_anonymous = "Bonjour ! Vous utilisez actuellement Cryptpad de manière anonyme, ce qui ne pose pas de problème mais vos pads peuvent être supprimés après un certain temps " +
"d'inactivité. Nous avons désactivé certaines fonctionnalités avancées de CryptDrive pour les utilisateurs anonymes afin de rendre clair le fait que ce n'est pas " +
'un endroit sûr pour le stockage des documents. Vous pouvez <a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">en lire plus</a> concernant ' +
'nos raisons pour ces changements et pourquoi vous devriez vraiment <a href="/register/">vous enregistrer</a> et <a href="/login/">vous connecter</a>.';
out.fm_backup_title = 'Lien de secours';
out.fm_nameFile = 'Comment souhaitez-vous nommer ce fichier ?';
out.fm_error_cantPin = "Erreur interne du serveur. Veuillez recharger la page et essayer de nouveau.";
// File - Context menu
out.fc_newfolder = "Nouveau dossier";
out.fc_rename = "Renommer";
@ -214,6 +266,7 @@ define(function () {
out.fc_remove = "Supprimer définitivement";
out.fc_empty = "Vider la corbeille";
out.fc_prop = "Propriétés";
out.fc_sizeInKilobytes = "Taille en kilo-octets";
// fileObject.js (logs)
out.fo_moveUnsortedError = "La liste des éléments non triés ne peut pas contenir de dossiers.";
out.fo_existingNameError = "Ce nom est déjà utilisé dans ce répertoire. Veuillez en choisir un autre.";
@ -295,6 +348,32 @@ define(function () {
out.settings_anonymous = "Vous n'êtes pas connectés. Ces préférences seront utilisées pour ce navigateur.";
out.settings_publicSigningKey = "Clé publique de signature";
out.settings_usage = "Utilisation";
out.settings_usageTitle = "Voir la taille totale de vos pads épinglés en Mo";
out.settings_pinningNotAvailable = "Les pads épinglés sont disponibles uniquement pour les utilisateurs enregistrés.";
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.";
out.upload_serverError = "Erreur interne: impossible d'uploader le fichier pour l'instant.";
out.upload_uploadPending = "Vous avez déjà un fichier en cours d'upload. Souhaitez-vous l'annuler et uploader ce nouveau fichier ?";
out.upload_success = "Votre fichier ({0}) a été uploadé avec succès et ajouté à votre CryptDrive.";
out.upload_notEnoughSpace = "Il n'y a pas assez d'espace libre dans votre CryptDrive pour ce fichier.";
out.upload_tooLarge = "Ce fichier dépasse la taille maximale autorisée.";
out.upload_choose = "Choisir un fichier";
out.upload_pending = "En attente";
out.upload_cancelled = "Annulé";
out.upload_name = "Nom du fichier";
out.upload_size = "Taille";
out.upload_progress = "État";
out.download_button = "Déchiffrer et télécharger";
// general warnings
out.warn_notPinned = "Ce pad n'est stocké dans aucun CryptDrive. Il va expirer après 3 mois d'inactivité. <a href='/about.html#pinning'>En savoir plus...</a>";
// index.html
//about.html
@ -378,12 +457,12 @@ define(function () {
// Initial states
out.initialState = [
'<span style="font-size:18px;"><p>',
'<span style="font-size:16px;"><p>',
'Voici <strong>CryptPad</strong>, l\'éditeur collaboratif en temps-réel Zero Knowledge. Tout est sauvegardé dés que vous le tapez.',
'<br>',
'Partagez le lien vers ce pad avec des amis ou utilisez le bouton <span style="background-color:#5cb85c;color:#ffffff;">&nbsp;Partager&nbsp;</span> pour obtenir le <em>lien de lecture-seule</em>, qui permet la lecture mais non la modification.',
'</p>',
'<p><span style="color:#808080; font-size: 18px;">',
'<p><span style="color:#808080; font-size: 16px;">',
'<em>',
'Lancez-vous, commencez à taper...',
'</em></span></p></span>',
@ -391,11 +470,10 @@ define(function () {
].join('');
out.codeInitialState = [
'/*\n',
' Voici l\'éditeur de code collaboratif et Zero Knowledge de CryptPad.\n',
' Ce que vous tapez ici est chiffré de manière que seules les personnes avec le lien peuvent y accéder.\n',
' Vous pouvez choisir le langage de programmation pour la coloration syntaxique, ainsi que le thème de couleurs, dans le coin supérieur droit.\n',
'*/'
'# Éditeur de code collaboratif et Zero Knowledge de CryptPad\n',
'\n',
'* Ce que vous tapez ici est chiffré de manière que seules les personnes avec le lien peuvent y accéder.\n',
'* Vous pouvez choisir le langage de programmation pour la coloration syntaxique, ainsi que le thème de couleurs, dans le coin supérieur droit.'
].join('');
out.slideInitialState = [
@ -446,7 +524,7 @@ define(function () {
out.tips.marker = "Vous pouvez surligner du texte dans un pad en utilisant l'option \"marker\" dans le menu déroulant des styles.";
out.feedback_about = "Si vous lisez ceci, vous vous demandez probablement pourquoi CryptPad envoie des requêtes vers des pages web quand vous realisez certaines actions.";
out.feedback_privacy = "Nous prenons au sérieux le respect de votre vie privée, et en même temps nous souhaitons rendre CryptPad très simple à utiliser. Nous utilisons cette page pour comprendre quelles foncitonnalités dans l'interface comptent le plus pour les utilisateurs, en l'appelant avec un paramètre spécifiant quelle action a été réalisée.";
out.feedback_privacy = "Nous prenons au sérieux le respect de votre vie privée, et en même temps nous souhaitons rendre CryptPad très simple à utiliser. Nous utilisons cette page pour comprendre quelles fonctionnalités dans l'interface comptent le plus pour les utilisateurs, en l'appelant avec un paramètre spécifiant quelle action a été réalisée.";
out.feedback_optout = "Si vous le souhaitez, vous pouvez désactiver ces requêtes en vous rendant dans <a href='/settings/'>votre page de préférences</a>, où vous trouverez une case à cocher pour désactiver le retour d'expérience.";
return out;

@ -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';
@ -32,6 +34,7 @@ define(function () {
out.error = "Error";
out.saved = "Saved";
out.synced = "Everything is saved";
out.deleted = "Pad deleted from your CryptDrive";
out.disconnected = 'Disconnected';
out.synchronizing = 'Synchronizing';
@ -51,10 +54,36 @@ define(function () {
out.language = "Language";
out.comingSoon = "Coming soon...";
out.newVersion = '<b>CryptPad has been updated!</b><br>' +
'Check out what\'s new in the latest version:<br>'+
'<a href="https://github.com/xwiki-labs/cryptpad/releases/tag/{0}" target="_blank">Release notes for CryptPad {0}</a>';
out.upgrade = "Upgrade";
out.upgradeTitle = "Upgrade your account to increase the storage limit";
out.MB = "MB";
out.GB = "GB";
out.KB = "KB";
out.formattedMB = "{0} MB";
out.formattedGB = "{0} GB";
out.formattedKB = "{0} KB";
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.updated_0_pinLimitReachedAlert = "You've reached your storage limit. New pads won't be stored in your CryptDrive.<br>" +
'You can either remove pads from your CryptDrive or <a href="https://accounts.cryptpad.fr/#!on={0}" target="_blank">subscribe to a premium offer</a> to increase your limit.';
out.pinLimitReachedAlert = out.updated_0_pinLimitReachedAlert;
out.pinAboveLimitAlert = 'As of this release, we are imposing a 50MB limit on free data storage and you are currently using {0}. You will need to either delete some pads or subscribe on <a href="https://accounts.cryptpad.fr/#!on={1}" target="_blank">accounts.cryptpad.fr</a>. Your contribution will help us improve CryptPad and spread Zero Knowledge. Please contact <a href="https://accounts.cryptpad.fr/#/support" target="_blank">support</a> if you have any other questions.';
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';
@ -82,6 +111,8 @@ define(function () {
out.templateSaved = "Template saved!";
out.selectTemplate = "Select a template or press escape";
out.previewButtonTitle = "Display or hide the Markdown preview mode";
out.presentButtonTitle = "Enter presentation mode";
out.presentSuccess = 'Hit ESC to exit presentation mode';
@ -95,6 +126,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)";
@ -117,6 +149,18 @@ define(function () {
out.cancel = "Cancel";
out.cancelButton = 'Cancel (esc)';
out.historyButton = "Display the document history";
out.history_next = "Go to the next version";
out.history_prev = "Go to the previous version";
out.history_goTo = "Go to the selected version";
out.history_close = "Back";
out.history_closeTitle = "Close the history";
out.history_restore = "Restore";
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
out.poll_title = "Zero Knowledge Date Picker";
@ -203,14 +247,22 @@ define(function () {
out.fm_info_root = "Create as many nested folders here as you want to sort your files.";
out.fm_info_unsorted = 'Contains all the files you\'ve visited that are not yet sorted in "Documents" or moved to the "Trash".'; // "My Documents" should match with the "out.fm_rootName" key, and "Trash" with "out.fm_trashName"
out.fm_info_template = 'Contains all the pads stored as templates and that you can re-use when you create a new pad.';
out.fm_info_trash = 'Files deleted from the trash are also removed from "All files" and it is impossible to recover them from the file manager.'; // Same here for "All files" and "out.fm_filesDataName"
out.updated_0_fm_info_trash = 'Empty your trash to free space in your CryptDrive.';
out.fm_info_trash = out.updated_0_fm_info_trash;
out.fm_info_allFiles = 'Contains all the files from "Documents", "Unsorted" and "Trash". You can\'t move or remove files from here.'; // Same here
out.fm_info_anonymous = 'You are not logged in so these pads may be deleted (<a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">find out why</a>). ' +
'<a href="/register/">Sign up</a> or <a href="/login/">Log in</a> to keep them alive.';
out.fm_alert_backupUrl = "Backup link for this drive.<br>" +
"It is <strong>highly recommended</strong> that you keep ip for yourself only.<br>" +
"You can use it to retrieve all your files in case your browser memory got erased.<br>" +
"Anybody with that link can edit or remove all the files in your file manager.<br>";
out.fm_alert_anonymous = "Hello there, you are currently using CryptPad anonymously, that's ok but your pads may be deleted after a period of " +
"inactivity. We have disabled advanced features of the drive for anonymous users because we want to be clear that it is " +
'not a safe place to store things. You can <a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">read more</a> about ' +
'why we are doing this and why you really should <a href="/register/">Sign up</a> and <a href="/login/">Log in</a>.';
out.fm_backup_title = 'Backup link';
out.fm_nameFile = 'How would you like to name that file?';
out.fm_error_cantPin = "Internal server error. Please reload the page and try again.";
// File - Context menu
out.fc_newfolder = "New folder";
out.fc_rename = "Rename";
@ -221,6 +273,7 @@ define(function () {
out.fc_remove = "Delete permanently";
out.fc_empty = "Empty the trash";
out.fc_prop = "Properties";
out.fc_sizeInKilobytes = "Size in Kilobytes";
// fileObject.js (logs)
out.fo_moveUnsortedError = "You can't move a folder to the list of unsorted pads";
out.fo_existingNameError = "Name already used in that directory. Please choose another one.";
@ -305,6 +358,32 @@ define(function () {
out.settings_anonymous = "You are not logged in. Settings here are specific to this browser.";
out.settings_publicSigningKey = "Public Signing Key";
out.settings_usage = "Usage";
out.settings_usageTitle = "See the total size of your pinned pads in MB";
out.settings_pinningNotAvailable = "Pinned pads are only available to registered users.";
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.";
out.upload_serverError = "Server Error: unable to upload your file at this time.";
out.upload_uploadPending = "You already have an upload in progress. Cancel it and upload your new file?";
out.upload_success = "Your file ({0}) has been successfully uploaded and added to your drive.";
out.upload_notEnoughSpace = "There is not enough space for this file in your CryptDrive.";
out.upload_tooLarge = "This file exceeds the maximum upload size.";
out.upload_choose = "Choose a file";
out.upload_pending = "Pending";
out.upload_cancelled = "Cancelled";
out.upload_name = "File name";
out.upload_size = "Size";
out.upload_progress = "Progress";
out.download_button = "Decrypt & Download";
// general warnings
out.warn_notPinned = "This pad is not in anyone's CryptDrive. It will expire after 3 months. <a href='/about.html#pinning'>Learn more...</a>";
// index.html
@ -391,7 +470,7 @@ define(function () {
// Initial states
out.initialState = [
'<span style="font-size:18px;"><p>',
'<span style="font-size:16px;"><p>',
'This is&nbsp;<strong>CryptPad</strong>, the Zero Knowledge realtime collaborative editor. Everything is saved as you type.',
'<br>',
'Share the link to this pad to edit with friends or use the <span style="background-color:#5cb85c;color:#ffffff;">&nbsp;Share&nbsp;</span> button to share a <em>read-only link</em>&nbsp;which allows viewing but not editing.',
@ -404,11 +483,10 @@ define(function () {
].join('');
out.codeInitialState = [
'/*\n',
' This is the CryptPad Zero Knowledge collaborative code editor.\n',
' What you type here is encrypted so only people who have the link can access it.\n',
' You can choose the programming language to highlight and the UI color scheme in the upper right.\n',
'*/'
'# CryptPad\'s Zero Knowledge collaborative code editor\n',
'\n',
'* What you type here is encrypted so only people who have the link can access it.\n',
'* You can choose the programming language to highlight and the UI color scheme in the upper right.'
].join('');
out.slideInitialState = [

@ -0,0 +1,371 @@
define(function () {
var out = {};
out.main_title = "CryptPad: Zero Knowledge, Colaborare în timp real";
out.main_slogan = "Puterea stă în cooperare - Colaborarea este cheia";
out.type = {};
out.pad = "Rich text";
out.code = "Code";
out.poll = "Poll";
out.slide = "Presentation";
out.drive = "Drive";
out.whiteboard = "Whiteboard";
out.file = "File";
out.media = "Media";
out.button_newpad = "Filă Text Nouă";
out.button_newcode = "Filă Cod Nouă";
out.button_newpoll = "Sondaj Nou";
out.button_newslide = "Prezentare Nouă";
out.button_newwhiteboard = "Fila Desen Nouă";
out.updated_0_common_connectionLost = "<b>Conexiunea la server este pierdută</b><br>Până la revenirea conexiunii, vei fi în modul citire";
out.common_connectionLost = out.updated_0_common_connectionLost;
out.websocketError = "Conexiune inexistentă către serverul websocket...";
out.typeError = "Această filă nu este compatibilă cu aplicația aleasă";
out.onLogout = "Nu mai ești autentificat, <a href=\"/\" target=\"_blank\">apasă aici</a> să te autentifici<br>sau apasă <em>Escape</em>să accesezi fila în modul citire.";
out.wrongApp = "Momentan nu putem arăta conținutul sesiunii în timp real în fereastra ta. Te rugăm reîncarcă pagina.";
out.loading = "Încarcă...";
out.error = "Eroare";
out.saved = "Salvat";
out.synced = "Totul a fost salvat";
out.deleted = "Pad șters din CryptDrive-ul tău";
out.disconnected = "Deconectat";
out.synchronizing = "Se sincronizează";
out.reconnecting = "Reconectare...";
out.lag = "Decalaj";
out.readonly = "Mod citire";
out.anonymous = "Anonim";
out.yourself = "Tu";
out.anonymousUsers = "editori anonimi";
out.anonymousUser = "editor anonim";
out.users = "Utilizatori";
out.and = "Și";
out.viewer = "privitor";
out.viewers = "privitori";
out.editor = "editor";
out.editors = "editori";
out.language = "Limbă";
out.upgrade = "Actualizare";
out.upgradeTitle = "Actualizează-ți contul pentru a mări limita de stocare";
out.MB = "MB";
out.greenLight = "Totul funcționează corespunzător";
out.orangeLight = "Conexiunea lentă la internet îți poate afecta experiența";
out.redLight = "Ai fost deconectat de la sesiune";
out.pinLimitReached = "Ai atins limita de stocare";
out.pinLimitReachedAlert = "Ai atins limita de stocare. Noile pad-uri nu vor mai fi stocate în CryptDrive.<br>Pentru a rezolva această problemă, poți să nlături pad-uri din CryptDrive-ul tău (incluzând gunoiul) sau să subscrii la un pachet premium pentru a-ți extinde spațiul de stocare.";
out.pinLimitNotPinned = "Ai atins limita de stocare.<br>Acest pad nu va fi stocat n CryptDrive-ul tău.";
out.pinLimitDrive = "Ai atins limita de stocare.<br>Nu poți să creezi alte pad-uri.";
out.importButtonTitle = "Importă un pad dintr-un fișier local";
out.exportButtonTitle = "Exportă pad-ul acesta către un fișier local";
out.exportPrompt = "Cum ai vrea să îți denumești fișierul?";
out.changeNamePrompt = "Schimbă-ți numele (lasă necompletat dacă vrei să fii anonim): ";
out.user_rename = "Schimbă numele afișat";
out.user_displayName = "Nume afișat";
out.user_accountName = "Nume cont";
out.clickToEdit = "Click pentru editare";
out.forgetButtonTitle = "Mută acest pad la gunoi";
out.forgetPrompt = "Click-ul pe OK va muta acest pad la gunoi. Ești sigur?";
out.movedToTrash = "Acest pad a fost mutat la gunoi.<br><a href=\"/drive/\">Acesează-mi Drive-ul</a>";
out.shareButton = "Distribuie";
out.shareSuccess = "Link copiat în clipboard";
out.newButton = "Nou";
out.newButtonTitle = "Crează un nou pad";
out.saveTemplateButton = "Salvează ca șablon";
out.saveTemplatePrompt = "Alege un titlu pentru șablon";
out.templateSaved = "Șablon salvat!";
out.selectTemplate = "Selectează un șablon sau apasă escape";
out.presentButtonTitle = "Intră în modul de prezentare";
out.presentSuccess = "Apasă ESC pentru a ieși din modul de prezentare";
out.backgroundButtonTitle = "Schimbă culoarea de fundal din prezentare";
out.colorButtonTitle = "Schimbă culoarea textului în modul de prezentare";
out.printButton = "Printează (enter)";
out.printButtonTitle = "Printează-ți slide-urile sau exportă-le ca fișier PDF";
out.printOptions = "Opțiuni schemă";
out.printSlideNumber = "Afișează numărul slide-ului";
out.printDate = "Afișează data";
out.printTitle = "Afișează titlul pad-ului";
out.printCSS = "Reguli de stil personalizate (CSS):";
out.printTransition = "Permite tranziția animațiilor";
out.slideOptionsTitle = "Personalizează-ți slide-urile";
out.slideOptionsButton = "Salvează (enter)";
out.editShare = "Editează link-ul";
out.editShareTitle = "Copiază link-ul de editare în clipboard";
out.editOpen = "Deschide link-ul de editare într-o nouă filă";
out.editOpenTitle = "Deschide acest pad în modul de editare într-o nouă filă";
out.viewShare = "Link în modul citire";
out.viewShareTitle = "Copiază link-ul în modul de citire în clipboard";
out.viewOpen = "Deschide link-ul în modul de citire într-o filă nouă";
out.viewOpenTitle = "Deschide acest pad în modul de citire într-o nouă filă";
out.notifyJoined = "{0} s-au alăturat sesiunii colaborative";
out.notifyRenamed = "{0} e cunoscut ca {1}";
out.notifyLeft = "{0} au părăsit sesiunea colaborativă";
out.okButton = "OK (enter)";
out.cancel = "Anulează";
out.cancelButton = "Anulează (esc)";
out.historyButton = "Afișează istoricul documentului";
out.history_next = "Mergi la versiunea următoare";
out.history_prev = "Mergi la versiunea trecută";
out.history_goTo = "Mergi la sesiunea selectată";
out.history_close = "Înapoi";
out.history_closeTitle = "Închide istoricul";
out.history_restore = "Restabilește";
out.history_restoreTitle = "Restabilește versiunea selectată a documentului";
out.history_restorePrompt = "Ești sigur că vrei să înlocuiești versiunea curentă a documentului cu cea afișată?";
out.history_restoreDone = "Document restabilit";
out.history_version = "Versiune:";
out.poll_title = "Zero Knowledge Selector Dată";
out.poll_subtitle = "Zero Knowledge, <em>realtime</em> programare";
out.poll_p_save = "Setările tale sunt actualizate instant, așa că tu nu trebuie să salvezi.";
out.poll_p_encryption = "Tot conținutul tău este criptat ca doar persoanele cărora tu le dai link-ul să aibă acces. Nici serverul nu poate să vadă ce modifici.";
out.wizardLog = "Click pe butonul din dreapta sus pentru a te ntoarce la sondajul tău";
out.wizardTitle = "Folosește wizard-ul pentru a crea sondajul tău";
out.wizardConfirm = "Ești pregătit să adaugi aceste opțiuni la sondajul tău?";
out.poll_publish_button = "Publică";
out.poll_admin_button = "Admin";
out.poll_create_user = "Adaugă un nou utilizator";
out.poll_create_option = "Adaugă o nouă opțiune";
out.poll_commit = "Comite";
out.poll_closeWizardButton = "Închide wizard-ul";
out.poll_closeWizardButtonTitle = "Închide wizard-ul";
out.poll_wizardComputeButton = "Calculează Opțiunile";
out.poll_wizardClearButton = "Curăță Tabelul";
out.poll_wizardDescription = "Crează automat un număr de opțiuni întroducând orice număr de zile sau intervale orare";
out.poll_wizardAddDateButton = "+ Zi";
out.poll_wizardAddTimeButton = "+ Ore";
out.poll_optionPlaceholder = "Opțiune";
out.poll_userPlaceholder = "Numele tău";
out.poll_removeOption = "Ești sigur că vrei să îndepărtezi această opțiune?";
out.poll_removeUser = "Ești sigur că vrei să îndepărtezi aceast utilizator?";
out.poll_titleHint = "Titlu";
out.poll_descriptionHint = "Descrie sondajul, și apoi folosește butonul 'publică' când ai terminat. Orice utilizator care are link-ul poate modifica descrierea, dar descurajăm această practică.";
out.canvas_clear = "Curăță";
out.canvas_delete = "Curăță selecția";
out.canvas_disable = "Dezactivează modul desen";
out.canvas_enable = "Activează modul desen";
out.canvas_width = "Lățime";
out.canvas_opacity = "Opacitate";
out.fm_rootName = "Documente";
out.fm_trashName = "Gunoi";
out.fm_unsortedName = "Fișiere nesortate";
out.fm_filesDataName = "Toate fișierele";
out.fm_templateName = "Șabloane";
out.fm_searchName = "Caută";
out.fm_searchPlaceholder = "Caută...";
out.fm_newButton = "Nou";
out.fm_newButtonTitle = "Crează un nou pad sau folder";
out.fm_newFolder = "Folder nou";
out.fm_newFile = "Pad nou";
out.fm_folder = "Folder";
out.fm_folderName = "Numele folderului";
out.fm_numberOfFolders = "# de foldere";
out.fm_numberOfFiles = "# of files";
out.fm_fileName = "Nume filă";
out.fm_title = "Titlu";
out.fm_type = "Tip";
out.fm_lastAccess = "Ultima accesare";
out.fm_creation = "Creare";
out.fm_forbidden = "Acțiune interzisă";
out.fm_originalPath = "Ruta inițială";
out.fm_openParent = "Arată în folder";
out.fm_noname = "Document nedenumit";
out.fm_emptyTrashDialog = "Ești sigur că vrei să golești coșul de gunoi?";
out.fm_removeSeveralPermanentlyDialog = "Ești sigur că vrei să ștergi pentru totdeauna aceste {0} elemente din coșul de gunoi?";
out.fm_removePermanentlyDialog = "Ești sigur că vrei să ștergi acest element pentru totdeauna?";
out.fm_removeSeveralDialog = "Ești sigur că vrei să muți aceste {0} elemente la coșul de gunoi?";
out.fm_removeDialog = "Ești sigur că vrei să muți {0} la gunoi?";
out.fm_restoreDialog = "Ești sigur că vrei să restabilești {0} în locația trecută?";
out.fm_unknownFolderError = "Ultima locație vizitată sau cea selectată nu mai există. Deschidem fișierul părinte...";
out.fm_contextMenuError = "Nu putem deschide meniul de context pentru acest element. Dacă problema persistă, reîncarcă pagina.";
out.fm_selectError = "Nu putem selecta elementul vizat. Dacă problema persistă, reîncarcă pagina.";
out.fm_categoryError = "Nu putem deschide categoria selectată, afișează sursa.";
out.fm_info_root = "Crează câte foldere tip cuib ai nevoie pentru a-ți sorta fișierele.";
out.fm_info_unsorted = "Conține toate fișierele pe care le-ai vizitat și nu sunt sortate în \"Documente\" sau mutate în \"Gunoi\".";
out.fm_info_template = "Conține toate pad-urile stocate ca șabloane și pe care le poți refolosi atunci când creezi un nou pad.";
out.fm_info_trash = "Fișierele șterse din gunoi vor fi șterse și din \"Toate fișierele\", făcând imposibilă recuperarea fișierelor din managerul de fișiere.";
out.fm_info_allFiles = "Conține toate fișierele din \"Documente\", \"Nesortate\" și \"Gunoi\". Poți să muți sau să ștergi fișierele aici.";
out.fm_info_login = "Loghează-te";
out.fm_info_register = "Înscrie-te";
out.fm_info_anonymous = "Nu ești logat cu un cont valid așa că aceste pad-uri vor fi șterse (<a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">află de ce</a>). <a href=\"/register/\">Înscrie-te</a> sau <a href=\"/login/\">Loghează-te</a> pentru a le salva.";
out.fm_alert_backupUrl = "Link copie de rezervă pentru acest drive.<br> Este <strong>foarte recomandat</strong> să o păstrezi pentru tine.<br>Poți să o folosești pentru a recupera toate fișierele în cazul în care memoria browserului tău este șterge..<br>Oricine are linkul poate să editeze sau să îndepărteze toate fișierele din managerul tău de documente.<br>";
out.fm_alert_anonymous = "Salut, momentan folosești CryptPad în mod anonim. Este ok, doar că fișierele tale vor fi șterse după o perioadă de inactivitate. Am dezactivat caracteristicile avansate ale drive-ului pentru utilizatorii anonimi pentru a face clar faptul că stocare documentelor acolo nu este o metodă sigură. Poți să <a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">citești mai multe</a> despre motivarea noastră și despre ce de trebuie să te <a href=\"/register/\">Înregistrezi</a> si sa te <a href=\"/login/\">Loghezi</a>.";
out.fm_backup_title = "Link de backup";
out.fm_nameFile = "Cum ai vrea să numești fișierul?";
out.fc_newfolder = "Folder nou";
out.fc_rename = "Redenumește";
out.fc_open = "Deschide";
out.fc_open_ro = "Deschide (modul citire)";
out.fc_delete = "Șterge";
out.fc_restore = "Restaurează";
out.fc_remove = "Șterge permanent";
out.fc_empty = "Curăță coșul";
out.fc_prop = "Proprietăți";
out.fc_sizeInKilobytes = "Dimensiune n Kilobytes";
out.fo_moveUnsortedError = "Nu poți să muți un folder la lista de pad-uri nesortate";
out.fo_existingNameError = "Numele ales este deja folosit în acest director. Te rugăm să alegi altul.";
out.fo_moveFolderToChildError = "Nu poți să muți un folder într-unul dintre descendenții săi";
out.fo_unableToRestore = "Nu am reușit să restaurăm fișierul în locația de origine. Poți să ncerci să îl muți într-o nouă locație.";
out.fo_unavailableName = "Un fișier sau un folder cu același nume există deja în locația nouă. Redenumește elementul și încearcă din nou.";
out.login_login = "Loghează-te";
out.login_makeAPad = "Crează un pad în modul anonim";
out.login_nologin = "Răsfoiește pad-urile locale";
out.login_register = "Înscrie-te";
out.logoutButton = "Deloghează-te";
out.settingsButton = "Setări";
out.login_username = "Nume utilizator";
out.login_password = "Parolă";
out.login_confirm = "Confirmă parola";
out.login_remember = "Ține-mă minte";
out.login_hashing = "Încriptăm parola, o să mai dureze.";
out.login_hello = "Salut {0},";
out.login_helloNoName = "Salut,";
out.login_accessDrive = "Acesează-ți drive-ul";
out.login_orNoLogin = "sau";
out.login_noSuchUser = "Nume de utilizator sau parolă invalide. Încearcă din nou sau înscrie-te.";
out.login_invalUser = "Nume utilizator cerut";
out.login_invalPass = "Parolă cerută";
out.login_unhandledError = "O eroare neașteptată a avut loc emoticon_unhappy";
out.register_importRecent = "Importă istoricul pad-ului (Recomandat)";
out.register_acceptTerms = "Accept <a href='/terms.html'>termenii serviciului</a>";
out.register_passwordsDontMatch = "Parolele nu se potrivesc!";
out.register_mustAcceptTerms = "Trebuie să accepți termenii serviciului";
out.register_mustRememberPass = "Nu putem să îți resetăm parola dacă o uiți. Este foarte important să o ții minte! Bifează căsuța pentru a confirma.";
out.register_header = "Bine ai venit în CryptPad";
out.register_explanation = "<p>Hai să stabilim câteva lucruri, mai întâi</p><ul><li>Parola ta este cheia secretă care criptează toate pad-urile tale. Dacă pierzi/uiți parola nu există nici-o metodă prin care îți putem recupera datele.</li><li>Poți importa pad-uri care au fost vizionate recent în browser pentru a le avea în cont.</li><li>Dacă folosești un computer împărțit, trebuie să te deloghezi, închiderea taburilor nu este de ajuns.</li></ul>";
out.register_writtenPassword = "Mi-am notat numele de utilizator și parola, înaintează.";
out.register_cancel = "Întoarce-te";
out.register_warning = "Zero Knowledge înseamnă că noi nu îți putem recupera datele dacă îți pierzi parola.";
out.register_alreadyRegistered = "Acest user există deja, vrei să te loghezi?";
out.settings_title = "Setări";
out.settings_save = "Salvează";
out.settings_backupTitle = "Fă o copie de rezervă sau restaurează toate datele";
out.settings_backup = "Copie de rezervă";
out.settings_restore = "Restaurează";
out.settings_resetTitle = "Curăță-ți drive-ul";
out.settings_reset = "Îndepărtează toate fișierele și folderele din CryptPad-ul tău.";
out.settings_resetPrompt = "Această acțiune va indepărta toate pad-urile din drive-ul tău.<br>Ești sigur că vrei să continui?<br>Tastează “<em>Iubesc CryptPad</em>” pentru a confirma.";
out.settings_resetDone = "Drive-ul tău este acum gol!";
out.settings_resetError = "Text de verificare incorect. CryptPad-ul tău nu a fost schimbat.";
out.settings_resetTips = "Sfaturi în CryptDrive";
out.settings_resetTipsButton = "Resetează sfaturile disponibile în CryptDrive";
out.settings_resetTipsDone = "Toate sfaturile sunt vizibile din nou.";
out.settings_importTitle = "Importă pad-urile recente ale acestui browser n CryptDrive-ul meu";
out.settings_import = "Importă";
out.settings_importConfirm = "Ești sigur că vrei să imporți pad-urile recente ale acestui browser în contul tău de CryptDrive?";
out.settings_importDone = "Import complet";
out.settings_userFeedbackHint1 = "CryptPad oferă niște feedback foarte simplu serverului, pentru a ne informa cum putem să îți îmbunătățim experiența voastră.";
out.settings_userFeedbackHint2 = "Conținutul pad-ului tău nu va fi împărțit cu serverele.";
out.settings_userFeedback = "Activează feedback";
out.settings_anonymous = "Nu ești logat. Setările sunt specifice browser-ului.";
out.settings_publicSigningKey = "Cheia de semnătură publică";
out.settings_usage = "Uzaj";
out.settings_usageTitle = "Vezi dimensiunea totală a pad-urilor fixate în MB";
out.settings_pinningNotAvailable = "Pad-urile fixate sunt disponibile doar utilizatorilor înregistrați.";
out.settings_pinningError = "Ceva nu a funcționat";
out.settings_usageAmount = "Pad-urile tale fixate ocupă {0}MB";
out.settings_logoutEverywhereTitle = "Deloghează-te peste tot";
out.settings_logoutEverywhere = "Deloghează-te din toate sesiunile web";
out.settings_logoutEverywhereConfirm = "Ești sigur? Va trebui să te loghezi, din nou, pe toate device-urile tale.";
out.upload_serverError = "Eroare de server: fișierele tale nu pot fi încărcate la momentul acesta.";
out.upload_uploadPending = "Ai deja o încărcare în desfășurare. Anulezi și încarci noul fișier?";
out.upload_success = "Fișierul tău ({0}) a fost ncărcat și adăugat la drive-ul tău cu succes.";
out.main_p2 = "Acest proiect folosește <a href=\"http://ckeditor.com/\">CKEditor</a> Visual Editor, <a href=\"https://codemirror.net/\">CodeMirror</a>, și <a href=\"https://github.com/xwiki-contrib/chainpad\">ChainPad</a> un motor în timp real.";
out.main_howitworks_p1 = "CryptPad folosește o variantă a algoritmului de <a href=\"https://en.wikipedia.org/wiki/Operational_transformation\">Operational transformation</a> care este capabil să găsescă consens distribuit folosind <a href=\"https://bitcoin.org/bitcoin.pdf\">Nakamoto Blockchain</a>, o construcție popularizată de <a href=\"https://en.wikipedia.org/wiki/Bitcoin\">Bitcoin</a>. Astfel algoritmul poate evita nevoia ca serverul central să rezove conflicte, iar serverul nu este interesat de conținutul care este editat în pad.";
out.main_about_p2 = "Dacă ai orice fel de întrebare sau comentariu, poți să ne <a href=\"https://twitter.com/cryptpad\">dai un tweet</a>, semnalezi o problemă <a href=\"https://github.com/xwiki-labs/cryptpad/issues/\" title=\"index de probleme\">on github</a>, spui salut pe IRC (<a href=\"http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7\" title=\"freenode webchat\">irc.freenode.net</a>), sau <a href=\"research@xwiki.com\">trimiți un email</a>.";
out.main_info = "<h1>Colaborează n siguranță</h1><br> Dezvoltă-ți ideile împreună cu documente partajate în timp ce tehnologia <strong>Zero Knowledge</strong> îți păstrează securitatea; chiar și de noi.";
out.main_howitworks = "Cum funcționează";
out.main_zeroKnowledge = "Zero Knowledge";
out.main_zeroKnowledge_p = "Nu trebuie să ne crezi că <em>nu ne uităm</em> la pad-urile tale, cu tehnologia revoluționară Zero Knowledge a CryptPad <em>nu putem</em>. Învață mai multe despre cum îți protejăm <a href=\"/privacy.html\" title='Intimitatea'>Intimitate și Securitate</a>.";
out.main_writeItDown = "Notează";
out.main_writeItDown_p = "Cele mai importante proiecte vin din idei mici. Notează-ți momentele de inspirație și ideile neașteptate pentru că nu știi niciodată care ar putea fi noua mare descoperire.";
out.main_share = "Partajează link-ul, partajează pad-ul";
out.main_share_p = "Dezvoltă-ți ideile împreună: organizează întâlniri eficiente, colaborează pe liste TODO și fă prezentări scurte cu toți prietenii tăi și device-urile tale.";
out.main_organize = "Organizează-te";
out.main_organize_p = "Cu CryptPad Drive, poți să stai cu ochii pe ce este important. Folderele îți permit să ții evidența proiectelor tale și să ai o viziune globală asupra evoluției lucrurilor.";
out.tryIt = "Testează!";
out.main_richText = "Rich Text editor";
out.main_richText_p = "Editează texte complexe în mod colaborativ cu Zero Knowledge în timp real. <a href=\"http://ckeditor.com\" target=\"_blank\">CkEditor</a> application.";
out.main_code = "Editor cod";
out.main_code_p = "Editează cod din softul tău, în mod colaborativ, cu Zero Knowledge în timp real.<a href=\"https://www.codemirror.net\" target=\"_blank\">CodeMirror</a> application.";
out.main_slide = "Editor slide-uri";
out.main_slide_p = "Crează-ți prezentări folosind sintaxa Markdown, și afișează-le în browser-ul tău.";
out.main_poll = "Sondaj";
out.main_poll_p = "Plănuiește întâlniri sau evenimente, sau votează pentru cea mai bună soluție pentru problema ta.";
out.main_drive = "CryptDrive";
out.footer_applications = "Aplicații";
out.footer_contact = "Contact";
out.footer_aboutUs = "Despre noi";
out.about = "Despre";
out.privacy = "Privacy";
out.contact = "Contact";
out.terms = "ToS";
out.blog = "Blog";
out.policy_title = "Politica de confidențialitate CryptPad";
out.policy_whatweknow = "Ce știm despre tine";
out.policy_whatweknow_p1 = "Ca o aplicație care este găzduită online, CryptPad are acces la metadatele expuse de protocolul HTTP. Asta include adresa IP-ului tău, și alte titluri HTTP care pot fi folosite ca să identifice un browser. Poți să vezi ce informații împărtășește browser-ul tău vizitând <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.whatismybrowser.com/detect/what-http-headers-is-my-browser-sending\" title=\"what http headers is my browser sending\">WhatIsMyBrowser.com</a>.";
out.policy_whatweknow_p2 = "Folosim <a href=\"https://www.elastic.co/products/kibana\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"platforma de analiză open source\">Kibana</a>, o platformă open source, pentru a afla mai multe despre utilizatorii noștri. Kibana ne spune despre cum ai găsit CryptPad, căutare directă, printr-un motor de căutare, sau prin recomandare de la un alt serviciu online ca Reddit sau Twitter.";
out.policy_howweuse = "Cum folosim ce aflăm";
out.policy_howweuse_p1 = "Folosim aceste informații pentru a lua decizii mai bune în promovarea CryptPad, prin evaluarea eforturilor trecute care au fost de succes. Informațiile despre locația ta ne ajută să aflăm dacă ar trebui să oferim suport pentru alte limbi, pe lângă engleză.";
out.policy_howweuse_p2 = "Informațiile despre browser-ul tău (dacă este bazat pe un sistem de operare desktop sau mobil) ne ajută să luăm decizii când prioritizăm viitoarele îmbunătățiri. Echipa noastră de dezvoltare este mică, și încercăm să facem alegeri care să îmbunătățească experiența câtor mai mulți utilizatori.";
out.policy_whatwetell = "Ce le spunem altora despre tine";
out.policy_whatwetell_p1 = "Nu furnizăm informațiile obținute terților, decât dacă ne este cerut în mod legal.";
out.policy_links = "Link-uri către alte site-uri";
out.policy_links_p1 = "Acest site conține link-uri către alte site-uri, incluzându-le pe cele produse de alte organizații. Nu suntem responsabili pentru practicile de intimitate sau pentru conținutul site-urilor externe. Ca regulă generală, link-urile către site-uri externe sunt deschise ntr-o fereastră noup, pentru a face clar faptul că părăsiți CryptPad.fr.";
out.policy_ads = "Reclame";
out.policy_ads_p1 = "Nu afișăm nici o formă de publicitate online, dar s-ar putea să atașăm link-uri către instituțiile care ne finanțează cerecetarea.";
out.policy_choices = "Ce alegeri ai";
out.policy_choices_open = "Codul nostru este open source, așa că tu ai mereu posibilitatea de a-ți găzdui propria instanță de CryptPad.";
out.policy_choices_vpn = "Dacă vrei să folosești instanța găzduită de noi, dar nu vrei să îți expui IP-ul, poți să îl protejezi folosind <a href=\"https://www.torproject.org/projects/torbrowser.html.en\" title=\"downloads from the Tor project\" target=\"_blank\" rel=\"noopener noreferrer\">Tor browser bundle</a>, sau <a href=\"https://riseup.net/en/vpn\" title=\"VPNs provided by Riseup\" target=\"_blank\" rel=\"noopener noreferrer\">VPN</a>.";
out.policy_choices_ads = "Dacă vrei doar să blochezi platforma noastră de analiză, poți folosi soluții de adblocking ca <a href=\"https://www.eff.org/privacybadger\" title=\"download privacy badger\" target=\"_blank\" rel=\"noopener noreferrer\">Privacy Badger</a>.";
out.tos_title = "CryptPad Termeni de Utilizare";
out.tos_legal = "Te rugăm să nu fii rău intenționat, abuziv, sau să faci orice ilegal.";
out.tos_availability = "Sperăm că o să găsești acest serviciu util, dar disponibilitatea sau performanța nu poate fi garantată. Te rugăm să îți exporți datele n mod regulat.";
out.tos_e2ee = "Conținutul CryptPad poate fi citit sau modificat de oricine care poate ghici sau obține fragmentul identificator al pad-ului. Recomandăm să folosești soluții de comunicare criptate end-to-end-encrypted (e2ee) pentru a partaja link-uri, evitând orice risc în cazul unei scurgeri de informații.";
out.tos_logs = "Metadatele oferite de browser-ul tău serverului ar putea fi înscrise în scopul de a menține serviciul.";
out.tos_3rdparties = "Nu oferim date personale terților, decât dacă ne sunt solicitate prin lege.";
out.bottom_france = "<a href=\"http://www.xwiki.com/\" target=\"_blank\" rel=\"noopener noreferrer\">Realizat cu <img class=\"bottom-bar-heart\" src=\"/customize/heart.png\" alt=\"love\" /> n <img class=\"bottom-bar-fr\" src=\"/customize/fr.png\" alt=\"Franța\" /></a>";
out.bottom_support = "<a href=\"http://labs.xwiki.com/\" title=\"XWiki Labs\" target=\"_blank\" rel=\"noopener noreferrer\">Un proiect al <img src=\"/customize/logo-xwiki2.png\" alt=\"XWiki SAS\" class=\"bottom-bar-xwiki\"/> Labs Project </a> cu susținerea <a href=\"http://ng.open-paas.org/\" title=\"OpenPaaS::ng\" target=\"_blank\" rel=\"noopener noreferrer\"> <img src=\"/customize/openpaasng.png\" alt=\"OpenPaaS-ng\" class=\"bottom-bar-openpaas\" /></a>";
out.header_france = "<a href=\"http://www.xwiki.com/\" target=\"_blank\" rel=\"noopener noreferrer\">With <img class=\"bottom-bar-heart\" src=\"/customize/heart.png\" alt=\"love\" /> from <img class=\"bottom-bar-fr\" src=\"/customize/fr.png\" title=\"Franța\" alt=\"Franța\"/> by <img src=\"/customize/logo-xwiki.png\" alt=\"XWiki SAS\" class=\"bottom-bar-xwiki\"/></a>";
out.header_support = "<a href=\"http://ng.open-paas.org/\" title=\"OpenPaaS::ng\" target=\"_blank\" rel=\"noopener noreferrer\"> <img src=\"/customize/openpaasng.png\" alt=\"OpenPaaS-ng\" class=\"bottom-bar-openpaas\" /></a>";
out.header_logoTitle = "Mergi la pagina principală";
out.initialState = "<span style=\"font-size:16px;\"><p>Acesta este&nbsp;<strong>CryptPad</strong>, editorul colaborativ bazat pe tehnologia Zero Knowledge în timp real. Totul este salvat pe măsură ce scrii.<br>Partajează link-ul către acest pad pentru a edita cu prieteni sau folosește <span style=\"background-color:#5cb85c;color:#ffffff;\">&nbsp;Share&nbsp;</span> butonul pentru a partaja <em>read-only link</em>&nbsp;permițând vizualizarea dar nu și editarea.</p><p><span style=\"color:#808080;\"><em>Îndrăznește, începe să scrii...</em></span></p></span><p>&nbsp;<br></p>";
out.codeInitialState = "/*\n Acesta este editorul colaborativ de cod bazat pe tehnologia Zero Knowledge CryptPad.\n Ce scrii aici este criptat, așa că doar oamenii care au link-ul pot să-l acceseze.\n Poți să alegi ce limbaj de programare pus n evidență și schema de culori UI n dreapta sus.\n*/";
out.slideInitialState = "# CryptSlide\n* Acesta este un editor colaborativ bazat pe tehnologia Zero Knowledge.\n* Ce scrii aici este criptat, așa că doar oamenii care au link-ul pot să-l acceseze.\n* Nici măcar serverele nu au acces la ce scrii tu.\n* Ce vezi aici, ce auzi aici, atunci când pleci, lași aici.\n\n-\n# Cum se folosește\n1. Scrie-ți conținutul slide-urilor folosind sintaxa markdown\n - Află mai multe despre sintaxa markdown [aici](http://www.markdowntutorial.com/)\n2. Separă-ți slide-urile cu -\n3. Click pe butonul \"Play\" pentru a vedea rezultatele - Slide-urile tale sunt actualizate în timp real.";
out.driveReadmeTitle = "Ce este CryptDrive?";
out.readme_welcome = "Bine ai venit n CryptPad !";
out.readme_p1 = "Bine ai venit în CryptPad, acesta este locul unde îți poți lua notițe, singur sau cu prietenii.";
out.readme_p2 = "Acest pad o să îți ofere un scurt ghid în cum poți să folosești CryptPad pentru a lua notițe, a le ține organizate și a colabora pe ele.";
out.readme_cat1 = "Descoperă-ți CryptDrive-ul";
out.readme_cat1_l1 = "Crează un pad: În CryptDrive-ul tău, dă click {0} apoi {1} și poți să creezi un pad.";
out.readme_cat1_l2 = "Deschide pad-urile din CryptDrive-ul tău: doublu-click pe iconița unui pad pentru a-l deschide.";
out.readme_cat1_l3 = "Organizează-ți pad-urile: Când ești logat, orice pad accesezi va fi afișat ca în secțiunea {0} a drive-ului tău.";
out.readme_cat1_l3_l1 = "Poți să folosești funcția click and drag pentru a muta fișierele în folderele secțiunii {0} a drive-ului tău și pentru a crea noi foldere.";
out.readme_cat1_l3_l2 = "Ține minte să încerci click-dreapta pe iconițe pentru că există și meniuri adiționale.";
out.readme_cat1_l4 = "Pune pad-urile vechi în gunoi. Poți să folosești funcția click and drag pe pad-uri în categoria {0} la fel ca și în cazul folderelor.";
out.readme_cat2 = "Crează pad-uri ca un profesionist";
out.edit = "editează";
out.view = "vezi";
out.readme_cat2_l1 = "Butonul {0} din pad-ul tău dă accesul colaboratorilor tăi să {1} sau să {2} pad-ul.";
out.readme_cat2_l2 = "Schimbă titlul pad-ului dând click pe creion";
out.readme_cat3 = "Descoperă aplicațiile CryptPad";
out.readme_cat3_l1 = "Cu editorul de cod CryptPad, poți colabora pe cod ca Javascript și markdown ca HTML și Markdown";
out.readme_cat3_l2 = "Cu editorul de slide-uri CryptPad, poți să faci prezentări scurte folosind Markdown";
out.readme_cat3_l3 = "Cu CryptPoll poți să organizezi votări rapide, mai ales pentru a programa ntâlniri care se potrivesc calendarelor tuturor";
out.tips = { };
out.tips.lag = "Iconița verde din dreapta-sus arată calitatea conexiunii internetului tău la serverele CryptPad.";
out.tips.shortcuts = "`ctrl+b`, `ctrl+i` and `ctrl+u` sunt scurtături pentru bold, italic și underline.";
out.tips.indentare = "În listele cu bulină sau cele numerotate, poți folosi tab sau shift+tab pentru a mări sau micșora indentarea.";
out.tips.titlu = "Poți seta titlul pad-urilor tale prin click pe centru sus.";
out.tips.stocare = "De fiecare dată când vizitezi un pad, dacă ești logat va fi salvat pe CryptDrive-ul tău.";
out.tips.marker = "Poți sublinia text într-un pad folosind itemul \"marker\" n meniul de stiluri.";
out.feedback_about = "Dacă citești asta, probabil că ești curios de ce CryptPad cere pagini web atunci când întreprinzi anumite acțiuni";
out.feedback_privacy = "Ne pasă de intimitatea ta, si în același timp vrem să păstrăm CryptPad ușor de folosit. Folosim acest fișier pentru a ne da seama care beneficii UI contează cel mai mult pentru utilizatori, cerându-l alături de un parametru specific atunci când acțiunea se desfășoară";
out.feedback_optout = "Dacă vrei să ieși, vizitează <a href='/settings/'>setările de pe pagina ta de user</a>, unde vei găsi o căsuță pentru a activa sau dezactiva feedback-ul de la user";
return out;
});

@ -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,13 +1,14 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "1.4.0",
"version": "1.7.0",
"dependencies": {
"chainpad-server": "^1.0.1",
"express": "~4.10.1",
"ws": "^1.0.1",
"nthen": "~0.1.0",
"saferphore": "0.0.1",
"tweetnacl": "~0.12.2",
"chainpad-server": "^1.0.1"
"ws": "^1.0.1"
},
"devDependencies": {
"jshint": "~2.9.1",
@ -15,9 +16,9 @@
"less": "2.7.1"
},
"scripts": {
"lint": "jshint --config .jshintrc --exclude-path .jshintignore .",
"test": "node TestSelenium.js",
"style": "lessc ./customize.dist/src/less/cryptpad.less > ./customize.dist/main.css && lessc ./customize.dist/src/less/toolbar.less > ./customize.dist/toolbar.css && lessc ./www/drive/file.less > ./www/drive/file.css && lessc ./www/settings/main.less > ./www/settings/main.css && lessc ./www/slide/slide.less > ./www/slide/slide.css && lessc ./www/whiteboard/whiteboard.less > ./www/whiteboard/whiteboard.css && lessc ./www/poll/poll.less > ./www/poll/poll.css",
"template": "cd customize.dist/src && node build.js"
"lint": "jshint --config .jshintrc --exclude-path .jshintignore .",
"test": "node TestSelenium.js",
"style": "lessc ./customize.dist/src/less/cryptpad.less > ./customize.dist/main.css && lessc ./customize.dist/src/less/toolbar.less > ./customize.dist/toolbar.css && lessc ./www/drive/file.less > ./www/drive/file.css && lessc ./www/settings/main.less > ./www/settings/main.css && lessc ./www/slide/slide.less > ./www/slide/slide.css && lessc ./www/whiteboard/whiteboard.less > ./www/whiteboard/whiteboard.css && lessc ./www/poll/poll.less > ./www/poll/poll.css && lessc ./www/file/file.less > ./www/file/file.css",
"template": "cd customize.dist/src && node build.js"
}
}

@ -0,0 +1,101 @@
/* jshint esversion: 6 */
const Fs = require('fs');
const Semaphore = require('saferphore');
const nThen = require('nthen');
const hashesFromPinFile = (pinFile, fileName) => {
var pins = {};
pinFile.split('\n').filter((x)=>(x)).map((l) => JSON.parse(l)).forEach((l) => {
switch (l[0]) {
case 'RESET': {
pins = {};
//jshint -W086
// fallthrough
}
case 'PIN': {
l[1].forEach((x) => { pins[x] = 1; });
break;
}
case 'UNPIN': {
l[1].forEach((x) => { delete pins[x]; });
break;
}
default: throw new Error(JSON.stringify(l) + ' ' + fileName);
}
});
return Object.keys(pins);
};
const sizeForHashes = (hashes, dsFileSizes) => {
let sum = 0;
hashes.forEach((h) => {
const s = dsFileSizes[h];
if (typeof(s) !== 'number') {
//console.log('missing ' + h + ' ' + typeof(s));
} else {
sum += s;
}
});
return sum;
};
const sema = Semaphore.create(20);
let dirList;
const fileList = [];
const dsFileSizes = {};
const out = [];
nThen((waitFor) => {
Fs.readdir('./datastore', waitFor((err, list) => {
if (err) { throw err; }
dirList = list;
}));
}).nThen((waitFor) => {
dirList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readdir('./datastore/' + f, waitFor(returnAfter((err, list2) => {
if (err) { throw err; }
list2.forEach((ff) => { fileList.push('./datastore/' + f + '/' + ff); });
})));
});
});
}).nThen((waitFor) => {
fileList.forEach((f) => {
sema.take((returnAfter) => {
Fs.stat(f, waitFor(returnAfter((err, st) => {
if (err) { throw err; }
dsFileSizes[f.replace(/^.*\/([^\/]*)\.ndjson$/, (all, a) => (a))] = st.size;
})));
});
});
}).nThen((waitFor) => {
Fs.readdir('./pins', waitFor((err, list) => {
if (err) { throw err; }
dirList = list;
}));
}).nThen((waitFor) => {
fileList.splice(0, fileList.length);
dirList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readdir('./pins/' + f, waitFor(returnAfter((err, list2) => {
if (err) { throw err; }
list2.forEach((ff) => { fileList.push('./pins/' + f + '/' + ff); });
})));
});
});
}).nThen((waitFor) => {
fileList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readFile(f, waitFor(returnAfter((err, content) => {
if (err) { throw err; }
const hashes = hashesFromPinFile(content.toString('utf8'), f);
const size = sizeForHashes(hashes, dsFileSizes);
out.push([f, Math.floor(size / (1024 * 1024))]);
})));
});
});
}).nThen(() => {
out.sort((a,b) => (a[1] - b[1]));
out.forEach((x) => { console.log(x[0] + ' ' + x[1] + ' MB'); });
});

@ -32,8 +32,8 @@ npm install
npm install -g bower ## if necessary
bower install
## copy config.js.dist to config.js
cp config.js.dist config.js
## copy config.example.js to config.js
cp config.example.js config.js
node ./server.js
```
@ -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;
@ -162,4 +168,3 @@ sales@xwiki.com
[fragment identifier]: https://en.wikipedia.org/wiki/Fragment_identifier
[active attack]: https://en.wikipedia.org/wiki/Attack_(computing)#Types_of_attacks
[Creative Commons Attribution 2.5 License]: http://creativecommons.org/licenses/by/2.5/

744
rpc.js

@ -1,12 +1,48 @@
/*@flow*/
/* Use Nacl for checking signatures of messages */
var Nacl = require("tweetnacl");
/* globals Buffer*/
/* globals process */
var Fs = require("fs");
var Path = require("path");
var Https = require("https");
var RPC = module.exports;
var Store = require("./storage/file");
var isValidChannel = function (chan) {
return /^[a-fA-F0-9]/.test(chan);
var DEFAULT_LIMIT = 50 * 1024 * 1024;
var isValidId = function (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 () {
@ -20,7 +56,7 @@ var makeCookie = function (token) {
return [
time,
process.pid, // jshint ignore:line
process.pid,
token
];
};
@ -38,12 +74,21 @@ var parseCookie = function (cookie) {
return c;
};
var escapeKeyCharacters = function (key) {
return key.replace(/\//g, '-');
};
var unescapeKeyCharacters = function (key) {
return key.replace(/\-/g, '/');
};
var beginSession = function (Sessions, key) {
if (Sessions[key]) {
Sessions[key].atime = +new Date();
return Sessions[key];
var safeKey = escapeKeyCharacters(key);
if (Sessions[safeKey]) {
Sessions[safeKey].atime = +new Date();
return Sessions[safeKey];
}
var user = Sessions[key] = {};
var user = Sessions[safeKey] = {};
user.atime = +new Date();
user.tokens = [
makeToken()
@ -58,7 +103,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];
}
});
@ -67,7 +116,7 @@ var expireSessions = function (Sessions) {
var addTokenForKey = function (Sessions, publicKey, token) {
if (!Sessions[publicKey]) { throw new Error('undefined user'); }
var user = Sessions[publicKey];
var user = beginSession(Sessions, publicKey);
user.tokens.push(token);
user.atime = +new Date();
if (user.tokens.length > 2) { user.tokens.shift(); }
@ -85,17 +134,16 @@ 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;
}
var user = Sessions[publicKey];
var user = beginSession(Sessions, publicKey);
if (!user) { return false; }
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());
@ -144,8 +192,9 @@ var checkSignature = function (signedMsg, signature, publicKey) {
return Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer);
};
var loadUserPins = function (store, Sessions, publicKey, cb) {
var session = beginSession(Sessions, publicKey);
var loadUserPins = function (Env, publicKey, cb) {
var pinStore = Env.pinStore;
var session = beginSession(Env.Sessions, publicKey);
if (session.channels) {
return cb(session.channels);
@ -162,7 +211,7 @@ var loadUserPins = function (store, Sessions, publicKey, cb) {
pins[channel] = false;
};
store.getMessages(publicKey, function (msg) {
pinStore.getMessages(publicKey, function (msg) {
// handle messages...
var parsed;
try {
@ -204,33 +253,92 @@ var truthyKeys = function (O) {
});
};
var getChannelList = function (store, Sessions, publicKey, cb) {
loadUserPins(store, Sessions, publicKey, function (pins) {
var getChannelList = function (Env, publicKey, cb) {
loadUserPins(Env, publicKey, function (pins) {
cb(truthyKeys(pins));
});
};
var getFileSize = function (store, channel, cb) {
if (!isValidChannel(channel)) { return void cb('INVALID_CHAN'); }
var makeFilePath = function (root, id) {
if (typeof(id) !== 'string' || id.length <= 2) { return null; }
return Path.join(root, id.slice(0, 2), id);
};
// TODO don't blow up if their store doesn't have this API
return void store.getChannelSize(channel, function (e, size) {
if (e) { return void cb(e.code); }
var getUploadSize = function (Env, channel, cb) {
var paths = Env.paths;
var path = makeFilePath(paths.blob, channel);
if (!path) {
return cb('INVALID_UPLOAD_ID');
}
Fs.stat(path, function (err, stats) {
if (err) { return void cb(err); }
cb(void 0, stats.size);
});
};
var getFileSize = function (Env, channel, cb) {
if (!isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length === 32) {
if (typeof(Env.msgStore.getChannelSize) !== 'function') {
return cb('GET_CHANNEL_SIZE_UNSUPPORTED');
}
return void Env.msgStore.getChannelSize(channel, function (e, size) {
if (e) {
if (e === 'ENOENT') { return void cb(void 0, 0); }
return void cb(e.code);
}
cb(void 0, size);
});
}
// 'channel' refers to a file, so you need anoter API
getUploadSize(Env, channel, function (e, size) {
if (e) { return void cb(e); }
cb(void 0, size);
});
};
var getTotalSize = function (pinStore, messageStore, Sessions, publicKey, cb) {
var bytes = 0;
var getMultipleFileSize = function (Env, channels, cb) {
var msgStore = Env.msgStore;
if (!Array.isArray(channels)) { return cb('INVALID_PIN_LIST'); }
if (typeof(msgStore.getChannelSize) !== 'function') {
return cb('GET_CHANNEL_SIZE_UNSUPPORTED');
}
var i = channels.length;
var counts = {};
return void getChannelList(pinStore, Sessions, publicKey, function (channels) {
if (!channels) { cb('NO_ARRAY'); } // unexpected
var done = function () {
i--;
if (i === 0) { return cb(void 0, counts); }
};
channels.forEach(function (channel) {
getFileSize(Env, channel, function (e, size) {
if (e) {
console.error(e);
counts[channel] = -1;
return done();
}
counts[channel] = size;
done();
});
});
};
var getTotalSize = function (Env, publicKey, cb) {
var bytes = 0;
return void getChannelList(Env, publicKey, function (channels) {
if (!channels) { return cb('INVALID_PIN_LIST'); } // unexpected
var count = channels.length;
if (!count) { cb(void 0, 0); }
channels.forEach(function (channel) {
return messageStore.getChannelSize(channel, function (e, size) {
getFileSize(Env, channel, function (e, size) {
count--;
if (!e) { bytes += size; }
if (count === 0) { return cb(void 0, bytes); }
@ -253,24 +361,118 @@ var hashChannelList = function (A) {
return hash;
};
var getHash = function (store, Sessions, publicKey, cb) {
getChannelList(store, Sessions, publicKey, function (channels) {
var getHash = function (Env, publicKey, cb) {
getChannelList(Env, publicKey, function (channels) {
cb(void 0, hashChannelList(channels));
});
};
var storeMessage = function (store, publicKey, msg, cb) {
store.message(publicKey, JSON.stringify(msg), cb);
// The limits object contains storage limits for all the publicKey that have paid
// To each key is associated an object containing the 'limit' value and a 'note' explaining that limit
var limits = {};
var updateLimits = function (config, publicKey, cb) {
if (typeof cb !== "function") { cb = function () {}; }
var defaultLimit = typeof(config.defaultStorageLimit) === 'number'?
config.defaultStorageLimit: DEFAULT_LIMIT;
var userId;
if (publicKey) {
userId = unescapeKeyCharacters(publicKey);
}
var body = JSON.stringify({
domain: config.domain,
subdomain: config.subdomain
});
var options = {
host: 'accounts.cryptpad.fr',
path: '/api/getauthorized',
method: 'POST',
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
}
};
var req = Https.request(options, function (response) {
if (!('' + response.statusCode).match(/^2\d\d$/)) {
return void cb('SERVER ERROR ' + response.statusCode);
}
var str = '';
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
try {
var json = JSON.parse(str);
limits = json;
var l;
if (userId) {
var limit = limits[userId];
l = limit && typeof limit.limit === "number" ?
[limit.limit, limit.plan, limit.note] : [defaultLimit, '', ''];
}
cb(void 0, l);
} catch (e) {
cb(e);
}
});
});
req.on('error', function (e) {
if (!config.domain) { return cb(); }
cb(e);
});
req.end(body);
};
var pinChannel = function (store, Sessions, publicKey, channels, cb) {
var getLimit = function (Env, publicKey, cb) {
var unescapedKey = unescapeKeyCharacters(publicKey);
var limit = limits[unescapedKey];
var defaultLimit = typeof(Env.defaultStorageLimit) === 'number'?
Env.defaultStorageLimit: DEFAULT_LIMIT;
var toSend = limit && typeof(limit.limit) === "number"?
[limit.limit, limit.plan, limit.note] : [defaultLimit, '', ''];
cb(void 0, toSend);
};
var getFreeSpace = function (Env, publicKey, cb) {
getLimit(Env, publicKey, function (e, limit) {
if (e) { return void cb(e); }
getTotalSize(Env, publicKey, function (e, size) {
if (e) { return void cb(e); }
var rem = limit[0] - size;
if (typeof(rem) !== 'number') {
return void cb('invalid_response');
}
cb(void 0, rem);
});
});
};
var sumChannelSizes = function (sizes) {
return Object.keys(sizes).map(function (id) { return sizes[id]; })
.filter(function (x) {
// only allow positive numbers
return !(typeof(x) !== 'number' || x <= 0);
})
.reduce(function (a, b) { return a + b; }, 0);
};
var pinChannel = function (Env, publicKey, channels, cb) {
if (!channels && channels.filter) {
// expected array
return void cb('[TYPE_ERROR] pin expects channel list argument');
return void cb('INVALID_PIN_LIST');
}
getChannelList(store, Sessions, publicKey, function (pinned) {
var session = beginSession(Sessions, publicKey);
// get channel list ensures your session has a cached channel list
getChannelList(Env, publicKey, function (pinned) {
var session = beginSession(Env.Sessions, publicKey);
// only pin channels which are not already pinned
var toStore = channels.filter(function (channel) {
@ -278,28 +480,42 @@ var pinChannel = function (store, Sessions, publicKey, channels, cb) {
});
if (toStore.length === 0) {
return void getHash(store, Sessions, publicKey, cb);
return void getHash(Env, publicKey, cb);
}
store.message(publicKey, JSON.stringify(['PIN', toStore]),
function (e) {
getMultipleFileSize(Env, toStore, function (e, sizes) {
if (e) { return void cb(e); }
toStore.forEach(function (channel) {
session.channels[channel] = true;
var pinSize = sumChannelSizes(sizes);
getFreeSpace(Env, publicKey, function (e, free) {
if (e) {
console.error(e);
return void cb(e);
}
if (pinSize > free) { return void cb('E_OVER_LIMIT'); }
Env.pinStore.message(publicKey, JSON.stringify(['PIN', toStore]),
function (e) {
if (e) { return void cb(e); }
toStore.forEach(function (channel) {
session.channels[channel] = true;
});
getHash(Env, publicKey, cb);
});
});
getHash(store, Sessions, publicKey, cb);
});
});
};
var unpinChannel = function (store, Sessions, publicKey, channels, cb) {
var unpinChannel = function (Env, publicKey, channels, cb) {
var pinStore = Env.pinStore;
if (!channels && channels.filter) {
// expected array
return void cb('[TYPE_ERROR] unpin expects channel list argument');
return void cb('INVALID_PIN_LIST');
}
getChannelList(store, Sessions, publicKey, function (pinned) {
var session = beginSession(Sessions, publicKey);
getChannelList(Env, publicKey, function (pinned) {
var session = beginSession(Env.Sessions, publicKey);
// only unpin channels which are pinned
var toStore = channels.filter(function (channel) {
@ -307,58 +523,308 @@ var unpinChannel = function (store, Sessions, publicKey, channels, cb) {
});
if (toStore.length === 0) {
return void getHash(store, Sessions, publicKey, cb);
return void getHash(Env, publicKey, cb);
}
store.message(publicKey, JSON.stringify(['UNPIN', toStore]),
pinStore.message(publicKey, JSON.stringify(['UNPIN', toStore]),
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);
getHash(Env, publicKey, cb);
});
});
};
var resetUserPins = function (store, Sessions, publicKey, channelList, cb) {
var session = beginSession(Sessions, publicKey);
var resetUserPins = function (Env, publicKey, channelList, cb) {
if (!Array.isArray(channelList)) { return void cb('INVALID_PIN_LIST'); }
var pinStore = Env.pinStore;
var session = beginSession(Env.Sessions, publicKey);
if (!channelList.length) {
return void getHash(Env, publicKey, function (e, hash) {
if (e) { return cb(e); }
cb(void 0, hash);
});
}
var pins = session.channels = {};
store.message(publicKey, JSON.stringify(['RESET', channelList]),
function (e) {
getMultipleFileSize(Env, channelList, function (e, sizes) {
if (e) { return void cb(e); }
channelList.forEach(function (channel) {
pins[channel] = true;
var pinSize = sumChannelSizes(sizes);
getFreeSpace(Env, publicKey, function (e, free) {
if (e) {
console.error(e);
return void cb(e);
}
if (pinSize > free) { return void(cb('E_OVER_LIMIT')); }
pinStore.message(publicKey, JSON.stringify(['RESET', channelList]),
function (e) {
if (e) { return void cb(e); }
channelList.forEach(function (channel) {
pins[channel] = true;
});
getHash(Env, publicKey, function (e, hash) {
cb(e, hash);
});
});
});
});
};
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 safeMkdir = function (path, cb) {
Fs.mkdir(path, function (e) {
if (!e || e.code === 'EEXIST') { return void cb(); }
cb(e);
});
};
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 (Env, publicKey, content, cb) {
var paths = Env.paths;
var dec = new Buffer(Nacl.util.decodeBase64(content)); // jshint ignore:line
var len = dec.length;
var session = beginSession(Env.Sessions, publicKey);
if (typeof(session.currentUploadSize) !== 'number') {
// improperly initialized... maybe they didn't check before uploading?
// reject it, just in case
return cb('NOT_READY');
}
if (session.currentUploadSize > session.pendingUploadSize) {
return cb('E_OVER_LIMIT');
}
if (!session.blobstage) {
makeFileStream(paths.staging, publicKey, function (e, stream) {
if (e) { return void cb(e); }
var blobstage = session.blobstage = stream;
blobstage.write(dec);
session.currentUploadSize += len;
cb(void 0, dec.length);
});
} else {
session.blobstage.write(dec);
session.currentUploadSize += len;
cb(void 0, dec.length);
}
};
var upload_cancel = function (Env, publicKey, cb) {
var paths = Env.paths;
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());
});
};
getHash(store, Sessions, publicKey, function (e, hash) {
cb(e, hash);
var upload_complete = function (Env, publicKey, cb) {
var paths = Env.paths;
var session = beginSession(Env.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('[safeMkdir]');
console.error(e);
console.log();
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);
});
});
};
var retries = 3;
var handleMove = function (e, newPath, id) {
if (e) {
if (retries--) {
setTimeout(function () {
return tryRandomLocation(handleMove);
}, 750);
}
}
// lol wut handle ur errors
Fs.rename(oldPath, newPath, function (e) {
if (e) {
console.error(e);
if (retries--) {
return setTimeout(function () {
tryRandomLocation(handleMove);
}, 750);
}
return cb(e);
}
cb(void 0, id);
});
};
tryRandomLocation(handleMove);
};
var upload_status = function (Env, publicKey, filesize, cb) {
var paths = Env.paths;
// validate that the provided size is actually a positive number
if (typeof(filesize) !== 'number' &&
filesize >= 0) { return void cb('E_INVALID_SIZE'); }
// validate that the provided path is not junk
var filePath = makeFilePath(paths.staging, publicKey);
if (!filePath) { return void cb('E_INVALID_PATH'); }
getFreeSpace(Env, publicKey, function (e, free) {
if (e) { return void cb(e); }
if (filesize >= free) { return cb('NOT_ENOUGH_SPACE'); }
isFile(filePath, function (e, yes) {
if (e) {
console.error("uploadError: [%s]", e);
return cb('UNNOWN_ERROR');
}
cb(e, yes);
});
});
};
RPC.create = function (config, cb) {
/*::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 warn = function (e, output) {
if (e && !config.suppressRPCErrors) {
console.error('[' + e + ']', output);
}
};
var keyOrDefaultString = function (key, def) {
return typeof(config[key]) === 'string'? config[key]: def;
};
var store;
var Env = {};
Env.defaultStorageLimit = config.defaultStorageLimit;
Env.maxUploadSize = config.maxUploadSize || (20 * 1024 * 1024);
var Sessions = Env.Sessions = {};
var paths = Env.paths = {};
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob');
var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
var rpc = function (
ctx /*:{ store: Object }*/,
data /*:Array<Array<any>>*/,
respond /*:(?string, ?Array<any>)=>void*/)
{
if (!Array.isArray(data)) {
return void respond('INVALID_ARG_FORMAT');
}
var rpc = function (ctx, data, respond) {
if (!data.length) {
return void respond("INSUFFICIENT_ARGS");
} else if (data.length !== 1) {
console.log(data.length);
console.log('[UNEXPECTED_ARGUMENTS_LENGTH] %s', data.length);
}
var msg = data[0].slice(0);
if (!Array.isArray(msg)) {
return void respond('INVALID_ARG_FORMAT');
}
var signature = msg.shift();
var publicKey = msg.shift();
@ -380,12 +846,11 @@ RPC.create = function (config, cb) {
return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY');
}
if (checkSignature(serialized, signature, publicKey) !== true) {
return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY");
}
var safeKey = publicKey.replace(/\//g, '-');
var safeKey = escapeKeyCharacters(publicKey);
/* If you have gotten this far, you have signed the message with the
public key which you provided.
@ -396,55 +861,164 @@ RPC.create = function (config, cb) {
msg.shift();
var Respond = function (e, msg) {
var token = Sessions[publicKey].tokens.slice(-1)[0];
var token = Sessions[safeKey].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');
};
if (!Env.msgStore) { Env.msgStore = ctx.store; }
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 resetUserPins(Env, safeKey, msg[1], function (e, hash) {
//warn(e, hash);
return void Respond(e, hash);
});
case 'PIN':
return pinChannel(store, Sessions, safeKey, msg[1], function (e, hash) {
return pinChannel(Env, safeKey, msg[1], function (e, hash) {
warn(e, hash);
Respond(e, hash);
});
case 'UNPIN':
return unpinChannel(store, Sessions, safeKey, msg[1], function (e, hash) {
return unpinChannel(Env, safeKey, msg[1], function (e, hash) {
warn(e, hash);
Respond(e, hash);
});
case 'GET_HASH':
return void getHash(store, Sessions, safeKey, function (e, hash) {
return void getHash(Env, safeKey, function (e, hash) {
warn(e, hash);
Respond(e, hash);
});
case 'GET_TOTAL_SIZE':
return getTotalSize(store, ctx.store, Sessions, safeKey, function (e, size) {
if (e) { return void Respond(e); }
case 'GET_TOTAL_SIZE': // TODO cache this, since it will get called quite a bit
return getTotalSize(Env, safeKey, function (e, size) {
if (e) {
warn(e, safeKey);
return void Respond(e);
}
Respond(e, size);
});
case 'GET_FILE_SIZE':
return void getFileSize(ctx.store, msg[1], Respond);
return void getFileSize(Env, msg[2], function (e, size) {
warn(e, msg[2]);
Respond(e, size);
});
case 'UPDATE_LIMITS':
return void updateLimits(config, safeKey, function (e, limit) {
if (e) {
warn(e, limit);
return void Respond(e);
}
Respond(void 0, limit);
});
case 'GET_LIMIT':
return void getLimit(Env, safeKey, function (e, limit) {
if (e) {
warn(e, limit);
return void Respond(e);
}
Respond(void 0, limit);
});
case 'GET_MULTIPLE_FILE_SIZE':
return void getMultipleFileSize(Env, msg[1], function (e, dict) {
if (e) {
warn(e, dict);
return void Respond(e);
}
Respond(void 0, dict);
});
// restricted to privileged users...
case 'UPLOAD':
if (!privileged) { return deny(); }
return void upload(Env, safeKey, msg[1], function (e, len) {
warn(e, len);
Respond(e, len);
});
case 'UPLOAD_STATUS':
if (!privileged) { return deny(); }
var filesize = msg[1];
return void upload_status(Env, safeKey, msg[1], function (e, yes) {
if (!e && !yes) {
// no pending uploads, set the new size
var user = beginSession(Sessions, safeKey);
user.pendingUploadSize = filesize;
user.currentUploadSize = 0;
}
Respond(e, yes);
});
case 'UPLOAD_COMPLETE':
if (!privileged) { return deny(); }
return void upload_complete(Env, safeKey, function (e, hash) {
warn(e, hash);
Respond(e, hash);
});
case 'UPLOAD_CANCEL':
if (!privileged) { return deny(); }
return void upload_cancel(Env, safeKey, function (e) {
warn(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 = beginSession(Sessions, safeKey);
if (typeof(session.privilege) !== 'boolean') {
return void isPrivilegedUser(publicKey, function (yes) {
session.privilege = yes;
handleMessage(yes);
});
}
// if authenticated, proceed
handleMessage(session.privilege);
};
var updateLimitDaily = function () {
updateLimits(config, undefined, function (e) {
if (e) { console.error('Error updating the storage limits', e); }
});
};
updateLimitDaily();
setInterval(updateLimitDaily, 24*3600*1000);
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);
Env.pinStore = s;
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);
});
});
});
};

@ -8,6 +8,7 @@ var Fs = require('fs');
var WebSocketServer = require('ws').Server;
var NetfluxSrv = require('./node_modules/chainpad-server/NetfluxWebsocketSrv');
var Package = require('./package.json');
var Path = require("path");
var config = require('./config');
var websocketPort = config.websocketPort || config.httpPort;
@ -47,6 +48,21 @@ var setHeaders = (function () {
return function () {};
}());
(function () {
if (!config.logFeedback) { return; }
const logFeedback = function (url) {
url.replace(/\?(.*?)=/, function (all, fb) {
console.log('[FEEDBACK] %s', fb);
});
};
app.head(/^\/common\/feedback\.html/, function (req, res, next) {
logFeedback(req.url);
next();
});
}());
app.use(function (req, res, next) {
setHeaders(req, res);
if (/[\?\&]ver=[^\/]+$/.test(req.url)) { res.setHeader("Cache-Control", "max-age=31536000"); }
@ -67,6 +83,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(Path.join(__dirname, (config.blobPath || './blob'))));
app.use("/customize", Express.static(__dirname + '/customize'));
app.use("/customize", Express.static(__dirname + '/customize.dist'));
app.use(/^\/[^\/]*$/, Express.static('customize'));

@ -46,14 +46,14 @@ While we migrate to our new Netflux API, only the leveldb adaptor will be suppor
## removeChannel(channelName, callback)
This method is called (optionally, see config.js.dist for more info) some amount of time after the last client in a channel disconnects.
This method is called (optionally, see config.example.js for more info) some amount of time after the last client in a channel disconnects.
It should remove any history of that channel, and execute a callback which takes an error message as an argument.
## Documenting your adaptor
Naturally, you should comment your code well before making a PR.
Failing that, you should definitely add notes to `cryptpad/config.js.dist` such that people who wish to install your adaptor know how to do so.
Failing that, you should definitely add notes to `cryptpad/config.example.js` such that people who wish to install your adaptor know how to do so.
Notes on how to install the back end, as well as how to install the client for connecting to the back end (as is the case with many datastores), as well as how to configure cryptpad to use your adaptor.
The current configuration file should serve as an example of what to add, and how to comment.

@ -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();
});

@ -3,8 +3,7 @@
<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-main="main" src="/bower_components/requirejs/require.js"></script>
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<style>
.report {

@ -1,12 +1,10 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([
'/bower_components/jquery/dist/jquery.min.js',
'jquery',
'/bower_components/hyperjson/hyperjson.js',
'/bower_components/textpatcher/TextPatcher.amd.js',
'json.sortify',
'/common/cryptpad-common.js',
], function (jQuery, Hyperjson, TextPatcher, Sortify, Cryptpad) {
var $ = window.jQuery;
], function ($, Hyperjson, TextPatcher, Sortify, Cryptpad) {
window.Hyperjson = Hyperjson;
window.TextPatcher = TextPatcher;
window.Sortify = Sortify;
@ -17,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"]]]]',
@ -62,7 +72,7 @@ define([
};
var HJSON_equal = function (shjson) {
assert(function () {
assert(function (cb) {
// parse your stringified Hyperjson
var hjson;
@ -86,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");
};
@ -98,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;
@ -115,7 +125,7 @@ define([
TextPatcher.log(target.outerHTML, op);
}
return success;
return cb(success);
}, "Round trip serialization introduced artifacts.");
};
@ -129,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 + ")");
};
@ -141,6 +151,59 @@ define([
strungJSON(orig);
});
// check that old hashes parse correctly
assert(function (cb) {
var secret = Cryptpad.parsePadUrl('/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy');
return cb(secret.hashData.channel === "67b8385b07352be53e40746d2be6ccd7" &&
secret.hashData.key === "XAYSuJYYqa9NfmInyHci7LNy" &&
secret.hashData.version === 0);
}, "Old hash failed to parse");
// make sure version 1 hashes parse correctly
assert(function (cb) {
var secret = Cryptpad.parsePadUrl('/pad/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI');
return cb(secret.hashData.version === 1 &&
secret.hashData.mode === "edit" &&
secret.hashData.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" &&
secret.hashData.key === "usn4+9CqVja8Q7RZOGTfRgqI" &&
!secret.hashData.present);
}, "version 1 hash (without present mode) failed to parse");
// test support for present mode in hashes
assert(function (cb) {
var secret = Cryptpad.parsePadUrl('/pad/#/1/edit/CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G/present');
return cb(secret.hashData.version === 1
&& secret.hashData.mode === "edit"
&& secret.hashData.channel === "CmN5+YJkrHFS3NSBg-P7Sg"
&& secret.hashData.key === "DNZ2wcG683GscU4fyOyqA87G"
&& secret.hashData.present);
}, "version 1 hash failed to parse");
// test support for present mode in hashes
assert(function (cb) {
var secret = Cryptpad.parsePadUrl('/pad/#/1/edit//CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G//present');
return cb(secret.hashData.version === 1
&& secret.hashData.mode === "edit"
&& secret.hashData.channel === "CmN5+YJkrHFS3NSBg-P7Sg"
&& secret.hashData.key === "DNZ2wcG683GscU4fyOyqA87G"
&& secret.hashData.present);
}, "Couldn't handle multiple successive slashes");
// test support for trailing slash
assert(function (cb) {
var secret = Cryptpad.parsePadUrl('/pad/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/');
return cb(secret.hashData.version === 1 &&
secret.hashData.mode === "edit" &&
secret.hashData.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" &&
secret.hashData.key === "usn4+9CqVja8Q7RZOGTfRgqI" &&
!secret.hashData.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;
@ -155,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:
@ -176,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.
@ -198,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');
});
});

@ -1,22 +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-main="main" src="/bower_components/requirejs/require.js"></script>
<style>
media {
border: 1px solid black;
height: 100px;
width: 100px;
display: block;
background-color: red;
}
</style>
</head>
<body>
<media> my media thing </media>
<br />
<a href="https://www.w3schools.com/html/html5_new_elements.asp">valid elements</a>

@ -1,9 +0,0 @@
define([
'/bower_components/jquery/dist/jquery.min.js',
], function () {
var $ = window.jQuery;
$('media').each(function () {
window.alert("media tag selection works!");
});
});

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
<style>
html{
width: 100%;
}
body {
width: 90%;
margin: auto;
}
.wrap {
white-space: normal;
}
</style>
</head>
<body>

@ -1,25 +0,0 @@
define([
'/bower_components/hyperjson/hyperjson.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Hyperjson) {
var $ = window.jQuery;
var shjson = '["BODY",{"class":"cke_editable cke_editable_themed cke_contents_ltr cke_show_borders","spellcheck":"false"},[["P",{},["This is ",["STRONG",{},["CryptPad"]],", the zero knowledge realtime collaborative editor.",["BR",{},[]],"What you type here is encrypted so only people who have the link can access it.",["BR",{},[]],"Even the server cannot see what you type."]],["P",{},[["SMALL",{},[["I",{},["What you see here, what you hear here, when you leave here, let it stay here"]]]],["BR",{"type":"_moz"},[]]]]]]';
var hjson = JSON.parse(shjson);
var pretty = Hyperjson.toString(hjson);
// set the body html to the rendered hyperjson
$('body')[0].outerHTML = pretty;
$('body')
// append the stringified-hyperjson source for reference
.append('<hr>').append($('<pre>', {
'class': 'wrap',
}).text(shjson))
// append the pretty-printed html source for reference
.append('<hr>').append($('<pre>').text(pretty));
// TODO write some tests to confirm whether the pretty printer is correct
});

@ -3,7 +3,7 @@
<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-main="main" src="/bower_components/requirejs/require.js"></script>
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
</head>
<body>

@ -1,9 +1,8 @@
define([
'/bower_components/jquery/dist/jquery.min.js',
'jquery',
'/common/cryptpad-common.js',
'/customize/translations/messages.js',
], function (jQuery, Cryptpad, English) {
var $ = window.jQuery;
], function ($, Cryptpad, English) {
var $body = $('body');

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html class="cp">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
</head>
<body class="html">
</body>
</html>

@ -0,0 +1,59 @@
define([
'jquery',
'/common/cryptpad-common.js',
'/bower_components/tweetnacl/nacl-fast.min.js'
], function ($, Cryptpad) {
var Nacl = window.nacl;
var signMsg = function (msg, privKey) {
var signKey = Nacl.util.decodeBase64(privKey);
var buffer = Nacl.util.decodeUTF8(msg);
return Nacl.util.encodeBase64(Nacl.sign(buffer, signKey));
};
// TODO: Allow authing for any domain as long as the user clicks an "accept" button
// inside of the iframe.
var AUTHORIZED_DOMAINS = [
/\.cryptpad\.fr$/,
/^http(s)?:\/\/localhost\:/
];
// Safari is weird about localStorage in iframes but seems to let sessionStorage slide.
localStorage.User_hash = localStorage.User_hash || sessionStorage.User_hash;
Cryptpad.ready(function () {
console.log('IFRAME READY');
$(window).on("message", function (jqe) {
var evt = jqe.originalEvent;
var data = JSON.parse(evt.data);
var domain = evt.origin;
var srcWindow = evt.source;
var ret = { txid: data.txid };
if (data.cmd === 'PING') {
ret.res = 'PONG';
} else if (data.cmd === 'SIGN') {
if (!AUTHORIZED_DOMAINS.filter(function (x) { return x.test(domain); }).length) {
ret.error = "UNAUTH_DOMAIN";
} else if (!Cryptpad.isLoggedIn()) {
ret.error = "NOT_LOGGED_IN";
} else {
var proxy = Cryptpad.getStore().getProxy().proxy;
var sig = signMsg(data.data, proxy.edPrivate);
ret.res = {
uname: proxy.login_name,
edPublic: proxy.edPublic,
sig: sig
};
}
} else if (data.cmd === 'UPDATE_LIMIT') {
return Cryptpad.updatePinLimit(function (e, limit, plan, note) {
ret.res = [limit, plan, note];
srcWindow.postMessage(JSON.stringify(ret), domain);
});
} else {
ret.error = "UNKNOWN_CMD";
}
srcWindow.postMessage(JSON.stringify(ret), domain);
});
});
});

@ -32,32 +32,75 @@
<script src="/bower_components/codemirror/addon/fold/comment-fold.js"></script>
<script src="/bower_components/codemirror/addon/display/placeholder.js"></script>
<style>
html, body{
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
overflow: hidden;
box-sizing: border-box;
position: relative;
}
body {
display: flex;
flex-flow: column;
}
.CodeMirror {
height: 100%;
}
.CodeMirror-focused .cm-matchhighlight {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
background-position: bottom;
background-repeat: repeat-x;
}
html, body{
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
overflow: hidden;
box-sizing: border-box;
position: relative;
}
body {
display: flex;
flex-flow: column;
max-height: 100%;
min-height: auto;
}
.CodeMirror {
display: inline-block;
height: 100%;
width: 50%;
min-width: 20%;
max-width: 80%;
resize: horizontal;
}
.CodeMirror.fullPage {
min-width: 100%;
max-width: 100%;
resize: none;
}
.CodeMirror-focused .cm-matchhighlight {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
background-position: bottom;
background-repeat: repeat-x;
}
#editorContainer {
flex: 1;
display: flex;
flex-flow: row;
height: 100%;
overflow: hidden;
}
#previewContainer {
flex: 1;
padding: 5px 20px;
overflow: auto;
display: inline-block;
height: 100%;
border-left: 1px solid black;
box-sizing: border-box;
font-family: Calibri,Ubuntu,sans-serif;
}
#preview {
max-width: 40vw;
margin: auto;
}
#preview table tr td, #preview table tr th {
border: 1px solid black;
padding: 15px;
}
#preview table tr th {
border: 3px solid black;
}
</style>
</head>
<body>
<div id="cme_toolbox" class="toolbar-container"></div>
<textarea id="editor1" name="editor1"></textarea>
<div id="editorContainer">
<textarea id="editor1" name="editor1"></textarea>
<div id="previewContainer"><div id="preview"></div></div>
</div>
</body>
</html>

@ -1,37 +1,45 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([
'jquery',
'/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',
'/bower_components/jquery/dist/jquery.min.js',
], function (Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Modes, Themes, Visible, Notify) {
var $ = window.jQuery;
var saveAs = window.saveAs;
'/common/diffMarked.js',
], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad,
Cryptget, DiffMd) {
var Messages = Cryptpad.Messages;
var module = window.APP = {
var APP = window.APP = {
Cryptpad: Cryptpad,
};
$(function () {
Cryptpad.addLoadingScreen();
var ifrw = module.ifrw = $('#pad-iframe')[0].contentWindow;
var ifrw = APP.ifrw = $('#pad-iframe')[0].contentWindow;
var stringify = function (obj) {
return JSONSortify(obj);
};
var toolbar;
var editor;
var $iframe = $('#pad-iframe').contents();
var $previewContainer = $iframe.find('#previewContainer');
var $preview = $iframe.find('#preview');
$preview.click(function (e) {
if (!e.target) { return; }
var $t = $(e.target);
if ($t.is('a') || $t.parents('a').length) {
e.preventDefault();
var $a = $t.is('a') ? $t : $t.parents('a').first();
var href = $a.attr('href');
window.open(href);
}
});
var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
@ -39,115 +47,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 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: {"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }},
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 isHistoryMode = false;
var setEditable = module.setEditable = function (bool) {
var setEditable = APP.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,16 +76,18 @@ define([
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys),
setMyID: setMyID,
network: Cryptpad.getNetwork(),
transformFunction: JsonOT.validate,
};
var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); };
var isDefaultTitle = function () {
var parsed = Cryptpad.parsePadUrl(window.location.href);
return Cryptpad.isDefaultName(parsed, document.title);
var setHistory = function (bool, update) {
isHistoryMode = bool;
setEditable(!bool);
if (!bool && update) {
config.onRemote();
}
};
var initializing = true;
@ -175,374 +96,183 @@ define([
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);
};
var forceDrawPreview = function () {
try {
DiffMd.apply(DiffMd.render(editor.getValue()), $preview);
} catch (e) { console.error(e); }
};
var drawPreview = Cryptpad.throttle(function () {
if (CodeMirror.highlightMode !== 'markdown') { return; }
if (!$previewContainer.is(':visible')) { return; }
forceDrawPreview();
}, 150);
var onLocal = config.onLocal = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
if (readOnly) { return; }
editor.save();
var textValue = canonicalize($textarea.val());
drawPreview();
var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson = stringifyInner(textValue);
module.patchText(shjson);
APP.patchText(shjson);
if (module.realtime.getUserDoc() !== shjson) {
if (APP.realtime.getUserDoc() !== shjson) {
console.error("realtime.getUserDoc() !== shjson");
}
};
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');
var onModeChanged = function (mode) {
var $codeMirror = $iframe.find('.CodeMirror');
if (mode === "markdown") {
APP.$previewButton.show();
$previewContainer.show();
$codeMirror.removeClass('fullPage');
return;
}
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);
});
APP.$previewButton.hide();
$previewContainer.hide();
$codeMirror.addClass('fullPage');
};
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 config = {
displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'],
userData: userData,
readOnly: readOnly,
ifrw: ifrw,
var configTb = {
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
};
if (readOnly) {delete config.changeNameID; }
toolbar = module.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, userList, config);
toolbar = APP.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 = {
onLocal: config.onLocal(),
onRemote: config.onRemote(),
setHistory: setHistory,
applyVal: function (val) {
var remoteDoc = JSON.parse(val || '{}').content;
editor.setValue(remoteDoc || '');
editor.save();
},
$toolbar: $bar
};
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist);
/* save as template */
if (!Cryptpad.isTemplate(window.location.href)) {
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);
};
var $previewButton = APP.$previewButton = Cryptpad.createButton(null, true);
$previewButton.removeClass('fa-question').addClass('fa-eye');
$previewButton.attr('title', Messages.previewButtonTitle);
$previewButton.click(function () {
var $codeMirror = $iframe.find('.CodeMirror');
if (CodeMirror.highlightMode !== 'markdown') {
$previewContainer.show();
}
$previewContainer.toggle();
if ($previewContainer.is(':visible')) {
$codeMirror.removeClass('fullPage');
} else {
$codeMirror.addClass('fullPage');
}
});
$rightside.append($previewButton);
if (!readOnly) {
configureLanguage(function () {
configureTheme();
CodeMirror.configureTheme(function () {
CodeMirror.configureLanguage(null, onModeChanged);
});
}
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;
if (module.realtime !== info.realtime) {
var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({
config.onReady = function (info) {
if (APP.realtime !== info.realtime) {
var realtime = APP.realtime = info.realtime;
APP.patchText = TextPatcher.create({
realtime: realtime,
//logging: true
});
}
var userDoc = module.realtime.getUserDoc();
var userDoc = APP.realtime.getUserDoc();
var isNew = false;
if (userDoc === "" || userDoc === "{}") { isNew = true; }
@ -560,154 +290,80 @@ define([
newDoc = hjson.content;
if (hjson.highlightMode) {
setMode(hjson.highlightMode, module.$language);
CodeMirror.setMode(hjson.highlightMode, onModeChanged);
}
}
if (!module.highlightMode) {
setMode('javascript', module.$language);
console.log("%s => %s", module.highlightMode, module.$language.val());
if (!CodeMirror.highlightMode) {
CodeMirror.setMode('markdown', onModeChanged);
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;
}
if (readOnly) {
config.onRemote();
return;
}
return pos;
UserList.getLastName(toolbar.$userNameButton, isNew);
};
var posToCursor = function(position, newText) {
var cursor = {
line: 0,
ch: 0
};
var textLines = newText.substr(0, position).split("\n");
cursor.line = textLines.length - 1;
cursor.ch = textLines[cursor.line].length;
return cursor;
};
var onRemote = config.onRemote = function (info) {
config.onRemote = function () {
if (initializing) { return; }
var scroll = editor.getScrollInfo();
if (isHistoryMode) { return; }
var oldDoc = canonicalize($textarea.val());
var shjson = module.realtime.getUserDoc();
var oldDoc = canonicalize(CodeMirror.$textarea.val());
var shjson = APP.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);
if (highlightMode && highlightMode !== APP.highlightMode) {
CodeMirror.setMode(highlightMode, onModeChanged);
}
//get old cursor here
var oldCursor = {};
oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc);
oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc);
editor.setValue(remoteDoc);
editor.save();
var op = TextPatcher.diff(oldDoc, remoteDoc);
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
return TextPatcher.transformCursor(oldCursor[attr], op);
});
if(selects[0] === selects[1]) {
editor.setCursor(posToCursor(selects[0], remoteDoc));
}
else {
editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc));
}
editor.scrollTo(scroll.left, scroll.top);
CodeMirror.setValueAndCursor(oldDoc, remoteDoc, TextPatcher);
drawPreview();
if (!readOnly) {
var textValue = canonicalize($textarea.val());
var textValue = canonicalize(CodeMirror.$textarea.val());
var shjson2 = stringifyInner(textValue);
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
TextPatcher.log(shjson, TextPatcher.diff(shjson, shjson2));
module.patchText(shjson2);
APP.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) {
@ -719,9 +375,9 @@ define([
}
};
var onError = config.onError = onConnectError;
config.onError = onConnectError;
var realtime = module.realtime = Realtime.start(config);
APP.realtime = Realtime.start(config);
editor.on('change', onLocal);
@ -731,8 +387,9 @@ define([
var interval = 100;
var second = function (CM) {
Cryptpad.ready(function (err, env) {
Cryptpad.ready(function () {
andThen(CM);
Cryptpad.reportAppUsage();
});
Cryptpad.onError(function (info) {
if (info && info.type === "store") {

@ -1,7 +1,22 @@
// This is stage 1, it can be changed but you must bump the version of the project.
define([], function () {
// fix up locations so that relative urls work.
require.config({ baseUrl: window.location.pathname });
require.config({
baseUrl: window.location.pathname,
paths: {
// jquery declares itself as literally "jquery" so it cannot be pulled by path :(
"jquery": "/bower_components/jquery/dist/jquery.min",
// json.sortify same
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify"
}
});
// most of CryptPad breaks if you don't support isArray
if (!Array.isArray) {
Array.isArray = function(arg) { // CRYPTPAD_SHIM
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]);
});

@ -1,19 +1,16 @@
define([
'/bower_components/jquery/dist/jquery.min.js',
], function () {
var $ = window.jQuery;
define(['jquery'], function ($) {
var Clipboard = {};
// 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);
$('body').append($ta);
if (!($ta.length && $ta[0].select)) {
if (!($ta.length && $ta[0].select)) {
// console.log("oops");
return;
}

@ -0,0 +1,300 @@
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, cb) {
exp.highlightMode = mode;
if (mode === 'text') {
editor.setOption('mode', 'text');
if (cb) { cb('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);
}
if(cb) { cb(mode); }
};
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, onModeChanged) {
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 = exp.$language = Cryptpad.createDropdown(dropdownConfig);
$block.find('a').click(function () {
setMode($(this).attr('data-value'), onModeChanged);
onLocal();
});
if ($rightside) { $rightside.append($block); }
if (cb) { cb(); }
};
exp.configureTheme = function (cb) {
/* 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); }
if (cb) { cb(); }
};
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;
});

@ -0,0 +1,322 @@
define([
'/common/common-util.js',
'/common/common-interface.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/tweetnacl/nacl-fast.min.js'
], function (Util, UI, Crypto) {
var Nacl = window.nacl;
var Hash = {};
var uint8ArrayToHex = Util.uint8ArrayToHex;
var hexToBase64 = Util.hexToBase64;
var base64ToHex = Util.base64ToHex;
// This implementation must match that on the server
// it's used for a checksum
Hash.hashChannelList = function (list) {
return Nacl.util.encodeBase64(Nacl.hash(Nacl.util
.decodeUTF8(JSON.stringify(list))));
};
var getEditHashFromKeys = Hash.getEditHashFromKeys = function (chanKey, keys) {
if (typeof keys === 'string') {
return chanKey + keys;
}
if (!keys.editKeyStr) { return; }
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)+'/';
};
var getFileHashFromKeys = Hash.getFileHashFromKeys = function (fileKey, cryptKey) {
return '/1/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(cryptKey) + '/';
};
Hash.getUserHrefFromKeys = function (username, pubkey) {
return window.location.origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-');
};
var fixDuplicateSlashes = function (s) {
return s.replace(/\/+/g, '/');
};
/*
Version 0
/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy
Version 1
/code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI
*/
var parseTypeHash = Hash.parseTypeHash = function (type, hash) {
if (!hash) { return; }
var parsed = {};
var hashArr = fixDuplicateSlashes(hash).split('/');
if (['media', 'file', 'user'].indexOf(type) === -1) {
parsed.type = 'pad';
if (hash.slice(0,1) !== '/' && hash.length >= 56) {
// Old hash
parsed.channel = hash.slice(0, 32);
parsed.key = hash.slice(32, 56);
parsed.version = 0;
return parsed;
}
if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1;
parsed.mode = hashArr[2];
parsed.channel = hashArr[3];
parsed.key = hashArr[4].replace(/-/g, '/');
parsed.present = typeof(hashArr[5]) === "string" && hashArr[5] === 'present';
return parsed;
}
return parsed;
}
if (['media', 'file'].indexOf(type) !== -1) {
parsed.type = 'file';
if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1;
parsed.channel = hashArr[2].replace(/-/g, '/');
parsed.key = hashArr[3].replace(/-/g, '/');
return parsed;
}
return parsed;
}
if (['user'].indexOf(type) !== -1) {
parsed.type = 'user';
if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1;
parsed.user = hashArr[2];
parsed.pubkey = hashArr[3].replace(/-/g, '/');
return parsed;
}
return parsed;
}
return;
};
var parsePadUrl = Hash.parsePadUrl = function (href) {
var patt = /^https*:\/\/([^\/]*)\/(.*?)\//i;
var ret = {};
if (!href) { return ret; }
if (href.slice(-1) !== '/') { href += '/'; }
var idx;
if (!/^https*:\/\//.test(href)) {
idx = href.indexOf('/#');
ret.type = href.slice(1, idx);
ret.hash = href.slice(idx + 2);
ret.hashData = parseTypeHash(ret.type, ret.hash);
return ret;
}
href.replace(patt, function (a, domain, type) {
ret.domain = domain;
ret.type = type;
return '';
});
idx = href.indexOf('/#');
ret.hash = href.slice(idx + 2);
ret.hashData = parseTypeHash(ret.type, ret.hash);
return ret;
};
var getRelativeHref = Hash.getRelativeHref = function (href) {
if (!href) { return; }
if (href.indexOf('#') === -1) { return; }
var parsed = parsePadUrl(href);
return '/' + parsed.type + '/#' + parsed.hash;
};
/*
* 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
*/
Hash.getSecrets = function (type, secretHash) {
var secret = {};
var generate = function () {
secret.keys = Crypto.createEditCryptor();
secret.key = Crypto.createEditCryptor().editKeyStr;
};
if (!secretHash && !/#/.test(window.location.href)) {
generate();
return secret;
} else {
var parsed;
var hash;
if (secretHash) {
if (!type) { throw new Error("getSecrets with a hash requires a type parameter"); }
parsed = parseTypeHash(type, secretHash);
hash = secretHash;
} else {
var pHref = parsePadUrl(window.location.href);
parsed = pHref.hashData;
hash = pHref.hash;
}
//var parsed = parsePadUrl(window.location.href);
//var hash = secretHash || window.location.hash.slice(1);
if (hash.length === 0) {
generate();
return secret;
}
// old hash system : #{hexChanKey}{cryptKey}
// new hash system : #/{hashVersion}/{b64ChanKey}/{cryptKey}
if (parsed.version === 0) {
// Old hash
secret.channel = parsed.channel;
secret.key = parsed.key;
}
else if (parsed.version === 1) {
// New hash
if (parsed.type === "pad") {
secret.channel = base64ToHex(parsed.channel);
if (parsed.mode === 'edit') {
secret.keys = Crypto.createEditCryptor(parsed.key);
secret.key = secret.keys.editKeyStr;
if (secret.channel.length !== 32 || secret.key.length !== 24) {
UI.alert("The channel key and/or the encryption key is invalid");
throw new Error("The channel key and/or the encryption key is invalid");
}
}
else if (parsed.mode === 'view') {
secret.keys = Crypto.createViewCryptor(parsed.key);
if (secret.channel.length !== 32) {
UI.alert("The channel key is invalid");
throw new Error("The channel key is invalid");
}
}
} else if (parsed.type === "file") {
// version 2 hashes are to be used for encrypted blobs
secret.channel = parsed.channel;
secret.keys = { fileKeyStr: parsed.key };
} else if (parsed.type === "user") {
// version 2 hashes are to be used for encrypted blobs
throw new Error("User hashes can't be opened (yet)");
}
}
}
return secret;
};
Hash.getHashes = function (channel, secret) {
var hashes = {};
if (secret.keys.editKeyStr) {
hashes.editHash = getEditHashFromKeys(channel, secret.keys);
}
if (secret.keys.viewKeyStr) {
hashes.viewHash = getViewHashFromKeys(channel, secret.keys);
}
if (secret.keys.fileKeyStr) {
hashes.fileHash = getFileHashFromKeys(channel, secret.keys.fileKeyStr);
}
return hashes;
};
var createChannelId = Hash.createChannelId = function () {
var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(16));
if (id.length !== 32 || /[^a-f0-9]/.test(id)) {
throw new Error('channel ids must consist of 32 hex characters');
}
return id;
};
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('/') + '/';
};
// STORAGE
Hash.findWeaker = function (href, recents) {
var rHref = href || getRelativeHref(window.location.href);
var parsed = parsePadUrl(rHref);
if (!parsed.hash) { return false; }
var weaker;
recents.some(function (pad) {
var p = parsePadUrl(pad.href);
if (p.type !== parsed.type) { return; } // Not the same type
if (p.hash === parsed.hash) { return; } // Same hash, not stronger
var pHash = p.hashData;
var parsedHash = parsed.hashData;
if (!parsedHash || !pHash) { return; }
// We don't have stronger/weaker versions of files or users
if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; }
if (pHash.version !== parsedHash.version) { return; }
if (pHash.channel !== parsedHash.channel) { return; }
if (pHash.mode === 'view' && parsedHash.mode === 'edit') {
weaker = pad.href;
return true;
}
return;
});
return weaker;
};
var findStronger = Hash.findStronger = function (href, recents) {
var rHref = href || getRelativeHref(window.location.href);
var parsed = parsePadUrl(rHref);
if (!parsed.hash) { return false; }
var stronger;
recents.some(function (pad) {
var p = parsePadUrl(pad.href);
if (p.type !== parsed.type) { return; } // Not the same type
if (p.hash === parsed.hash) { return; } // Same hash, not stronger
var pHash = p.hashData;
var parsedHash = parsed.hashData;
if (!parsedHash || !pHash) { return; }
// We don't have stronger/weaker versions of files or users
if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; }
if (pHash.version !== parsedHash.version) { return; }
if (pHash.channel !== parsedHash.channel) { return; }
if (pHash.mode === 'edit' && parsedHash.mode === 'view') {
stronger = pad.href;
return true;
}
return;
});
return stronger;
};
Hash.isNotStrongestStored = function (href, recents) {
return findStronger(href, recents);
};
Hash.hrefToHexChannelId = function (href) {
var parsed = Hash.parsePadUrl(href);
if (!parsed || !parsed.hash) { return; }
parsed = parsed.hashData;
if (parsed.version === 0) {
return parsed.channel;
} else if (parsed.version !== 1 && parsed.version !== 2) {
console.error("parsed href had no version");
console.error(parsed);
return;
}
var channel = parsed.channel;
if (!channel) { return; }
var hex = base64ToHex(channel);
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;
});

@ -0,0 +1,249 @@
define([
'jquery',
'/bower_components/chainpad-json-validator/json-ot.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
], function ($, JsonOT, Crypto) {
var ChainPad = window.ChainPad;
var History = {};
var getStates = function (rt) {
var states = [];
var b = rt.getAuthBlock();
if (b) { states.unshift(b); }
while (b.getParent()) {
b = b.getParent();
states.unshift(b);
}
return states;
};
var loadHistory = function (config, common, cb) {
var network = common.getNetwork();
var hkn = network.historyKeeper;
var wcId = common.hrefToHexChannelId(config.href || window.location.href);
var createRealtime = function () {
return ChainPad.create({
userName: 'history',
initialState: '',
transformFunction: JsonOT.validate,
logLevel: 0,
noPrune: true
});
};
var realtime = createRealtime();
var parsed = config.href ? common.parsePadUrl(config.href) : {};
var secret = common.getSecrets(parsed.type, parsed.hash);
var crypto = Crypto.createEncryptor(secret.keys);
var to = window.setTimeout(function () {
cb('[GET_FULL_HISTORY_TIMEOUT]');
}, 30000);
var parse = function (msg) {
try {
return JSON.parse(msg);
} catch (e) {
return null;
}
};
var onMsg = function (msg) {
var parsed = parse(msg);
if (parsed[0] === 'FULL_HISTORY_END') {
console.log('END');
window.clearTimeout(to);
cb(null, realtime);
return;
}
if (parsed[0] !== 'FULL_HISTORY') { return; }
msg = parsed[1][4];
if (msg) {
msg = msg.replace(/^cp\|/, '');
var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey);
realtime.message(decryptedMsg);
}
};
network.on('message', function (msg) {
onMsg(msg);
});
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', wcId, secret.keys.validateKey]));
};
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;
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;
var realtime;
var states = [];
var c = states.length - 1;
var $hist = $toolbar.find('.cryptpad-toolbar-history');
var $left = $toolbar.find('.cryptpad-toolbar-leftside');
var $right = $toolbar.find('.cryptpad-toolbar-rightside');
var $cke = $toolbar.find('.cke_toolbox_main');
var onUpdate;
var update = function () {
if (!realtime) { return []; }
states = getStates(realtime);
if (typeof onUpdate === "function") { onUpdate(); }
return states;
};
// Get the content of the selected version, and change the version number
var get = function (i) {
i = parseInt(i);
if (isNaN(i)) { return; }
if (i < 0) { i = 0; }
if (i > states.length - 1) { i = states.length - 1; }
var val = states[i].getContent().doc;
c = i;
if (typeof onUpdate === "function") { onUpdate(); }
$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 || '';
};
var getNext = function (step) {
return typeof step === "number" ? get(c + step) : get(c + 1);
};
var getPrevious = function (step) {
return typeof step === "number" ? get(c - step) : get(c - 1);
};
// Create the history toolbar
var display = function () {
$hist.html('').show();
$left.hide();
$right.hide();
$cke.hide();
var $prev =$('<button>', {
'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 buttonPrimary',
title: Messages.history_next
}).appendTo($hist);
$('<label>').text(Messages.history_version).appendTo($nav);
var $cur = $('<input>', {
'class' : 'gotoInput',
'type' : 'number',
'min' : '1',
'max' : states.length
}).val(c + 1).appendTo($nav).mousedown(function (e) {
// stopPropagation because the event would be cancelled by the dropdown menus
e.stopPropagation();
});
var $label2 = $('<label>').text(' / '+ states.length).appendTo($nav);
$('<br>').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);
$label2.text(' / ' + states.length);
};
var close = function () {
$hist.hide();
$left.show();
$right.show();
$cke.show();
};
// Buttons actions
$prev.click(function () { render(getPrevious()); });
$next.click(function () { render(getNext()); });
$cur.keydown(function (e) {
var p = function () { e.preventDefault(); };
if (e.which === 13) { p(); return render( get($cur.val() - 1) ); } // Enter
if ([37, 40].indexOf(e.which) >= 0) { p(); return render(getPrevious()); } // Left
if ([38, 39].indexOf(e.which) >= 0) { p(); return render(getNext()); } // Right
if (e.which === 33) { p(); return render(getNext(10)); } // PageUp
if (e.which === 34) { p(); return render(getPrevious(10)); } // PageUp
if (e.which === 27) { p(); $close.click(); }
}).focus();
$cur.on('change', function () {
render( get($cur.val() - 1) );
});
$close.click(function () {
states = [];
close();
onClose();
});
$rev.click(function () {
common.confirm(Messages.history_restorePrompt, function (yes) {
if (!yes) { return; }
close();
onRevert();
common.log(Messages.history_restoreDone);
});
});
// Display the latest content
render(get(c));
};
// Load all the history messages into a new chainpad object
loadHistory(config, common, function (err, newRt) {
History.loading = false;
if (err) { throw new Error(err); }
realtime = newRt;
update();
c = states.length - 1;
display();
onReady();
});
};
return History;
});

@ -0,0 +1,244 @@
define([
'jquery',
'/customize/messages.js',
'/common/common-util.js',
'/customize/application_config.js',
'/bower_components/alertifyjs/dist/js/alertify.js',
'/common/notify.js',
'/common/visible.js'
], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible) {
var UI = {};
/*
* Alertifyjs
*/
UI.Alertify = Alertify;
// set notification timeout
Alertify._$$alertify.delay = AppConfig.notificationTimeout || 5000;
var findCancelButton = UI.findCancelButton = function () {
return $('button.cancel');
};
var findOKButton = UI.findOKButton = function () {
return $('button.ok');
};
var listenForKeys = UI.listenForKeys = function (yes, no) {
var handler = function (e) {
switch (e.which) {
case 27: // cancel
if (typeof(no) === 'function') { no(e); }
no();
break;
case 13: // enter
if (typeof(yes) === 'function') { yes(e); }
break;
}
};
$(window).keyup(handler);
return handler;
};
var stopListening = UI.stopListening = function (handler) {
$(window).off('keyup', handler);
};
UI.alert = function (msg, cb, force) {
cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); }
var close = function () {
findOKButton().click();
};
var keyHandler = listenForKeys(close, close);
Alertify.alert(msg, function (ev) {
cb(ev);
stopListening(keyHandler);
});
window.setTimeout(function () {
findOKButton().focus();
});
};
UI.prompt = function (msg, def, cb, opt, force) {
opt = opt || {};
cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); }
var keyHandler = listenForKeys(function () { // yes
findOKButton().click();
}, function () { // no
findCancelButton().click();
});
Alertify
.defaultValue(def || '')
.okBtn(opt.ok || Messages.okButton || 'OK')
.cancelBtn(opt.cancel || Messages.cancelButton || 'Cancel')
.prompt(msg, function (val, ev) {
cb(val, ev);
stopListening(keyHandler);
}, function (ev) {
cb(null, ev);
stopListening(keyHandler);
});
};
UI.confirm = function (msg, cb, opt, force, styleCB) {
opt = opt || {};
cb = cb || function () {};
if (force !== true) { msg = Util.fixHTML(msg); }
var keyHandler = listenForKeys(function () {
findOKButton().click();
}, function () {
findCancelButton().click();
});
Alertify
.okBtn(opt.ok || Messages.okButton || 'OK')
.cancelBtn(opt.cancel || Messages.cancelButton || 'Cancel')
.confirm(msg, function () {
cb(true);
stopListening(keyHandler);
}, function () {
cb(false);
stopListening(keyHandler);
});
window.setTimeout(function () {
var $ok = findOKButton();
var $cancel = findCancelButton();
if (opt.okClass) { $ok.addClass(opt.okClass); }
if (opt.cancelClass) { $cancel.addClass(opt.cancelClass); }
if (opt.reverseOrder) {
$ok.insertBefore($ok.prev());
}
if (typeof(styleCB) === 'function') {
styleCB($ok.closest('.dialog'));
}
}, 0);
};
UI.log = function (msg) {
Alertify.success(Util.fixHTML(msg));
};
UI.warn = function (msg) {
Alertify.error(Util.fixHTML(msg));
};
/*
* spinner
*/
UI.spinner = function (parent) {
var $target = $('<span>', {
'class': 'fa fa-spinner fa-pulse fa-4x fa-fw'
}).hide();
$(parent).append($target);
return {
show: function () {
$target.css('display', 'inline');
return this;
},
hide: function () {
$target.hide();
return this;
},
get: function () {
return $target;
},
};
};
var LOADING = 'loading';
var getRandomTip = function () {
if (!Messages.tips || !Object.keys(Messages.tips).length) { return ''; }
var keys = Object.keys(Messages.tips);
var rdm = Math.floor(Math.random() * keys.length);
return Messages.tips[keys[rdm]];
};
UI.addLoadingScreen = function (loadingText, hideTips) {
var $loading, $container;
if ($('#' + LOADING).length) {
$loading = $('#' + LOADING).show();
if (loadingText) {
$('#' + LOADING).find('p').text(loadingText);
}
$container = $loading.find('.loadingContainer');
} else {
$loading = $('<div>', {id: LOADING});
$container = $('<div>', {'class': 'loadingContainer'});
$container.append('<img class="cryptofist" src="/customize/cryptofist_small.png" />');
var $spinner = $('<div>', {'class': 'spinnerContainer'});
UI.spinner($spinner).show();
var $text = $('<p>').text(loadingText || Messages.loading);
$container.append($spinner).append($text);
$loading.append($container);
$('body').append($loading);
}
if (Messages.tips && !hideTips) {
var $loadingTip = $('<div>', {'id': 'loadingTip'});
$('<span>', {'class': 'tips'}).text(getRandomTip()).appendTo($loadingTip);
$loadingTip.css({
'top': $('body').height()/2 + $container.height()/2 + 20 + 'px'
});
$('body').append($loadingTip);
}
};
UI.removeLoadingScreen = function (cb) {
$('#' + LOADING).fadeOut(750, cb);
$('#loadingTip').css('top', '');
window.setTimeout(function () {
$('#loadingTip').fadeOut(750);
}, 3000);
};
UI.errorLoadingScreen = function (error, transparent) {
if (!$('#' + LOADING).is(':visible')) { UI.addLoadingScreen(undefined, true); }
$('.spinnerContainer').hide();
if (transparent) { $('#' + LOADING).css('opacity', 0.8); }
$('#' + LOADING).find('p').html(error || Messages.error);
};
// 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) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = function (e) { f(e.target.result, file); };
reader.readAsText(file, type);
});
};
};
return UI;
});

@ -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,30 +1,30 @@
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 ")
.replace(/ +$/, "")
.split(" ");
var byteString = String.fromCharCode.apply(null, hexArray);
return window.btoa(byteString).replace(/\//g, '-').slice(0,-2);
return window.btoa(byteString).replace(/\//g, '-').replace(/=+$/, '');
};
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,10 +76,60 @@ define([], function () {
/*
* Saving files
*/
var fixFileName = Util.fixFileName = function (filename) {
Util.fixFileName = function (filename) {
return filename.replace(/ /g, '-').replace(/[\/\?]/g, '_')
.replace(/_+/g, '_');
};
var oneKilobyte = 1024;
var oneMegabyte = 1024 * oneKilobyte;
var oneGigabyte = 1024 * oneMegabyte;
Util.bytesToGigabytes = function (bytes) {
return Math.ceil(bytes / oneGigabyte * 100) / 100;
};
Util.bytesToMegabytes = function (bytes) {
return Math.ceil(bytes / oneMegabyte * 100) / 100;
};
Util.bytesToKilobytes = function (bytes) {
return Math.ceil(bytes / oneKilobyte * 100) / 100;
};
Util.magnitudeOfBytes = function (bytes) {
if (bytes >= oneGigabyte) { return 'GB'; }
else if (bytes >= oneMegabyte) { return 'MB'; }
};
Util.fetch = function (src, cb) {
var done = false;
var CB = function (err, res) {
if (done) { return; }
done = true;
cb(err, res);
};
var xhr = new XMLHttpRequest();
xhr.open("GET", src, true);
xhr.responseType = "arraybuffer";
xhr.onload = function () {
if (/^4/.test(''+this.status)) {
return CB('XHR_ERROR');
}
return void CB(void 0, new Uint8Array(xhr.response));
};
xhr.send(null);
};
Util.throttle = function (f, ms) {
var to;
var g = function () {
window.clearTimeout(to);
to = window.setTimeout(f, ms);
};
return g;
};
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,
};

@ -1,12 +1,12 @@
define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/cryptpad-common.js',
'/bower_components/textpatcher/TextPatcher.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Crypto, Realtime, Cryptpad, TextPatcher) {
var Messages = Cryptpad.Messages;
var noop = function () {};
'/bower_components/textpatcher/TextPatcher.js'
], function ($, Crypto, Realtime, Cryptpad, TextPatcher) {
//var Messages = Cryptpad.Messages;
//var noop = function () {};
var finish = function (S, err, doc) {
if (S.done) { return; }
S.cb(err, doc);
@ -22,7 +22,8 @@ define([
};
var makeConfig = function (hash) {
var secret = Cryptpad.getSecrets(hash);
// We can't use cryptget with a file or a user so we can use 'pad' as hash type
var secret = Cryptpad.getSecrets('pad', hash);
if (!secret.keys) { secret.keys = secret.key; } // support old hashses
var config = {
websocketURL: Cryptpad.getWebsocketURL(),
@ -50,14 +51,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 +88,7 @@ define([
};
overwrite(config, opt);
var realtime = Session.session = Realtime.start(config);
Session.session = Realtime.start(config);
};
return {

File diff suppressed because it is too large Load Diff

@ -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)) {

@ -0,0 +1,127 @@
define([
'jquery',
'/bower_components/marked/marked.min.js',
'/bower_components/diff-dom/diffDOM.js'
],function ($, Marked) {
var DiffMd = {};
var DiffDOM = window.diffDOM;
var renderer = new Marked.Renderer();
Marked.setOptions({
renderer: renderer
});
DiffMd.render = function (md) {
return Marked(md);
};
// Tasks list
var checkedTaskItemPtn = /^\s*\[x\]\s*/;
var uncheckedTaskItemPtn = /^\s*\[ \]\s*/;
renderer.listitem = function (text) {
var isCheckedTaskItem = checkedTaskItemPtn.test(text);
var isUncheckedTaskItem = uncheckedTaskItemPtn.test(text);
if (isCheckedTaskItem) {
text = text.replace(checkedTaskItemPtn,
'<i class="fa fa-check-square" aria-hidden="true"></i>&nbsp;') + '\n';
}
if (isUncheckedTaskItem) {
text = text.replace(uncheckedTaskItemPtn,
'<i class="fa fa-square-o" aria-hidden="true"></i>&nbsp;') + '\n';
}
var cls = (isCheckedTaskItem || isUncheckedTaskItem) ? ' class="todo-list-item"' : '';
return '<li'+ cls + '>' + text + '</li>\n';
};
var forbiddenTags = [
'SCRIPT',
'IFRAME',
'OBJECT',
'APPLET',
'VIDEO',
'AUDIO',
];
var unsafeTag = function (info) {
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
if (/^on/.test(info.diff.name)) {
console.log("Rejecting forbidden element attribute with name", info.diff.name);
return true;
}
}
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
var msg = "Rejecting forbidden tag of type (%s)";
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
console.log(msg, info.diff.element.nodeName);
return true;
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeName) !== -1) {
console.log("Replacing restricted element type (%s) with PRE", info.diff.newValue.nodeName);
info.diff.newValue.nodeName = 'PRE';
}
}
};
var slice = function (coll) {
return Array.prototype.slice.call(coll);
};
/* remove listeners from the DOM */
var removeListeners = function (root) {
slice(root.attributes).map(function (attr) {
if (/^on/.test(attr.name)) {
root.attributes.removeNamedItem(attr.name);
}
});
// all the way down
slice(root.children).forEach(removeListeners);
};
var domFromHTML = function (html) {
var Dom = new DOMParser().parseFromString(html, "text/html");
removeListeners(Dom.body);
return Dom;
};
var DD = new DiffDOM({
preDiffApply: function (info) {
if (unsafeTag(info)) { return true; }
}
});
var makeDiff = function (A, B, id) {
var Err;
var Els = [A, B].map(function (frag) {
if (typeof(frag) === 'object') {
if (!frag || (frag && !frag.body)) {
Err = "No body";
return;
}
var els = frag.body.querySelectorAll('#'+id);
if (els.length) {
return els[0];
}
}
Err = 'No candidate found';
});
if (Err) { return Err; }
var patch = DD.diff(Els[0], Els[1]);
return patch;
};
DiffMd.apply = function (newHtml, $content) {
var id = $content.attr('id');
if (!id) { throw new Error("The element must have a valid id"); }
var $div = $('<div>', {id: id}).append(newHtml);
var Dom = domFromHTML($('<div>').append($div).html());
var oldDom = domFromHTML($content[0].outerHTML);
var patch = makeDiff(oldDom, Dom, id);
if (typeof(patch) === 'string') {
throw new Error(patch);
} else {
DD.apply($content[0], patch);
}
};
return DiffMd;
});

@ -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);
}, '');

@ -1,7 +1,7 @@
<!DOCTYPE html>
<head>
<title>About our feedback api</title>
<script data-main="feedback-main.js" src="/bower_components/requirejs/require.js"></script>
<script data-bootload="feedback-main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<style>
body {
max-width: 60vw;
@ -11,7 +11,7 @@ body {
</style>
</head>
<body>
<p data-localization="feedback_about">If you're reading this, you were probably curious why CryptPad is requesting web pages when you perform certain actions.</p>
<p data-localization="feedback_about">If you're reading this, you were probably curious why CryptPad is requesting web pages when you perform certain actions</p>
<p data-localization="feedback_privacy">We care about your privacy, and at the same time we want CryptPad to be very easy to use.
We use this file to figure out which UI features matter to our users, by requesting it along with a parameter specifying which action was taken.</p>
<p data-localization="feedback_optout">If you would like to opt out, visit <a href="/settings/">your user settings page</a>, where you'll find a checkbox to enable or disable user feedback</p>

@ -1,7 +1,6 @@
define([
'/bower_components/jquery/dist/jquery.min.js',
], function () {
var $ = window.jQuery;
'jquery',
], function ($) {
var module = {};
var Messages = {};
@ -11,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;
@ -19,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);
};
@ -35,7 +34,7 @@ define([
console.error.apply(console, arguments);
};
var getStructure = exp.getStructure = function () {
exp.getStructure = function () {
var a = {};
a[ROOT] = {};
a[UNSORTED] = [];
@ -93,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;
};
@ -101,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; }
@ -115,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) {
@ -138,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) {
@ -190,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;
};
@ -206,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]) {
@ -245,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 {
@ -262,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) {
@ -352,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
@ -403,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)
@ -510,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);
@ -724,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
@ -736,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; }
@ -749,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] = {};
@ -768,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();
@ -800,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;
@ -839,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();
@ -861,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 = [];
@ -890,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;
@ -915,7 +910,7 @@ define([
};
var forgetPad = exp.forgetPad = function (href) {
exp.forgetPad = function (href) {
if (workgroup) { return; }
if (!href || !isFile(href)) { return; }
var path;
@ -986,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);
@ -1013,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");
@ -1032,7 +1027,7 @@ define([
}
};
var listTemplates = exp.listTemplates = function (type) {
exp.listTemplates = function () {
if (workgroup) { return; }
var templateFiles = getTemplateFiles();
var res = [];
@ -1050,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
@ -1139,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);

@ -1,10 +1,10 @@
define([
'jquery',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js?v=0.1.5',
'/bower_components/textpatcher/TextPatcher.amd.js',
'/common/fileObject.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Listmap, Crypto, TextPatcher, FO) {
'/common/userObject.js',
], function ($, Listmap, Crypto, TextPatcher, FO) {
/*
This module uses localStorage, which is synchronous, but exposes an
asyncronous API. This is so that we can substitute other storage
@ -13,7 +13,6 @@ define([
To override these methods, create another file at:
/customize/storage.js
*/
var $ = window.jQuery;
var Store = {};
var store;
@ -89,21 +88,23 @@ define([
ret.removeData = filesOp.removeData;
ret.pushData = filesOp.pushData;
ret.addPad = function (href, path, name) {
filesOp.addPad(href, path, name);
ret.addPad = function (data, path) {
filesOp.add(data, path);
};
ret.forgetPad = function (href, cb) {
filesOp.forgetPad(href);
filesOp.forget(href);
cb();
};
ret.addTemplate = function (href) {
filesOp.addTemplate(href);
};
ret.listTemplates = function () {
return filesOp.listTemplates();
var templateFiles = filesOp.getFiles(['template']);
var res = [];
templateFiles.forEach(function (f) {
var data = filesOp.getFileData(f);
res.push(JSON.parse(JSON.stringify(data)));
});
return res;
};
ret.getProxy = function () {
@ -123,16 +124,24 @@ define([
};
ret.replaceHref = function (o, n) {
return filesOp.replaceHref(o, n);
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
@ -144,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);
}
if (sessionStorage) { sessionStorage.setItem('User_hash', localStorage.getItem('User_hash')); }
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;
}
@ -154,10 +194,22 @@ define([
proxy.uid = Cryptpad.createChannelId();
}
proxy.on('change', [Cryptpad.displayNameKey], function (o, n, p) {
// if the user is logged in, but does not have signing keys...
if (Cryptpad.isLoggedIn() && !Cryptpad.hasSigningKeys(proxy)) {
return void requestLogin();
}
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;
@ -170,7 +222,7 @@ define([
if (!hash) {
throw new Error('[Store.init] Unable to find or create a drive hash. Aborting...');
}
var secret = Cryptpad.getSecrets(hash);
var secret = Cryptpad.getSecrets('drive', hash);
var listmapConfig = {
data: {},
websocketURL: Cryptpad.getWebsocketURL(),
@ -185,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;

@ -1,12 +1,12 @@
define([
'jquery',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptpad-common.js',
'/common/credential.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
'/bower_components/scrypt-async/scrypt-async.min.js', // better load speed
'/bower_components/jquery/dist/jquery.min.js',
], function (Listmap, Crypto, Cryptpad, Cred) {
], function ($, Listmap, Crypto, Cryptpad, Cred) {
var Exports = {
Cred: Cred,
};
@ -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

@ -1,8 +1,7 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/common/fileObject.js',
'/common/userObject.js',
'json.sortify'
], function (Cryptpad, Crypt, FO, Sortify) {
var exp = {};
@ -47,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]))) {
@ -76,12 +75,12 @@ define([
console.error(msg || "Unable to find that path", path);
};
if (path[0] === FO.TRASH && path.length === 4) {
href = oldFo.getTrashElementData(path);
if (oldFo.isInTrashRoot(path)) {
href = oldFo.find(path.slice(0,3));
path.pop();
}
var p, next, nextRoot;
var next, nextRoot;
path.forEach(function (p, i) {
if (!root) { return; }
if (typeof(p) === "string") {
@ -129,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(); }
@ -156,13 +155,13 @@ define([
var newData = Cryptpad.getStore().getProxy();
var newFo = newData.fo;
var newRecentPads = proxy.drive[Cryptpad.storageKey];
var newFiles = newFo.getFilesDataFiles();
var oldFiles = oldFo.getFilesDataFiles();
var newFiles = newFo.getFiles([newFo.FILES_DATA]);
var oldFiles = oldFo.getFiles([newFo.FILES_DATA]);
oldFiles.forEach(function (href) {
// Do not migrate a pad if we already have it, it would create a duplicate in the drive
if (newFiles.indexOf(href) !== -1) { return; }
// If we have a stronger version, do not add the current href
if (Cryptpad.findStronger(href, newRecentPads)) { return; }
if (Cryptpad.findStronger(href, newRecentPads)) { console.log(href); return; }
// If we have a weaker version, replace the href by the new one
// NOTE: if that weaker version is in the trash, the strong one will be put in unsorted
var weaker = Cryptpad.findWeaker(href, newRecentPads);
@ -176,7 +175,7 @@ define([
return;
});
// Update the file in the drive
newFo.replaceHref(weaker, href);
newFo.replace(weaker, href);
return;
}
// Here it means we have a new href, so we should add it to the drive at its old location
@ -189,6 +188,10 @@ define([
newRecentPads.push(data);
}
});
if (!proxy.FS_hashes || !Array.isArray(proxy.FS_hashes)) {
proxy.FS_hashes = [];
}
proxy.FS_hashes.push(localStorage.FS_hash);
}
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,9 +1,6 @@
define([
'/common/rpc.js',
'/bower_components/tweetnacl/nacl-fast.min.js'
], function (Rpc) {
var Nacl = window.nacl;
var create = function (network, proxy, cb) {
if (!network) {
window.setTimeout(function () {
@ -41,21 +38,26 @@ define([
// you can ask the server to pin a particular channel for you
exp.pin = function (channels, cb) {
if (!Array.isArray(channels)) {
window.setTimeout(function () {
cb('[TypeError] pin expects an array');
});
return;
}
rpc.send('PIN', channels, cb);
};
// you can also ask to unpin a particular channel
exp.unpin = function (channels, cb) {
if (!Array.isArray(channels)) {
window.setTimeout(function () {
cb('[TypeError] pin expects an array');
});
return;
}
rpc.send('UNPIN', channels, cb);
};
// This implementation must match that on the server
// it's used for a checksum
exp.hashChannelList = function (list) {
return Nacl.util.encodeBase64(Nacl.hash(Nacl.util
.decodeUTF8(JSON.stringify(list))));
};
// ask the server what it thinks your hash is
exp.getServerHash = function (cb) {
rpc.send('GET_HASH', edPublic, function (e, hash) {
@ -67,21 +69,120 @@ define([
};
// if local and remote hashes don't match, send a reset
exp.reset = function (list, cb) {
rpc.send('RESET', list, function (e, response) {
exp.reset = function (channels, cb) {
if (!Array.isArray(channels)) {
window.setTimeout(function () {
cb('[TypeError] pin expects an array');
});
return;
}
rpc.send('RESET', channels, function (e, response) {
if (e) {
return void cb(e);
}
if (!response.length) {
console.log(response);
return void cb('INVALID_RESPONSE');
}
cb(e, response[0]);
});
};
// get the total stored size of a channel's patches (in bytes)
exp.getFileSize = function (file, cb) {
rpc.send('GET_FILE_SIZE', file, cb);
rpc.send('GET_FILE_SIZE', file, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length) {
cb(void 0, response[0]);
} else {
cb('INVALID_RESPONSE');
}
});
};
// take a list of channels and return a dictionary of their sizes
exp.getMultipleFileSize = function (files, cb) {
if (!Array.isArray(files)) {
return window.setTimeout(function () {
cb('[TypeError] pin expects an array');
});
}
rpc.send('GET_MULTIPLE_FILE_SIZE', files, function (e, res) {
if (e) { return void cb(e); }
if (typeof(res) !== 'object') {
return void cb('INVALID_RESPONSE');
}
});
};
// get the combined size of all channels (in bytes) for all the
// channels which the server has pinned for your publicKey
exp.getFileListSize = function (cb) {
rpc.send('GET_TOTAL_SIZE', undefined, cb);
rpc.send('GET_TOTAL_SIZE', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length) {
cb(void 0, response[0]);
} else {
cb('INVALID_RESPONSE');
}
});
};
// Update the limit value for all the users and return the limit for your publicKey
exp.updatePinLimits = function (cb) {
rpc.send('UPDATE_LIMITS', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && typeof(response[0]) === "number") {
cb (void 0, response[0], response[1], response[2]);
} else {
cb('INVALID_RESPONSE');
}
});
};
// Get the storage limit associated with your publicKey
exp.getLimit = function (cb) {
rpc.send('GET_LIMIT', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && typeof(response[0]) === "number") {
cb (void 0, response[0], response[1], response[2]);
} else {
cb('INVALID_RESPONSE');
}
});
};
exp.uploadComplete = function (cb) {
rpc.send('UPLOAD_COMPLETE', null, function (e, res) {
if (e) { return void cb(e); }
var id = res[0];
if (typeof(id) !== 'string') {
return void cb('INVALID_ID');
}
cb(void 0, id);
});
};
exp.uploadStatus = function (size, cb) {
if (typeof(size) !== 'number') {
return void window.setTimeout(function () {
cb('INVALID_SIZE');
});
}
rpc.send('UPLOAD_STATUS', size, function (e, res) {
if (e) { return void cb(e); }
var pending = res[0];
if (typeof(pending) !== 'boolean') {
return void cb('INVALID_RESPONSE');
}
cb(void 0, pending);
});
};
exp.uploadCancel = function (cb) {
rpc.send('UPLOAD_CANCEL', void 0, function (e) {
if (e) { return void cb(e); }
cb();
});
};
cb(e, exp);

@ -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, });

@ -22,7 +22,7 @@ define(function () {
"isotope isotope.css",
"lesser-dark lesser-dark.css",
"liquibyte liquibyte.css",
"lol lol.css",
"LOL lol.css",
"material material.css",
"mbo mbo.css",
"mdn-like mdn-like.css",

@ -1,9 +1,8 @@
define([
'jquery',
'/customize/application_config.js',
'/api/config',
'/bower_components/jquery/dist/jquery.min.js'
], function (Config, ApiConfig) {
var $ = window.jQuery;
'/api/config'
], function ($, Config, ApiConfig) {
var Messages = {};
@ -17,12 +16,15 @@ 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';
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';
var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner';
@ -74,7 +76,8 @@ define([
})
.append($('<div>', {'class': TOP_CLS}))
.append($('<div>', {'class': LEFTSIDE_CLS}))
.append($('<div>', {'class': RIGHTSIDE_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 || typeof config !== "object") {
@ -93,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);
@ -204,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
@ -222,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); }
});
@ -371,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);
@ -390,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) {
@ -411,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;
@ -473,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) {
@ -502,10 +537,13 @@ define([
// User dropdown
if (config.displayed.indexOf('useradmin') !== -1) {
var userMenuCfg = {
displayNameCls: USERNAME_CLS,
changeNameButtonCls: USERBUTTON_CLS,
};
var userMenuCfg = {};
if (!config.hideDisplayName) {
userMenuCfg = {
displayNameCls: USERNAME_CLS,
changeNameButtonCls: USERBUTTON_CLS,
};
}
if (readOnly !== 1) {
userMenuCfg.displayName = 1;
userMenuCfg.displayChangeName = 1;
@ -520,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 = ''; }
@ -673,25 +712,27 @@ define([
}
// Update user list
if (userData) {
userList.change.push(function (newUserData) {
var users = userList.users;
if (users.indexOf(myUserName) !== -1) { connected = true; }
if (!connected) { return; }
checkSynchronizing(users, myUserName, $stateElement);
updateUserList(config, myUserName, userListElement, users, userData, readOnly, $userAdminElement);
});
} else {
userList.change.push(function () {
var users = userList.users;
if (users.indexOf(myUserName) !== -1) { connected = true; }
if (!connected) { return; }
checkSynchronizing(users, myUserName, $stateElement);
});
if (userList) {
if (userData) {
userList.change.push(function (newUserData) {
var users = userList.users;
if (users.indexOf(myUserName) !== -1) { connected = true; }
if (!connected) { return; }
checkSynchronizing(users, myUserName, $stateElement);
updateUserList(config, myUserName, userListElement, users, userData, readOnly, $userAdminElement);
});
} else {
userList.change.push(function () {
var users = userList.users;
if (users.indexOf(myUserName) !== -1) { connected = true; }
if (!connected) { return; }
checkSynchronizing(users, myUserName, $stateElement);
});
}
}
// Display notifications when users are joining/leaving the session
var oldUserData;
if (typeof Cryptpad !== "undefined") {
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; }
@ -757,7 +798,7 @@ define([
for (var k in newdata) {
if (k !== myUserName && userList.users.indexOf(k) !== -1) {
if (typeof oldUserData[k] === "undefined") {
// if the same uid is already present in the userdata, don't notify
// if the same uid is already present in the userdata, don't notify
if (!userPresent(k, newdata[k], oldUserData)) {
notify(1, newdata[k].name);
}
@ -776,14 +817,16 @@ define([
};
};
realtime.onPatch(ks());
realtime.onMessage(ks(true));
if (realtime) {
realtime.onPatch(ks());
realtime.onMessage(ks(true));
checkLag(getLag, lagElement);
setInterval(function () {
if (!connected) { return; }
checkLag(getLag, lagElement);
}, 3000);
setInterval(function () {
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._getKey('pinLimitReachedAlert', [encodeURIComponent(window.location.hostname)]), 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

@ -0,0 +1,953 @@
define([
'jquery',
'/customize/application_config.js'
], function ($, AppConfig) {
var module = {};
var ROOT = module.ROOT = "root";
var UNSORTED = module.UNSORTED = "unsorted";
var TRASH = module.TRASH = "trash";
var TEMPLATE = module.TEMPLATE = "template";
module.init = function (files, config) {
var exp = {};
var Cryptpad = config.Cryptpad;
var Messages = Cryptpad.Messages;
var FILES_DATA = module.FILES_DATA = exp.FILES_DATA = Cryptpad.storageKey;
var NEW_FOLDER_NAME = Messages.fm_newFolder;
var NEW_FILE_NAME = Messages.fm_newFile;
// Logging
var logging = function () {
console.log.apply(console, arguments);
};
var log = config.log || logging;
var logError = config.logError || logging;
var debug = config.debug || logging;
var error = exp.error = function() {
exp.fixFiles();
console.error.apply(console, arguments);
};
// TODO: workgroup
var workgroup = config.workgroup;
/*
* UTILS
*/
exp.getStructure = function () {
var a = {};
a[ROOT] = {};
a[TRASH] = {};
a[FILES_DATA] = [];
a[TEMPLATE] = [];
return a;
};
var getHrefArray = function () {
return [TEMPLATE];
};
var compareFiles = function (fileA, fileB) { return fileA === fileB; };
var isFile = exp.isFile = function (element) {
return typeof(element) === "string";
};
exp.isReadOnlyFile = function (element) {
if (!isFile(element)) { return false; }
var parsed = Cryptpad.parsePadUrl(element);
if (!parsed) { return false; }
var pHash = parsed.hashData;
return pHash && pHash.mode === 'view';
};
var isFolder = exp.isFolder = function (element) {
return typeof(element) === "object";
};
exp.isFolderEmpty = function (element) {
if (typeof(element) !== "object") { return false; }
return Object.keys(element).length === 0;
};
exp.hasSubfolder = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; }
var subfolder = 0;
var addSubfolder = function (el) {
subfolder += isFolder(el.element) ? 1 : 0;
};
for (var f in element) {
if (trashRoot) {
if ($.isArray(element[f])) {
element[f].forEach(addSubfolder);
}
} else {
subfolder += isFolder(element[f]) ? 1 : 0;
}
}
return subfolder;
};
exp.hasFile = function (element, trashRoot) {
if (typeof(element) !== "object") { return false; }
var file = 0;
var addFile = function (el) {
file += isFile(el.element) ? 1 : 0;
};
for (var f in element) {
if (trashRoot) {
if ($.isArray(element[f])) {
element[f].forEach(addFile);
}
} else {
file += isFile(element[f]) ? 1 : 0;
}
}
return file;
};
// Get data from AllFiles (Cryptpad_RECENTPADS)
var getFileData = exp.getFileData = function (file) {
if (!file) { return; }
var res;
files[FILES_DATA].some(function(arr) {
var href = arr.href;
if (href === file) {
res = arr;
return true;
}
return false;
});
return res;
};
// Data from filesData
var getTitle = exp.getTitle = function (href) {
if (workgroup) { debug("No titles in workgroups"); return; }
var data = getFileData(href);
if (!href || !data) {
error("getTitle called with a non-existing href: ", href);
return;
}
return data.title;
};
// PATHS
var comparePath = exp.comparePath = function (a, b) {
if (!a || !b || !$.isArray(a) || !$.isArray(b)) { return false; }
if (a.length !== b.length) { return false; }
var result = true;
var i = a.length - 1;
while (result && i >= 0) {
result = a[i] === b[i];
i--;
}
return result;
};
var isSubpath = exp.isSubpath = function (path, parentPath) {
var pathA = parentPath.slice();
var pathB = path.slice(0, pathA.length);
return comparePath(pathA, pathB);
};
var isPathIn = exp.isPathIn = function (path, categories) {
if (!categories) { return; }
var idx = categories.indexOf('hrefArray');
if (idx !== -1) {
categories.splice(idx, 1);
categories = categories.concat(getHrefArray());
}
return categories.some(function (c) {
return Array.isArray(path) && path[0] === c;
});
};
var isInTrashRoot = exp.isInTrashRoot = function (path) {
return path[0] === TRASH && path.length === 4;
};
// FIND
var findElement = function (root, pathInput) {
if (!pathInput) {
error("Invalid path:\n", pathInput, "\nin root\n", root);
return;
}
if (pathInput.length === 0) { return root; }
var path = pathInput.slice();
var key = path.shift();
if (typeof root[key] === "undefined") {
debug("Unable to find the key '" + key + "' in the root object provided:", root);
return;
}
return findElement(root[key], path);
};
var find = exp.find = function (path) {
return findElement(files, path);
};
// GET FILES
var getFilesRecursively = function (root, arr) {
for (var e in root) {
if (isFile(root[e])) {
if(arr.indexOf(root[e]) === -1) { arr.push(root[e]); }
} else {
getFilesRecursively(root[e], arr);
}
}
};
var _getFiles = {};
_getFiles['array'] = function (cat) {
if (!files[cat]) { files[cat] = []; }
return files[cat].slice();
};
getHrefArray().forEach(function (c) {
_getFiles[c] = function () { return _getFiles['array'](c); };
});
_getFiles['hrefArray'] = function () {
var ret = [];
getHrefArray().forEach(function (c) {
ret = ret.concat(_getFiles[c]());
});
return Cryptpad.deduplicateString(ret);
};
_getFiles[ROOT] = function () {
var ret = [];
getFilesRecursively(files[ROOT], ret);
return ret;
};
_getFiles[TRASH] = function () {
var root = files[TRASH];
var ret = [];
var addFiles = function (el) {
if (isFile(el.element)) {
if(ret.indexOf(el.element) === -1) { ret.push(el.element); }
} else {
getFilesRecursively(el.element, ret);
}
};
for (var e in root) {
if (!$.isArray(root[e])) {
error("Trash contains a non-array element");
return;
}
root[e].forEach(addFiles);
}
return ret;
};
_getFiles[FILES_DATA] = function () {
var ret = [];
files[FILES_DATA].forEach(function (el) {
if (el.href && ret.indexOf(el.href) === -1) {
ret.push(el.href);
}
});
return ret;
};
var getFiles = exp.getFiles = function (categories) {
var ret = [];
if (!categories || !categories.length) {
categories = [ROOT, 'hrefArray', TRASH, FILES_DATA];
}
categories.forEach(function (c) {
if (typeof _getFiles[c] === "function") {
ret = ret.concat(_getFiles[c]());
}
});
return Cryptpad.deduplicateString(ret);
};
// SEARCH
var _findFileInRoot = function (path, href) {
if (!isPathIn(path, [ROOT, TRASH])) { return []; }
var paths = [];
var root = find(path);
var addPaths = function (p) {
if (paths.indexOf(p) === -1) {
paths.push(p);
}
};
if (isFile(root)) {
if (compareFiles(href, root)) {
if (paths.indexOf(path) === -1) {
paths.push(path);
}
}
return paths;
}
for (var e in root) {
var nPath = path.slice();
nPath.push(e);
_findFileInRoot(nPath, href).forEach(addPaths);
}
return paths;
};
exp.findFileInRoot = function (href) {
return _findFileInRoot([ROOT], href);
};
var _findFileInHrefArray = function (rootName, href) {
var unsorted = files[rootName].slice();
var ret = [];
var i = -1;
while ((i = unsorted.indexOf(href, i+1)) !== -1){
ret.push([rootName, i]);
}
return ret;
};
var _findFileInTrash = function (path, href) {
var root = find(path);
var paths = [];
var addPaths = function (p) {
if (paths.indexOf(p) === -1) {
paths.push(p);
}
};
if (path.length === 1 && typeof(root) === 'object') {
Object.keys(root).forEach(function (key) {
var arr = root[key];
if (!Array.isArray(arr)) { return; }
var nPath = path.slice();
nPath.push(key);
_findFileInTrash(nPath, href).forEach(addPaths);
});
}
if (path.length === 2) {
if (!Array.isArray(root)) { return []; }
root.forEach(function (el, i) {
var nPath = path.slice();
nPath.push(i);
nPath.push('element');
if (isFile(el.element)) {
if (compareFiles(href, el.element)) {
addPaths(nPath);
}
return;
}
_findFileInTrash(nPath, href).forEach(addPaths);
});
}
if (path.length >= 4) {
_findFileInRoot(path, href).forEach(addPaths);
}
return paths;
};
var findFile = exp.findFile = function (href) {
var rootpaths = _findFileInRoot([ROOT], href);
var templatepaths = _findFileInHrefArray(TEMPLATE, href);
var trashpaths = _findFileInTrash([TRASH], href);
return rootpaths.concat(templatepaths, trashpaths);
};
exp.search = function (value) {
if (typeof(value) !== "string") { return []; }
var res = [];
// Search in ROOT
var findIn = function (root) {
Object.keys(root).forEach(function (k) {
if (isFile(root[k])) {
if (k.toLowerCase().indexOf(value.toLowerCase()) !== -1) {
res.push(root[k]);
}
return;
}
findIn(root[k]);
});
};
findIn(files[ROOT]);
// Search in TRASH
var trash = files[TRASH];
Object.keys(trash).forEach(function (k) {
if (k.toLowerCase().indexOf(value.toLowerCase()) !== -1) {
trash[k].forEach(function (el) {
if (isFile(el.element)) {
res.push(el.element);
}
});
}
trash[k].forEach(function (el) {
if (isFolder(el.element)) {
findIn(el.element);
}
});
});
// Search title
var allFilesList = files[FILES_DATA].slice();
allFilesList.forEach(function (t) {
if (t.title && t.title.toLowerCase().indexOf(value.toLowerCase()) !== -1) {
res.push(t.href);
}
});
// Search Href
var href = Cryptpad.getRelativeHref(value);
if (href) {
res.push(href);
}
res = Cryptpad.deduplicateString(res);
var ret = [];
res.forEach(function (l) {
//var paths = findFile(l);
ret.push({
paths: findFile(l),
data: exp.getFileData(l)
});
});
return ret;
};
/**
* OPERATIONS
*/
var getAvailableName = function (parentEl, name) {
if (typeof(parentEl[name]) === "undefined") { return name; }
var newName = name;
var i = 1;
while (typeof(parentEl[newName]) !== "undefined") {
newName = name + "_" + i;
i++;
}
return newName;
};
// FILES DATA
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();
});
};
var spliceFileData = exp.removeData = function (idx) {
var data = files[FILES_DATA][idx];
if (typeof data === "object" && Cryptpad.isLoggedIn() && AppConfig.enablePinning) {
Cryptpad.unpinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) {
if (e) { return void logError(e); }
debug('UNPIN', hash);
});
}
files[FILES_DATA].splice(idx, 1);
};
// MOVE
var pushToTrash = function (name, element, path) {
var trash = files[TRASH];
if (typeof(trash[name]) === "undefined") { trash[name] = []; }
var trashArray = trash[name];
var trashElement = {
element: element,
path: path
};
trashArray.push(trashElement);
};
var copyElement = function (elementPath, newParentPath) {
if (comparePath(elementPath, newParentPath)) { return; } // Nothing to do...
var element = find(elementPath);
var newParent = find(newParentPath);
// Move to Trash
if (isPathIn(newParentPath, [TRASH])) {
if (!elementPath || elementPath.length < 2 || elementPath[0] === TRASH) {
debug("Can't move an element from the trash to the trash: ", elementPath);
return;
}
var key = elementPath[elementPath.length - 1];
var elName = isPathIn(elementPath, ['hrefArray']) ? getTitle(element) : key;
var parentPath = elementPath.slice();
parentPath.pop();
pushToTrash(elName, element, parentPath);
return true;
}
// Move to hrefArray
if (isPathIn(newParentPath, ['hrefArray'])) {
if (isFolder(element)) {
log(Messages.fo_moveUnsortedError);
return;
} else {
if (elementPath[0] === newParentPath[0]) { return; }
var fileRoot = newParentPath[0];
if (files[fileRoot].indexOf(element) === -1) {
files[fileRoot].push(element);
}
return true;
}
}
// Move to root
var name;
if (isPathIn(elementPath, ['hrefArray'])) {
name = getTitle(element);
} else if (isInTrashRoot(elementPath)) {
// Element from the trash root: elementPath = [TRASH, "{dirName}", 0, 'element']
name = elementPath[1];
} else {
name = elementPath[elementPath.length-1];
}
var newName = !isPathIn(elementPath, [ROOT]) ? getAvailableName(newParent, name) : name;
if (typeof(newParent[newName]) !== "undefined") {
log(Messages.fo_unavailableName);
return;
}
newParent[newName] = element;
return true;
};
var move = exp.move = function (paths, newPath, cb) {
// Copy the elements to their new location
var toRemove = [];
paths.forEach(function (p) {
var parentPath = p.slice();
parentPath.pop();
if (comparePath(parentPath, newPath)) { return; }
if (isSubpath(newPath, p)) {
log(Messages.fo_moveFolderToChildError);
return;
}
// Try to copy, and if success, remove the element from the old location
if (copyElement(p, newPath)) {
toRemove.push(p);
}
});
exp.delete(toRemove, cb);
};
exp.restore = function (path, cb) {
if (!isInTrashRoot(path)) { return; }
var parentPath = path.slice();
parentPath.pop();
var oldPath = find(parentPath).path;
move([path], oldPath, cb);
};
// ADD
var add = exp.add = function (data, path) {
if (!Cryptpad.isLoggedIn()) { return; }
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(',');
}
// Add to href array
if (path && isPathIn(newPath, ['hrefArray'])) {
parentEl = find(newPath);
parentEl.push(href);
return;
}
// 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;
}
}
};
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();
pushFileData({
href: href,
title: fileName,
atime: +new Date(),
ctime: +new Date()
}, function (err) {
if (err) {
logError(err);
return void cb(err);
}
parentEl[fileName] = href;
var newPath = filePath.slice();
newPath.push(fileName);
cb(void 0, {
newPath: newPath
});
});
};
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(void 0, {
newPath: newPath
});
};
// FORGET (move with href not path)
exp.forget = function (href) {
if (!Cryptpad.isLoggedIn()) {
// delete permanently
var data = getFileData(href);
if (data) {
var i = find([FILES_DATA]).indexOf(data);
if (i !== -1) {
exp.removePadAttribute(href);
spliceFileData(i);
}
}
return;
}
var paths = findFile(href);
move(paths, [TRASH]);
};
// DELETE
// 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 = exp.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) {
debug("Deleting pad attribute in the realtime object");
files[key] = undefined;
delete files[key];
}
});
};
var checkDeletedFiles = function () {
// Nothing in FILES_DATA for workgroups
if (workgroup || !Cryptpad.isLoggedIn()) { return; }
var filesList = getFiles([ROOT, 'hrefArray', TRASH]);
var toRemove = [];
files[FILES_DATA].forEach(function (arr) {
var f = arr.href;
if (filesList.indexOf(f) === -1) {
toRemove.push(arr);
}
});
toRemove.forEach(function (f) {
var idx = files[FILES_DATA].indexOf(f);
if (idx !== -1) {
debug("Removing", f, "from filesData");
spliceFileData(idx);
removePadAttribute(f.href);
}
});
};
var deleteHrefs = function (hrefs) {
hrefs.forEach(function (obj) {
var idx = files[obj.root].indexOf(obj.href);
files[obj.root].splice(idx, 1);
});
};
var deleteMultipleTrashRoot = function (roots) {
roots.forEach(function (obj) {
var idx = files[TRASH][obj.name].indexOf(obj.el);
files[TRASH][obj.name].splice(idx, 1);
});
};
var deleteMultiplePermanently = function (paths, nocheck) {
var hrefPaths = paths.filter(function(x) { return isPathIn(x, ['hrefArray']); });
var rootPaths = paths.filter(function(x) { return isPathIn(x, [ROOT]); });
var trashPaths = paths.filter(function(x) { return isPathIn(x, [TRASH]); });
var allFilesPaths = paths.filter(function(x) { return isPathIn(x, [FILES_DATA]); });
if (!Cryptpad.isLoggedIn()) {
var toSplice = [];
allFilesPaths.forEach(function (path) {
var el = find(path);
toSplice.push(el);
});
toSplice.forEach(function (el) {
var i = find([FILES_DATA]).indexOf(el);
if (i === -1) { return; }
removePadAttribute(el.href);
console.log(el.href);
spliceFileData(i);
});
return;
}
var hrefs = [];
hrefPaths.forEach(function (path) {
var href = find(path);
hrefs.push({
root: path[0],
href: href
});
});
deleteHrefs(hrefs);
rootPaths.forEach(function (path) {
var parentPath = path.slice();
var key = parentPath.pop();
var parentEl = find(parentPath);
parentEl[key] = undefined;
delete parentEl[key];
});
var trashRoot = [];
trashPaths.forEach(function (path) {
var parentPath = path.slice();
var key = parentPath.pop();
var parentEl = find(parentPath);
// Trash root: we have array here, we can't just splice with the path otherwise we might break the path
// of another element in the loop
if (path.length === 4) {
trashRoot.push({
name: path[1],
el: parentEl
});
return;
}
// Trash but not root: it's just a tree so remove the key
parentEl[key] = undefined;
delete parentEl[key];
});
deleteMultipleTrashRoot(trashRoot);
// In some cases, we want to remove pads from a location without removing them from
// FILES_DATA (replaceHref)
if (!nocheck) { checkDeletedFiles(); }
};
exp.delete = function (paths, cb, nocheck) {
deleteMultiplePermanently(paths, nocheck);
if (typeof cb === "function") { cb(); }
};
exp.emptyTrash = function (cb) {
files[TRASH] = {};
checkDeletedFiles();
if(cb) { cb(); }
};
// RENAME
exp.rename = function (path, newName, cb) {
if (path.length <= 1) {
logError('Renaming `root` is forbidden');
return;
}
if (!newName || newName.trim() === "") { return; }
// Copy the element path and remove the last value to have the parent path and the old name
var element = find(path);
var parentPath = path.slice();
var oldName = parentPath.pop();
if (oldName === newName) {
return;
}
var parentEl = find(parentPath);
if (typeof(parentEl[newName]) !== "undefined") {
log(Messages.fo_existingNameError);
return;
}
parentEl[newName] = element;
parentEl[oldName] = undefined;
delete parentEl[oldName];
if (typeof cb === "function") { cb(); }
};
// REPLACE
var replaceFile = function (path, o, n) {
var root = find(path);
if (isFile(root)) { return; }
for (var e in root) {
if (isFile(root[e])) {
if (compareFiles(o, root[e])) {
root[e] = n;
}
} else {
var nPath = path.slice();
nPath.push(e);
replaceFile(nPath, o, n);
}
}
};
// Replace a href by a stronger one everywhere in the drive (except FILES_DATA)
exp.replace = function (o, n) {
if (!isFile(o) || !isFile(n)) { return; }
var paths = findFile(o);
// Remove all the occurences in the trash
// Replace all the occurences not in the trash
// If all the occurences are in the trash or no occurence, add the pad to unsorted
var allInTrash = true;
paths.forEach(function (p) {
if (p[0] === TRASH) {
exp.delete(p, null, true); // 3rd parameter means skip "checkDeletedFiles"
return;
} else {
allInTrash = false;
var parentPath = p.slice();
var key = parentPath.pop();
var parentEl = find(parentPath);
parentEl[key] = n;
}
});
if (allInTrash) {
add(n);
}
};
/**
* INTEGRITY CHECK
*/
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
// * TRASH: Trash root contains only arrays, each element of the array is an object {element:.., path:..}
// * 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'
// * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT
debug("Cleaning file system...");
var before = JSON.stringify(files);
var fixRoot = function (elem) {
if (typeof(files[ROOT]) !== "object") { debug("ROOT was not an object"); files[ROOT] = {}; }
var element = elem || files[ROOT];
for (var el in element) {
if (!isFile(element[el]) && !isFolder(element[el])) {
debug("An element in ROOT was not a folder nor a file. ", element[el]);
element[el] = undefined;
delete element[el];
} else if (isFolder(element[el])) {
fixRoot(element[el]);
}
}
};
var fixTrashRoot = function () {
if (typeof(files[TRASH]) !== "object") { debug("TRASH was not an object"); files[TRASH] = {}; }
var tr = files[TRASH];
var toClean;
var addToClean = function (obj, idx) {
if (typeof(obj) !== "object") { toClean.push(idx); return; }
if (!isFile(obj.element) && !isFolder(obj.element)) { toClean.push(idx); return; }
if (!$.isArray(obj.path)) { toClean.push(idx); return; }
};
for (var el in tr) {
if (!$.isArray(tr[el])) {
debug("An element in TRASH root is not an array. ", tr[el]);
tr[el] = undefined;
delete tr[el];
} else {
toClean = [];
tr[el].forEach(addToClean);
for (var i = toClean.length-1; i>=0; i--) {
tr[el].splice(toClean[i], 1);
}
}
}
};
// Make sure unsorted doesn't exist anymore
var fixUnsorted = function () {
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 root = find([ROOT]);
us.forEach(function (el) {
if (!isFile(el) || rootFiles.indexOf(el) !== -1) {
return;
}
var data = getFileData(el);
var name = data ? data.title : NEW_FILE_NAME;
var newName = getAvailableName(root, name);
root[newName] = el;
});
delete files[UNSORTED];
};
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]).slice();
var toClean = [];
us.forEach(function (el, idx) {
if (!isFile(el) || rootFiles.indexOf(el) !== -1) {
toClean.push(idx);
}
});
toClean.forEach(function (idx) {
us.splice(idx, 1);
});
};
var fixFilesData = function () {
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) {
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 (Cryptpad.isLoggedIn() && rootFiles.indexOf(el.href) === -1) {
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;
}
});
toClean.forEach(function (el) {
var idx = fd.indexOf(el);
if (idx !== -1) {
spliceFileData(idx);
}
});
};
fixRoot();
fixTrashRoot();
if (!workgroup) {
fixUnsorted();
fixTemplate();
fixFilesData();
}
if (JSON.stringify(files) !== before) {
debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely");
return;
}
debug("File system was clean");
};
return exp;
};
return module;
});

@ -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;
@ -141,6 +154,9 @@ span.fa-folder-open {
min-width: 30px;
cursor: pointer;
}
#tree #allfilesTree {
margin-top: 0;
}
#tree #searchContainer {
text-align: center;
padding: 5px 0;
@ -215,6 +231,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;
@ -236,13 +259,16 @@ span.fa-folder-open {
margin-left: 10px;
float: right;
}
#content .info-box.noclose {
padding-right: 10px;
}
#content li {
cursor: default;
}
#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 +315,9 @@ span.fa-folder-open {
width: 50px;
font-size: 40px;
}
#content .element .truncated {
display: none;
}
#content div.grid {
padding: 20px;
}
@ -296,19 +325,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 +362,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 +372,9 @@ span.fa-folder-open {
#content div.grid .listElement {
display: none;
}
#content .list {
padding-left: 20px;
}
#content .list ul {
display: table;
width: 100%;
@ -401,7 +450,7 @@ span.fa-folder-open {
#driveToolbar {
background: #ddd;
color: #555;
height: 40px;
height: 30px;
display: flex;
flex-flow: row;
border-top: 1px solid #ccc;
@ -409,6 +458,7 @@ span.fa-folder-open {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 100;
box-sizing: content-box;
padding: 0 6px;
/* The container <div> - needed to position the dropdown content */
}
#driveToolbar .newPadContainer {
@ -416,24 +466,32 @@ span.fa-folder-open {
height: 100%;
}
#driveToolbar button {
height: 30px;
height: 24px;
font: 12px Ubuntu, Arial, sans-serif;
}
#driveToolbar button span {
font: 12px Ubuntu, Arial, sans-serif;
}
#driveToolbar button .fa,
#driveToolbar button.fa {
font-family: FontAwesome;
}
#driveToolbar button.element {
border-radius: 2px;
background: #888;
color: #eee;
font-size: 16px;
border: none;
font-size: 14px;
border: 1px solid #888;
font-weight: bold;
}
#driveToolbar button.element:hover {
box-shadow: 0px 0px 2px #000;
background: #777;
}
#driveToolbar button.new {
padding: 0 5px;
}
#driveToolbar .dropdown-bar {
margin: 5px 5px;
margin: 2px 2px;
line-height: 1em;
position: relative;
display: inline-block;
@ -467,7 +525,7 @@ span.fa-folder-open {
#driveToolbar .path {
display: inline-block;
height: 100%;
line-height: 40px;
line-height: 30px;
cursor: default;
width: auto;
overflow: hidden;
@ -489,7 +547,7 @@ span.fa-folder-open {
}
#driveToolbar #contextButtonsContainer {
float: right;
margin: 5px;
margin: 2px;
}
#driveToolbar #contextButtonsContainer button {
vertical-align: top;

@ -1,3 +1,5 @@
@import "../../customize.dist/src/less/variables.less";
@tree-bg: #fff;
@tree-fg: #000;
@tree-lines-col: #888;
@ -17,6 +19,8 @@
@toolbar-fg: #555;
@toolbar-border-col: #ccc;
@toolbar-button-bg: #888;
@toolbar-button-border: #888;
@toolbar-button-bg-hover: #777;
@toolbar-button-fg: #eee;
@toolbar-path-bg: #fff;
@toolbar-path-border: #888;
@ -32,7 +36,7 @@ html, body {
padding: 0;
margin: 0;
position: relative;
font-size: 20px;
font-size: 16px;
overflow: auto;
}
@ -70,6 +74,10 @@ body {
}
}
div:focus {
outline: none;
}
.fa {
/*min-width: 17px;*/
margin-right: 3px;
@ -96,7 +104,7 @@ li {
.contextMenu {
display: none;
position: absolute;
z-index: 50;
z-index: 500;
li {
padding: 0;
font-size: 16px;
@ -121,6 +129,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;
@ -180,6 +198,9 @@ span {
}
}
}
#allfilesTree {
margin-top: 0;
}
#searchContainer {
text-align: center;
padding: 5px 0;
@ -260,6 +281,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;
}
@ -279,6 +307,9 @@ span {
margin-left: 10px;
float: right;
}
&.noclose {
padding-right: 10px;
}
}
li {
cursor: default;
@ -287,7 +318,7 @@ span {
/*pointer-events: none;*/
}
&:hover {
&:not(.selected) {
&:not(.selected, .selectedTmp) {
background-color: @drive-hover;
}
.name {
@ -343,25 +374,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 +421,8 @@ span {
.fa {
display: block;
margin: auto;
font-size: 40px;
font-size: 48px;
margin: 8px 0;
text-align: center;
&.listonly {
display: none;
@ -384,6 +436,7 @@ span {
.list {
// Make it act as a table!
padding-left: 20px;
ul {
display: table;
width: 100%;
@ -466,7 +519,7 @@ span {
#driveToolbar {
background: @toolbar-bg;
color: @toolbar-fg;
height: 40px;
height: 30px;
display: flex;
flex-flow: row;
border-top: 1px solid @toolbar-border-col;
@ -474,6 +527,7 @@ span {
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
z-index: 100;
box-sizing: content-box;
padding: 0 6px;
.newPadContainer {
display: inline-block;
@ -481,16 +535,23 @@ span {
}
button {
height: 30px;
height: 24px;
font: @toolbar-button-font;
span {
font: @toolbar-button-font;
}
.fa, &.fa {
font-family: FontAwesome;
}
&.element {
border-radius: 2px;
background: @toolbar-button-bg;
color: @toolbar-button-fg;
font-size: 16px;
border: none;
font-size: 14px;
border: 1px solid @toolbar-button-border;
font-weight: bold;
&:hover {
box-shadow: 0px 0px 2px #000;
background: @toolbar-button-bg-hover;
}
}
&.new {
@ -499,7 +560,7 @@ span {
}
/* The container <div> - needed to position the dropdown content */
.dropdown-bar {
margin: 5px 5px;
margin: 2px 2px;
line-height: 1em;
position: relative;
display: inline-block;
@ -534,7 +595,7 @@ span {
.path {
display: inline-block;
height: 100%;
line-height: 40px;
line-height: 30px;
cursor: default;
width: auto;
overflow: hidden;
@ -556,7 +617,7 @@ span {
}
#contextButtonsContainer {
float: right;
margin: 5px;
margin: 2px;
button {
vertical-align: top;
}

@ -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

@ -1,7 +1,6 @@
define([
'/bower_components/jquery/dist/jquery.min.js',
],function () {
var $ = window.jQuery;
'jquery'
],function ($) {
var Board = {};
var proxy;
@ -170,6 +169,7 @@ define([
var $input = Input({
placeholder: 'card description',
id: id,
})
.addClass('card-title');
@ -207,7 +207,7 @@ define([
/*
*/
Card.move = function (uid, A, B) {
Card.move = function (/*uid, A, B*/) {
};
@ -229,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);
});

@ -1,18 +1,18 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'jquery',
'/api/config',
'/customize/messages.js',
'board.js',
'/bower_components/textpatcher/TextPatcher.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptpad-common.js',
'/common/visible.js',
'/common/notify.js',
'/bower_components/file-saver/FileSaver.min.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Config, Messages, Board, TextPatcher, Listmap, Crypto, Cryptpad, Visible, Notify) {
var $ = window.jQuery;
var saveAs = window.saveAs;
//'/common/visible.js',
//'/common/notify.js',
'/bower_components/file-saver/FileSaver.min.js'
], function ($, Config, Messages, Board, TextPatcher, Listmap, Crypto, Cryptpad /*, Visible, Notify*/) {
// 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,16 +1,14 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'jquery',
'/api/config',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js',
'json.sortify',
'ula.js',
'/bower_components/chainpad-json-validator/json-ot.js',
'/common/cryptpad-common.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT, Cryptpad) {
var $ = window.jQuery;
'/common/cryptpad-common.js'
], function ($, Config, Realtime, Crypto, TextPatcher, Sortify, Formula, JsonOT, Cryptpad) {
var secret = Cryptpad.getSecrets();
@ -130,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;
@ -142,7 +140,7 @@ define([
};
var readValues = function () {
UI.each(function (ui, i, list) {
UI.each(function (ui) {
Map[ui.id] = ui.value();
});
};
@ -167,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();
@ -180,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);
});
}
@ -197,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");
@ -211,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,162 +0,0 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js',
'/common/cryptpad-common.js',
'/bower_components/jquery/dist/jquery.min.js'
], function (Config, Realtime, Crypto, TextPatcher, Cryptpad) {
var $ = window.jQuery;
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>

@ -1,11 +1,10 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'jquery',
'/api/config',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/common/cryptpad-common.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Config, RtListMap, Crypto, Common) {
var $ = window.jQuery;
'/common/cryptpad-common.js'
], function ($, Config, RtListMap, Crypto, Common) {
var secret = Common.getSecrets();
@ -27,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
@ -43,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) {
@ -52,7 +49,7 @@ define([
return false;
})
// on(event, cb)
.on('disconnect', function (info) {
.on('disconnect', function () {
setEditable(false);
window.alert("Network connection lost");
});
@ -66,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();

@ -1,11 +1,9 @@
require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } });
define([
'jquery',
'/common/cryptpad-common.js',
'/common/pinpad.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Cryptpad, Pinpad) {
var $ = window.jQuery;
var APP = window.APP = {
'/common/pinpad.js'
], function ($, Cryptpad, Pinpad) {
window.APP = {
Cryptpad: Cryptpad,
};
@ -39,7 +37,7 @@ define([
};
$(function () {
Cryptpad.ready(function (err, env) {
Cryptpad.ready(function () {
var network = Cryptpad.getNetwork();
var proxy = Cryptpad.getStore().getProxy().proxy;

@ -1,11 +1,8 @@
define([
'/common/cryptget.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Crypt) {
var $ = window.jQuery;
'jquery',
'/common/cryptget.js'
], function ($, Crypt) {
var $target = $('#target');
var $dest = $('#dest');
var useDoc = function (err, doc) {
if (err) { return console.error(err); }

@ -1,14 +1,13 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'jquery',
'/api/config',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/marked/marked.min.js',
'/bower_components/hyperjson/hyperjson.js',
'/common/cryptpad-common.js',
'/bower_components/jquery/dist/jquery.min.js',
'/bower_components/diff-dom/diffDOM.js',
], function (Config, Realtime, Crypto, Marked, Hyperjson, Cryptpad) {
var $ = window.jQuery;
], function ($, Config, Realtime, Crypto, Marked, Hyperjson, Cryptpad) {
var DiffDom = window.diffDOM;
var secret = Cryptpad.getSecrets();
@ -56,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;
};
@ -74,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));
@ -82,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 */
@ -97,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);
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save