diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 000000000..4a58bdcde --- /dev/null +++ b/.flowconfig @@ -0,0 +1,7 @@ +[ignore] + +[include] + +[libs] + +[options] diff --git a/.gitignore b/.gitignore index 996e55b97..139fab33c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ www/scratch data npm-debug.log pins/ +blob/ +privileged.conf diff --git a/.jshintignore b/.jshintignore index ae60d4ba0..919395546 100644 --- a/.jshintignore +++ b/.jshintignore @@ -9,4 +9,7 @@ server.js NetFluxWebsocketSrv.js NetFluxWebsocketServer.js WebRTCSrv.js +www/common/media-tag.js +www/scratch +www/common/toolbar.js diff --git a/.jshintrc b/.jshintrc index c55ec0518..4928c524d 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,7 +10,7 @@ "notypeof": true, "shadow": false, "undef": true, - "unused": false, + "unused": true, "futurehostile":true, "browser": true, "predef": [ diff --git a/.travis.yml b/.travis.yml index 24288c6f2..4160b8719 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 & diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b1d2515f1..0d1d94873 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 - - diff --git a/config.js.dist b/config.example.js similarity index 66% rename from config.js.dist rename to config.example.js index 6ef7267c1..fe3f2fb91 100644 --- a/config.js.dist +++ b/config.example.js @@ -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 */ diff --git a/container-start.sh b/container-start.sh index 89f3be1f1..2aa4ae10f 100755 --- a/container-start.sh +++ b/container-start.sh @@ -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 diff --git a/customize.dist/about.html b/customize.dist/about.html index f3e87dd25..954a3fb6f 100644 --- a/customize.dist/about.html +++ b/customize.dist/about.html @@ -8,7 +8,6 @@ - @@ -107,7 +106,7 @@
- + diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index 8f190a9ad..24ed2c740 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -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; }); diff --git a/customize.dist/ckeditor-config.js b/customize.dist/ckeditor-config.js index 38e1165e6..45e3bd5f5 100644 --- a/customize.dist/ckeditor-config.js +++ b/customize.dist/ckeditor-config.js @@ -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/' diff --git a/customize.dist/contact.html b/customize.dist/contact.html index fc98c6eba..5d43d15a5 100644 --- a/customize.dist/contact.html +++ b/customize.dist/contact.html @@ -8,7 +8,6 @@ - @@ -104,7 +103,7 @@
- + diff --git a/customize.dist/index.html b/customize.dist/index.html index 02a10078d..a1b1fe56b 100644 --- a/customize.dist/index.html +++ b/customize.dist/index.html @@ -8,7 +8,6 @@ - @@ -226,7 +225,7 @@
- + diff --git a/customize.dist/main.css b/customize.dist/main.css index 026649207..063324504 100644 --- a/customize.dist/main.css +++ b/customize.dist/main.css @@ -388,6 +388,11 @@ right: 0; text-align: center; } +@media screen and (max-height: 600px) { + .cp #loadingTip { + display: none; + } +} .cp #loadingTip span { background-color: #302B28; color: #fafafa; @@ -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; } diff --git a/customize.dist/main.js b/customize.dist/main.js index f4720afca..088e1f51a 100644 --- a/customize.dist/main.js +++ b/customize.dist/main.js @@ -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"); }); }); - diff --git a/customize.dist/messages.js b/customize.dist/messages.js index ba35f1d96..2dac5b6c9 100644 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -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]); diff --git a/customize.dist/privacy.html b/customize.dist/privacy.html index dd1a7a686..35bab1958 100644 --- a/customize.dist/privacy.html +++ b/customize.dist/privacy.html @@ -8,7 +8,6 @@ - @@ -125,7 +124,7 @@
- + diff --git a/customize.dist/share/frame.js b/customize.dist/share/frame.js index 9f604af23..2698372fd 100644 --- a/customize.dist/share/frame.js +++ b/customize.dist/share/frame.js @@ -10,7 +10,7 @@ // create an invisible iframe with a given source // append it to a parent element // execute a callback when it has loaded - var create = Frame.create = function (parent, src, onload, timeout) { + Frame.create = function (parent, src, onload, timeout) { var iframe = document.createElement('iframe'); timeout = timeout || 10000; @@ -34,7 +34,7 @@ /* given an iframe with an rpc script loaded, create a frame object with an asynchronous 'send' method */ - var open = Frame.open = function (e, A, timeout) { + Frame.open = function (e, A, timeout) { var win = e.contentWindow; var frame = {}; @@ -44,7 +44,7 @@ timeout = timeout || 5000; - var accepts = frame.accepts = function (o) { + frame.accepts = function (o) { return A.some(function (e) { switch (typeof(e)) { case 'string': return e === o; @@ -55,7 +55,7 @@ var changeHandlers = frame.changeHandlers = []; - var change = frame.change = function (f) { + frame.change = function (f) { if (typeof(f) !== 'function') { throw new Error('[Frame.change] expected callback'); } @@ -94,7 +94,7 @@ }; window.addEventListener('message', _listener); - var close = frame.close = function () { + frame.close = function () { window.removeEventListener('message', _listener); }; @@ -130,31 +130,31 @@ win.postMessage(JSON.stringify(req), '*'); }; - var set = frame.set = function (key, val, cb) { + frame.set = function (key, val, cb) { send('set', key, val, cb); }; - var batchset = frame.setBatch = function (map, cb) { + frame.setBatch = function (map, cb) { send('batchset', void 0, map, cb); }; - var get = frame.get = function (key, cb) { + frame.get = function (key, cb) { send('get', key, void 0, cb); }; - var batchget = frame.getBatch = function (keys, cb) { + frame.getBatch = function (keys, cb) { send('batchget', void 0, keys, cb); }; - var remove = frame.remove = function (key, cb) { + frame.remove = function (key, cb) { send('remove', key, void 0, cb); }; - var batchremove = frame.removeBatch = function (keys, cb) { + frame.removeBatch = function (keys, cb) { send('batchremove', void 0, keys, cb); }; - var keys = frame.keys = function (cb) { + frame.keys = function (cb) { send('keys', void 0, void 0, cb); }; @@ -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 { diff --git a/customize.dist/share/test.js b/customize.dist/share/test.js index c54d7ac79..a236dcfab 100644 --- a/customize.dist/share/test.js +++ b/customize.dist/share/test.js @@ -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); }); }); - diff --git a/customize.dist/src/build.js b/customize.dist/src/build.js index c5c5c8d77..fbcc34942 100644 --- a/customize.dist/src/build.js +++ b/customize.dist/src/build.js @@ -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, diff --git a/customize.dist/src/fragments/footer.html b/customize.dist/src/fragments/footer.html index 0882cecbd..55b332a0f 100644 --- a/customize.dist/src/fragments/footer.html +++ b/customize.dist/src/fragments/footer.html @@ -31,7 +31,7 @@
- + diff --git a/customize.dist/src/less/cryptpad.less b/customize.dist/src/less/cryptpad.less index db8af27ce..0ee8785e6 100644 --- a/customize.dist/src/less/cryptpad.less +++ b/customize.dist/src/less/cryptpad.less @@ -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; diff --git a/customize.dist/src/less/dropdown.less b/customize.dist/src/less/dropdown.less index 79a7edbec..2d93bba8b 100644 --- a/customize.dist/src/less/dropdown.less +++ b/customize.dist/src/less/dropdown.less @@ -18,6 +18,7 @@ button { .fa-caret-down{ + margin-right: 0px; margin-left: 5px; } } diff --git a/customize.dist/src/less/loading.less b/customize.dist/src/less/loading.less index b48045622..6dcf2491a 100644 --- a/customize.dist/src/less/loading.less +++ b/customize.dist/src/less/loading.less @@ -36,6 +36,9 @@ left: 0; right: 0; text-align: center; + @media screen and (max-height: @media-medium-screen) { + display: none; + } span { background-color: @bg-loading; color: @color-loading; diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less index 9b93a0da6..df68bc8c4 100644 --- a/customize.dist/src/less/toolbar.less +++ b/customize.dist/src/less/toolbar.less @@ -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; diff --git a/customize.dist/src/less/variables.less b/customize.dist/src/less/variables.less index 8a0b4222e..43bcb393b 100644 --- a/customize.dist/src/less/variables.less +++ b/customize.dist/src/less/variables.less @@ -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; diff --git a/customize.dist/src/template.html b/customize.dist/src/template.html index 069e23511..4615cce7f 100644 --- a/customize.dist/src/template.html +++ b/customize.dist/src/template.html @@ -8,7 +8,6 @@ - {{script}} diff --git a/customize.dist/terms.html b/customize.dist/terms.html index 28f0b62ac..2504b1147 100644 --- a/customize.dist/terms.html +++ b/customize.dist/terms.html @@ -8,7 +8,6 @@ - @@ -108,7 +107,7 @@
- + diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index bbcffbf2f..ba7d7ea35 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -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; diff --git a/customize.dist/translations/messages.es.js b/customize.dist/translations/messages.es.js index 9e6c8ceb7..4dc77e16d 100644 --- a/customize.dist/translations/messages.es.js +++ b/customize.dist/translations/messages.es.js @@ -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 = "

Esto es CryptPad, el editor collaborativo en tiempo real zero knowledge.
Lo que escribes aquí es cifrado, con lo cual solo las personas con el enlace pueden accederlo.
Incluso el servido no puede ver lo que escribes.

Lo que ves aquí, lo que escuchas aquí, cuando sales, se queda aquí

 

"; + + out.initialState = [ + '

', + 'Esto es CryptPad, el editor collaborativo en tiempo real Zero Knowledge. Todo está guardado cuando escribes.', + '
', + 'Comparte el enlace a este pad para editar con amigos o utiliza el botón  Compartir  para obtener un enlace solo lectura que permite leer pero no escribir.', + '

', + + '

', + 'Vamos, solo empezia a escribir...', + '

', + '

 

' + ].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.
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.
Este pad no estará presente en tu CryptDrive."; + out.pinLimitDrive = "Has llegado al limite de espacio.
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"; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 6d8b3899a..576e0d403 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -11,6 +11,8 @@ define(function () { out.type.slide = 'Présentation'; out.type.drive = 'Drive'; out.type.whiteboard = "Tableau Blanc"; + out.type.file = "Fichier"; + out.type.media = "Média"; out.button_newpad = 'Nouveau document texte'; out.button_newcode = 'Nouvelle page de code'; @@ -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 = 'CryptPad a été mis à jour !
' + + 'Découvrez les nouveautés de la dernière version :
'+ + 'Notes de version pour CryptPad {0}'; + + 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.
" + + 'Vous pouvez soit supprimer des pads de votre CryptDrive, soit vous abonner à une offre premium 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 accounts.cryptpad.fr. Votre contribution nous aidera à améliorer CryptPad et à répandre le Zero Knowledge. Vous pouvez contacter le support pour tout problème ou question concernant ces changements.'; + out.pinLimitNotPinned = "Vous avez atteint votre limite de stockage.
"+ + "Ce pad n'est pas enregistré dans votre CryptDrive."; + out.pinLimitDrive = out.pinLimitReached+ ".
" + + "Vous ne pouvez pas créer de nouveaux pads."; + out.importButtonTitle = 'Importer un pad depuis un fichier local'; out.exportButtonTitle = 'Exporter ce pad vers un fichier local'; @@ -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 (découvrez pourquoi). ' + + 'Inscrivez-vous ou connectez-vous pour les maintenir en vie.'; out.fm_alert_backupUrl = "Lien de secours pour ce disque.
" + "Il est fortement recommandé de garder ce lien pour vous-même.
" + "Elle vous servira en cas de perte des données de votre navigateur afin de retrouver vos fichiers.
" + "Quiconque se trouve en possession de celle-ci peut modifier ou supprimer tous les fichiers de ce gestionnaire.
"; + 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 en lire plus concernant ' + + 'nos raisons pour ces changements et pourquoi vous devriez vraiment vous enregistrer et vous connecter.'; 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é. En savoir plus..."; + // index.html //about.html @@ -378,12 +457,12 @@ define(function () { // Initial states out.initialState = [ - '

', + '

', 'Voici CryptPad, l\'éditeur collaboratif en temps-réel Zero Knowledge. Tout est sauvegardé dés que vous le tapez.', '
', 'Partagez le lien vers ce pad avec des amis ou utilisez le bouton  Partager  pour obtenir le lien de lecture-seule, qui permet la lecture mais non la modification.', '

', - '

', + '

', '', 'Lancez-vous, commencez à taper...', '

', @@ -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 votre page de préférences, où vous trouverez une case à cocher pour désactiver le retour d'expérience."; return out; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 7055a2b86..e63f0d426 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -11,6 +11,8 @@ define(function () { out.type.slide = 'Presentation'; out.type.drive = 'Drive'; out.type.whiteboard = 'Whiteboard'; + out.type.file = 'File'; + out.type.media = 'Media'; out.button_newpad = 'New Rich Text pad'; out.button_newcode = 'New Code pad'; @@ -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 = 'CryptPad has been updated!
' + + 'Check out what\'s new in the latest version:
'+ + 'Release notes for CryptPad {0}'; + + 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.
" + + 'You can either remove pads from your CryptDrive or subscribe to a premium offer 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 accounts.cryptpad.fr. Your contribution will help us improve CryptPad and spread Zero Knowledge. Please contact support if you have any other questions.'; + out.pinLimitNotPinned = "You've reached your storage limit.
"+ + "This pad is not stored in your CryptDrive."; + out.pinLimitDrive = "You've reached your storage limit.
" + + "You can't create new pads."; + out.importButtonTitle = 'Import a pad from a local file'; out.exportButtonTitle = 'Export this pad to a local file'; @@ -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 (find out why). ' + + 'Sign up or Log in to keep them alive.'; out.fm_alert_backupUrl = "Backup link for this drive.
" + "It is highly recommended that you keep ip for yourself only.
" + "You can use it to retrieve all your files in case your browser memory got erased.
" + "Anybody with that link can edit or remove all the files in your file manager.
"; + 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 read more about ' + + 'why we are doing this and why you really should Sign up and Log in.'; 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. Learn more..."; + // index.html @@ -391,7 +470,7 @@ define(function () { // Initial states out.initialState = [ - '

', + '

', 'This is CryptPad, the Zero Knowledge realtime collaborative editor. Everything is saved as you type.', '
', 'Share the link to this pad to edit with friends or use the  Share  button to share a read-only link 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 = [ diff --git a/customize.dist/translations/messages.ro.js b/customize.dist/translations/messages.ro.js new file mode 100644 index 000000000..2c09b9a76 --- /dev/null +++ b/customize.dist/translations/messages.ro.js @@ -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 = "Conexiunea la server este pierdută
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, apasă aici să te autentifici
sau apasă Escapesă 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.
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.
Acest pad nu va fi stocat n CryptDrive-ul tău."; + out.pinLimitDrive = "Ai atins limita de stocare.
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.
Acesează-mi Drive-ul"; + 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, realtime 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 (află de ce). Înscrie-te sau Loghează-te pentru a le salva."; + out.fm_alert_backupUrl = "Link copie de rezervă pentru acest drive.
Este foarte recomandat să o păstrezi pentru tine.
Poți să o folosești pentru a recupera toate fișierele în cazul în care memoria browserului tău este șterge..
Oricine are linkul poate să editeze sau să îndepărteze toate fișierele din managerul tău de documente.
"; + 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ă citești mai multe despre motivarea noastră și despre ce de trebuie să te Înregistrezi si sa te Loghezi."; + 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 termenii serviciului"; + 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 = "

Hai să stabilim câteva lucruri, mai întâi

"; + 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.
Ești sigur că vrei să continui?
Tastează “Iubesc CryptPad” 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 CKEditor Visual Editor, CodeMirror, și ChainPad un motor în timp real."; + out.main_howitworks_p1 = "CryptPad folosește o variantă a algoritmului de Operational transformation care este capabil să găsescă consens distribuit folosind Nakamoto Blockchain, o construcție popularizată de Bitcoin. 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 dai un tweet, semnalezi o problemă on github, spui salut pe IRC (irc.freenode.net), sau trimiți un email."; + out.main_info = "

Colaborează n siguranță


Dezvoltă-ți ideile împreună cu documente partajate în timp ce tehnologia Zero Knowledge îț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ă nu ne uităm la pad-urile tale, cu tehnologia revoluționară Zero Knowledge a CryptPad nu putem. Învață mai multe despre cum îți protejăm Intimitate și Securitate."; + 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. CkEditor 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.CodeMirror 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 WhatIsMyBrowser.com."; + out.policy_whatweknow_p2 = "Folosim Kibana, 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 Tor browser bundle, sau VPN."; + out.policy_choices_ads = "Dacă vrei doar să blochezi platforma noastră de analiză, poți folosi soluții de adblocking ca Privacy Badger."; + 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 = "Realizat cu \"love\" n \"Franța\""; + out.bottom_support = "Un proiect al \"XWiki Labs Project cu susținerea \"OpenPaaS-ng\""; + out.header_france = "With \"love\" from \"Franța\"/ by \"XWiki"; + out.header_support = " \"OpenPaaS-ng\""; + out.header_logoTitle = "Mergi la pagina principală"; + out.initialState = "

Acesta este CryptPad, editorul colaborativ bazat pe tehnologia Zero Knowledge în timp real. Totul este salvat pe măsură ce scrii.
Partajează link-ul către acest pad pentru a edita cu prieteni sau folosește  Share  butonul pentru a partaja read-only link permițând vizualizarea dar nu și editarea.

Îndrăznește, începe să scrii...

 

"; + 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ă setările de pe pagina ta de user, unde vei găsi o căsuță pentru a activa sau dezactiva feedback-ul de la user"; + + return out; +}); diff --git a/example.nginx.conf b/example.nginx.conf index 5067ca9c0..fcb8b7435 100644 --- a/example.nginx.conf +++ b/example.nginx.conf @@ -32,7 +32,7 @@ server { set $scriptSrc "'self'"; set $connectSrc "'self' wss://cryptpad.fr wss://api.cryptpad.fr"; set $fontSrc "'self'"; - set $imgSrc "data: *"; + set $imgSrc "data: * blob:"; set $frameSrc "'self' beta.cryptpad.fr"; if ($uri = /pad/inner.html) { @@ -65,8 +65,12 @@ server { rewrite ^.*$ /customize/api/config break; } + location ^~ /blob/ { + try_files $uri =404; + } + ## TODO fix in the code so that we don't need this - location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard)$ { + location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media)$ { rewrite ^(.*)$ $1/ redirect; } diff --git a/package.json b/package.json index 48aed5d0d..e75ec7b32 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/pinneddata.js b/pinneddata.js new file mode 100644 index 000000000..0bf9be75f --- /dev/null +++ b/pinneddata.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'); }); +}); diff --git a/readme.md b/readme.md index b3c8a1a61..4ed335c9e 100644 --- a/readme.md +++ b/readme.md @@ -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/ - diff --git a/rpc.js b/rpc.js index 15e06c8a4..0257c498f 100644 --- a/rpc.js +++ b/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>*/, + respond /*:(?string, ?Array)=>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); + }); + }); }); }; - diff --git a/server.js b/server.js index 1f3dfdaea..037a8adeb 100644 --- a/server.js +++ b/server.js @@ -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')); diff --git a/storage/README.md b/storage/README.md index 03117d95c..8fabdf53b 100644 --- a/storage/README.md +++ b/storage/README.md @@ -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. diff --git a/storage/file.js b/storage/file.js index ab2bce617..857f147f4 100644 --- a/storage/file.js +++ b/storage/file.js @@ -28,7 +28,8 @@ var readMessages = function (path, msgHandler, cb) { }; var checkPath = function (path, callback) { - Fs.stat(path, function (err, stats) { + // TODO check if we actually need to use stat at all + Fs.stat(path, function (err) { if (!err) { callback(undefined, true); return; @@ -166,7 +167,7 @@ var getChannel = function (env, id, callback) { }); } }); - }).nThen(function (waitFor) { + }).nThen(function () { if (errorState) { return; } complete(); }); diff --git a/www/assert/index.html b/www/assert/index.html index ea6628bd8..f4867629f 100644 --- a/www/assert/index.html +++ b/www/assert/index.html @@ -3,8 +3,7 @@ - - + - - - my media thing -
-valid elements - - diff --git a/www/assert/media/main.js b/www/assert/media/main.js deleted file mode 100644 index 31a496893..000000000 --- a/www/assert/media/main.js +++ /dev/null @@ -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!"); - }); -}); diff --git a/www/assert/pretty/index.html b/www/assert/pretty/index.html deleted file mode 100644 index 01f82bdba..000000000 --- a/www/assert/pretty/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/www/assert/pretty/main.js b/www/assert/pretty/main.js deleted file mode 100644 index 1b8272884..000000000 --- a/www/assert/pretty/main.js +++ /dev/null @@ -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('
').append($('
', {
-            'class': 'wrap',
-        }).text(shjson))
-        // append the pretty-printed html source for reference
-        .append('
').append($('
').text(pretty));
-
-
-    // TODO write some tests to confirm whether the pretty printer is correct
-});
diff --git a/www/assert/translations/index.html b/www/assert/translations/index.html
index 4b9e7b715..cd8c31e3a 100644
--- a/www/assert/translations/index.html
+++ b/www/assert/translations/index.html
@@ -3,7 +3,7 @@
 
     
     
-    
+    
 
 
 
diff --git a/www/assert/translations/main.js b/www/assert/translations/main.js
index e8825d422..9a5397c73 100644
--- a/www/assert/translations/main.js
+++ b/www/assert/translations/main.js
@@ -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');
 
diff --git a/www/auth/index.html b/www/auth/index.html
new file mode 100644
index 000000000..685ca37c4
--- /dev/null
+++ b/www/auth/index.html
@@ -0,0 +1,9 @@
+
+
+
+    
+    
+
+
+
+
diff --git a/www/auth/main.js b/www/auth/main.js
new file mode 100644
index 000000000..747434c23
--- /dev/null
+++ b/www/auth/main.js
@@ -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);
+        });
+    });
+});
diff --git a/www/code/inner.html b/www/code/inner.html
index cec96bbac..f27544238 100644
--- a/www/code/inner.html
+++ b/www/code/inner.html
@@ -32,32 +32,75 @@
     
     
     
 
 
   
- +
+ +
+
diff --git a/www/code/main.js b/www/code/main.js index 227c1e1fa..d82d73273 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -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($('', { - 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") { diff --git a/www/common/boot2.js b/www/common/boot2.js index 8a38fad2f..d894191e8 100644 --- a/www/common/boot2.js +++ b/www/common/boot2.js @@ -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')]); }); - diff --git a/www/common/clipboard.js b/www/common/clipboard.js index 5e62f5a37..191895dfd 100644 --- a/www/common/clipboard.js +++ b/www/common/clipboard.js @@ -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 = $('', { type: 'text', }).val(text); $('body').append($ta); - if (!($ta.length && $ta[0].select)) { + if (!($ta.length && $ta[0].select)) { // console.log("oops"); return; } diff --git a/www/common/common-codemirror.js b/www/common/common-codemirror.js new file mode 100644 index 000000000..429e9bd9a --- /dev/null +++ b/www/common/common-codemirror.js @@ -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($('', { + 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; +}); + diff --git a/www/common/common-hash.js b/www/common/common-hash.js new file mode 100644 index 000000000..5de76ab99 --- /dev/null +++ b/www/common/common-hash.js @@ -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; +}); diff --git a/www/common/common-history.js b/www/common/common-history.js new file mode 100644 index 000000000..48b210eb3 --- /dev/null +++ b/www/common/common-history.js @@ -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 =$('