diff --git a/.gitignore b/.gitignore index 139fab33c..fc1136152 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ data npm-debug.log pins/ blob/ +blobstage/ privileged.conf diff --git a/.travis.yml b/.travis.yml index 4160b8719..09967f1f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,12 @@ language: node_js env: matrix: - - "BROWSER='firefox:19:Windows 2012'" - - "BROWSER='chrome::Windows 2008'" + - "BROWSER='firefox::Windows 10'" + - "BROWSER='chrome::Windows 10'" + #- "BROWSER='MicrosoftEdge:14.14393:Windows 10'" + #- "BROWSER='internet explorer:11.103:Windows 10'" + #- "BROWSER='safari:10.0:macOS 10.12'" + #- "BROWSER='safari:9.0:OS X 10.11'" branches: only: - master diff --git a/TestSelenium.js b/TestSelenium.js index 9623577b5..fccd4e067 100644 --- a/TestSelenium.js +++ b/TestSelenium.js @@ -1,11 +1,13 @@ /* global process */ var WebDriver = require("selenium-webdriver"); +var nThen = require('nthen'); if (process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== 'false') { // We can't do saucelabs on pull requests so don't fail. return; } +// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/ var driver; if (process.env.SAUCE_USERNAME !== undefined) { var browserArray = process.env.BROWSER.split(':'); @@ -21,18 +23,59 @@ if (process.env.SAUCE_USERNAME !== undefined) { driver = new WebDriver.Builder().withCapabilities({ browserName: "chrome" }).build(); } -driver.get('http://localhost:3000/assert/'); -var report = driver.wait(WebDriver.until.elementLocated(WebDriver.By.className("report")), 5000); -report.getAttribute("class").then(function (cls) { - report.getText().then(function (text) { - console.log("\n-----\n" + text + "\n-----"); - driver.quit(); - if (!cls) { - throw new Error("cls is null"); - } else if (cls.indexOf("failure") !== -1) { - throw new Error("cls contains the word failure"); - } else if (cls.indexOf("success") === -1) { - throw new Error("cls does not contain the word success"); - } - }); +var SC_GET_DATA = "return (window.__CRYPTPAD_TEST__) ? window.__CRYPTPAD_TEST__.getData() : '[]'"; + +var failed = false; +var nt = nThen; +[ + //'/register/#?test=test', + '/assert/#?test=test', + // '/auth/#?test=test' // TODO(cjd): Not working on automatic tests, understand why. +].forEach(function (path) { + if (failed) { return; } + var url = 'http://localhost:3000' + path; + nt = nt(function (waitFor) { + var done = waitFor(); + console.log('\n\n-----TEST ' + url + ' -----'); + var waitTo = setTimeout(function () { + console.log("no report in 20 seconds, timing out"); + failed = true; + done(); + done = undefined; + }, 20000); + var logMore = function () { + if (!done) { return; } + driver.executeScript(SC_GET_DATA).then(waitFor(function (dataS) { + if (!done) { return; } + var data = JSON.parse(dataS); + data.forEach(function (d) { + if (d.type !== 'log') { return; } + console.log('>' + d.val); + }); + data.forEach(function (d) { + if (d.type !== 'report') { return; } + console.log('RESULT: ' + d.val); + if (d.val !== 'passed') { + if (d.error) { + console.log(d.error.message); + console.log(d.error.stack); + } + failed = true; + } + clearTimeout(waitTo); + console.log('-----END TEST ' + url + ' -----'); + done(); + done = undefined; + }); + if (done) { setTimeout(logMore, 50); } + })); + }; + driver.get(url).then(waitFor(logMore)); + }).nThen; +}); + +nt(function (waitFor) { + driver.quit().then(waitFor(function () { + if (failed) { process.exit(100); } + })); }); diff --git a/bower.json b/bower.json index 5734be6ec..0c80203da 100644 --- a/bower.json +++ b/bower.json @@ -37,6 +37,7 @@ "diff-dom": "^2.1.1", "alertifyjs": "^1.0.11", "scrypt-async": "^1.2.0", - "bootstrap": "#v4.0.0-alpha.6" + "bootstrap": "#v4.0.0-alpha.6", + "pdfjs-dist": "^1.8.398" } } diff --git a/config.example.js b/config.example.js index 76bba6eae..d5b0c5dbf 100644 --- a/config.example.js +++ b/config.example.js @@ -10,7 +10,7 @@ module.exports = { // the port on which your httpd will listen - /* Cryptpad can be configured to send customized HTTP Headers + /* CryptPad can be configured to send customized HTTP Headers * These settings may vary widely depending on your needs * Examples are provided below */ @@ -31,15 +31,17 @@ module.exports = { * connect-src is used to restrict what domains can connect to the websocket. * * it is recommended that you configure these fields to match the - * domain which will serve your cryptpad instance. + * domain which will serve your CryptPad instance. */ "child-src 'self' *", + "media-src *", + /* this allows connections over secure or insecure websockets 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: blob:", // data: is used by codemirror "img-src 'self' data: blob:", @@ -82,24 +84,24 @@ module.exports = { */ //websocketPort: 3000, - /* if you want to run a different version of cryptpad but using the same websocket + /* if you want to run a different version of CryptPad but using the same websocket * server, you should use the other server port as websocketPort and disable * the websockets on that server */ //useExternalWebsocket: false, - /* If Cryptpad is proxied without using https, the server needs to know. + /* If CryptPad is proxied without using https, the server needs to know. * Specify 'useSecureWebsockets: true' so that it can send * Content Security Policy Headers that prevent http and https from mixing */ useSecureWebsockets: false, - /* Cryptpad can log activity to stdout + /* CryptPad can log activity to stdout * This may be useful for debugging */ logToStdout: false, - /* Cryptpad supports verbose logging + /* CryptPad supports verbose logging * (false by default) */ verbose: false, @@ -116,6 +118,58 @@ module.exports = { 'contact', ], + /* Limits, Donations, Subscriptions and Contact + * + * By default, CryptPad limits every registered user to 50MB of storage. It also shows a + * donate button which allows for making a donation to support CryptPad development. + * + * You can either: + * A: Leave it exactly as it is. + * B: Hide the donate button. + * C: Change the donate button to a subscribe button, people who subscribe will get more + * storage on your instance and you get 50% of the revenue earned. + * + * CryptPad is developed by people who need to live and who deserve an equivilent life to + * what they would get at a company which monitizes user data. However, we intend to have + * a mutually positive relationship with every one of our users, including you. If you are + * getting value from CryptPad, you should be giving equal value back. + * + * If you are using CryptPad in a business context, please consider taking a support contract + * by contacting sales@cryptpad.fr + * + * If you choose A then there's nothing to do. + * + * If you choose B, set this variable to true and it will remove the donate button. + */ + removeDonateButton: false, + /* + * If you choose C, set allowSubscriptions to true, then set myDomain to the domain which people + * use to reach your CryptPad instance. Then contact sales@cryptpad.fr and tell us your domain. + * We will tell you what is needed to get paid. + */ + allowSubscriptions: false, + myDomain: 'i.did.not.read.my.config.myserver.tld', + + /* + * If you are using CryptPad internally and you want to increase the per-user storage limit, + * change the following value. + * + * Please note: This limit is what makes people subscribe and what pays for CryptPad + * development. Running a public instance that provides a "better deal" than cryptpad.fr + * is effectively using the project against itself. + */ + defaultStorageLimit: 50 * 1024 * 1024, + + /* + * By default, CryptPad also contacts our accounts server once a day to check for changes in + * the people who have accounts. This check-in will also send the version of your CryptPad + * instance and your email so we can reach you if we are aware of a serious problem. We will + * never sell it or send you marketing mail. If you want to block this check-in and remain + * completely invisible, set this and allowSubscriptions both to false. + */ + adminEmail: 'i.did.not.read.my.config@cryptpad.fr', + + /* You have the option of specifying an alternative storage adaptor. These status of these alternatives are specified in their READMEs, @@ -135,7 +189,7 @@ module.exports = { storage: './storage/file', /* - Cryptpad stores each document in an individual file on your hard drive. + CryptPad stores each document in an individual file on your hard drive. Specify a directory where files should be stored. It will be created automatically if it does not already exist. */ @@ -158,17 +212,17 @@ module.exports = { */ blobStagingPath: './blobstage', - /* Cryptpad's file storage adaptor closes unused files after a configurale + /* CryptPad's file storage adaptor closes unused files after a configurale * number of milliseconds (default 30000 (30 seconds)) */ channelExpirationMs: 30000, - /* Cryptpad's file storage adaptor is limited by the number of open files. + /* CryptPad's file storage adaptor is limited by the number of open files. * When the adaptor reaches openFileLimit, it will clean up older files */ openFileLimit: 2048, - /* Cryptpad's socket server can be extended to respond to RPC calls + /* CryptPad's socket server can be extended to respond to RPC calls * you can configure it to respond to custom RPC calls if you like. * provide the path to your RPC module here, or `false` if you would * like to disable the RPC interface completely @@ -191,7 +245,7 @@ module.exports = { /* Setting this value to anything other than true will cause file upload * attempts to be rejected outright. */ - enableUploads: true, + 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 @@ -203,9 +257,24 @@ module.exports = { * This is a temporary measure until a better quota system is in place. * registered users' public keys can be found on the settings page. */ - restrictUploads: true, + //restrictUploads: false, - /* it is recommended that you serve cryptpad over https + /* 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 */ //privKeyAndCertFiles: [ diff --git a/customize.dist/BottomBar.html b/customize.dist/BottomBar.html deleted file mode 100644 index c8d43dfce..000000000 --- a/customize.dist/BottomBar.html +++ /dev/null @@ -1,16 +0,0 @@ - -
-
-
- - - -

-

-
-
-

-

-
-
-
diff --git a/customize.dist/about.html b/customize.dist/about.html index b0b719033..dadd21e3b 100644 --- a/customize.dist/about.html +++ b/customize.dist/about.html @@ -39,6 +39,9 @@ Blog + + + @@ -106,7 +109,7 @@
- + diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index 949c612ee..af2474775 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -4,7 +4,8 @@ define(function() { /* Select the buttons displayed on the main page to create new collaborative sessions * Existing types : pad, code, poll, slide */ - config.availablePadTypes = ['drive', 'pad', 'code', 'slide', 'poll', 'whiteboard']; + config.availablePadTypes = ['drive', 'pad', 'code', 'slide', 'poll', 'whiteboard', 'file']; + config.registeredOnlyTypes = ['file']; /* Cryptpad apps use a common API to display notifications to users * by default, notifications are hidden after 5 seconds @@ -37,9 +38,6 @@ define(function() { config.enableHistory = true; - //config.enablePinLimit = true; - //config.pinLimit = 1000; - /* user passwords are hashed with scrypt, and salted with their username. this value will be appended to the username, causing the resulting hash to differ from other CryptPad instances if customized. This makes it diff --git a/customize.dist/bg.jpg b/customize.dist/bg.jpg deleted file mode 100644 index fa5214591..000000000 Binary files a/customize.dist/bg.jpg and /dev/null differ diff --git a/customize.dist/bg2.jpg b/customize.dist/bg2.jpg deleted file mode 100644 index a7598eee1..000000000 Binary files a/customize.dist/bg2.jpg and /dev/null differ diff --git a/customize.dist/contact.html b/customize.dist/contact.html index aa81fac60..768cf2f7c 100644 --- a/customize.dist/contact.html +++ b/customize.dist/contact.html @@ -39,6 +39,9 @@ Blog + + + @@ -103,7 +106,7 @@
- + diff --git a/customize.dist/fr.png b/customize.dist/fr.png deleted file mode 100644 index 8332c4ec2..000000000 Binary files a/customize.dist/fr.png and /dev/null differ diff --git a/customize.dist/header.js b/customize.dist/header.js new file mode 100644 index 000000000..7a2ac9295 --- /dev/null +++ b/customize.dist/header.js @@ -0,0 +1,53 @@ +define([ + 'jquery', + '/customize/application_config.js', + '/common/cryptpad-common.js', + '/api/config', +], function ($, Config, Cryptpad, ApiConfig) { + + window.APP = { + Cryptpad: Cryptpad, + }; + + var Messages = Cryptpad.Messages; + + $(function () { + // Language selector + var $sel = $('#language-selector'); + Cryptpad.createLanguageSelector(undefined, $sel); + $sel.find('button').addClass('btn').addClass('btn-secondary'); + $sel.show(); + + var $upgrade = $('#upgrade'); + + var showUpgrade = function (text, feedback, url) { + if (ApiConfig.removeDonateButton) { return; } + if (localStorage.plan) { return; } + if (!text) { return; } + $upgrade.text(text).show(); + $upgrade.click(function () { + Cryptpad.feedback(feedback); + window.open(url,'_blank'); + }); + }; + + // User admin menu + var $userMenu = $('#user-menu'); + var userMenuCfg = { + $initBlock: $userMenu + }; + var $userAdmin = Cryptpad.createUserAdminMenu(userMenuCfg); + $userAdmin.find('button').addClass('btn').addClass('btn-secondary'); + + $(window).click(function () { + $('.cryptpad-dropdown').hide(); + }); + + if (Cryptpad.isLoggedIn() && ApiConfig.allowSubscriptions) { + showUpgrade(Messages.upgradeAccount, "HOME_UPGRADE_ACCOUNT", Cryptpad.upgradeURL); + } else { + showUpgrade(Messages.supportCryptpad, "HOME_SUPPORT_CRYPTPAD", Cryptpad.donateURL); + } + }); +}); + diff --git a/customize.dist/heart.png b/customize.dist/heart.png deleted file mode 100644 index d9ee53e59..000000000 Binary files a/customize.dist/heart.png and /dev/null differ diff --git a/customize.dist/index.html b/customize.dist/index.html index d246d84d0..db9cef793 100644 --- a/customize.dist/index.html +++ b/customize.dist/index.html @@ -39,6 +39,9 @@ Blog + + + @@ -225,7 +228,7 @@
- + diff --git a/customize.dist/logo-xwiki.png b/customize.dist/logo-xwiki.png deleted file mode 100644 index 375739d2b..000000000 Binary files a/customize.dist/logo-xwiki.png and /dev/null differ diff --git a/customize.dist/logo-xwiki2.png b/customize.dist/logo-xwiki2.png deleted file mode 100644 index 047c89ea7..000000000 Binary files a/customize.dist/logo-xwiki2.png and /dev/null differ diff --git a/customize.dist/main.css b/customize.dist/main.css index 026649207..49f06eedc 100644 --- a/customize.dist/main.css +++ b/customize.dist/main.css @@ -387,6 +387,13 @@ left: 0; right: 0; text-align: center; + transition: opacity 750ms; + transition-delay: 3000ms; +} +@media screen and (max-height: 600px) { + .cp #loadingTip { + display: none; + } } .cp #loadingTip span { background-color: #302B28; @@ -408,6 +415,7 @@ font-family: FontAwesome; } .dropdown-bar button .fa-caret-down { + margin-right: 0px; margin-left: 5px; } .dropdown-bar .dropdown-bar-content { @@ -519,6 +527,22 @@ margin: 0px 10px; line-height: 40px; } +#cryptpadTopBar .right .buttonSuccess { + color: #fff; + background: #5cb85c; + border-color: #5cb85c; +} +#cryptpadTopBar .right .buttonSuccess:hover { + color: #fff; + background: #449d44; + border: 1px solid #419641; +} +#cryptpadTopBar .right .buttonSuccess span { + color: #fff; +} +#cryptpadTopBar .right .buttonSuccess .large { + margin-left: 5px; +} #cryptpadTopBar .right button .buttonTitle .fa-user { margin-right: 5px; } @@ -571,7 +595,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 +621,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, @@ -861,6 +893,12 @@ html.cp, .cp #main_other #main-container { display: inline-block; } +.cp #main #userForm .extra p, +.cp #main_other #userForm .extra p { + font-size: 28px; + padding: 15px; + text-align: center; +} .cp #main #data, .cp #main_other #data { width: 600px; @@ -1084,6 +1122,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 cdaeedfdb..f2580bbe1 100644 --- a/customize.dist/main.js +++ b/customize.dist/main.js @@ -1,7 +1,8 @@ define([ 'jquery', '/customize/application_config.js', - '/common/cryptpad-common.js' + '/common/cryptpad-common.js', + '/customize/header.js', ], function ($, Config, Cryptpad) { window.APP = { @@ -13,25 +14,10 @@ define([ $(function () { var $main = $('#mainBlock'); - // Language selector - var $sel = $('#language-selector'); - Cryptpad.createLanguageSelector(undefined, $sel); - $sel.find('button').addClass('btn').addClass('btn-secondary'); - $sel.show(); - - // User admin menu - var $userMenu = $('#user-menu'); - var userMenuCfg = { - $initBlock: $userMenu - }; - var $userAdmin = Cryptpad.createUserAdminMenu(userMenuCfg); - $userAdmin.find('button').addClass('btn').addClass('btn-secondary'); - $(window).click(function () { $('.cryptpad-dropdown').hide(); }); - // main block is hidden in case javascript is disabled $main.removeClass('hidden'); @@ -58,8 +44,8 @@ define([ }); $loggedInBlock.removeClass('hidden'); - //return; - } else { + } + else { $main.find('#userForm').removeClass('hidden'); $('#name').focus(); } @@ -70,6 +56,8 @@ define([ var $container = $('
', {'class': 'dropdown-bar'}).appendTo($parent); Config.availablePadTypes.forEach(function (el) { if (el === 'drive') { return; } + if (!Cryptpad.isLoggedIn() && Config.registeredOnlyTypes && + Config.registeredOnlyTypes.indexOf(el) !== -1) { return; } options.push({ tag: 'a', attributes: { @@ -90,7 +78,6 @@ define([ $block.appendTo($parent); }; - /* Log in UI */ var Login; // deferred execution to avoid unnecessary asset loading @@ -119,54 +106,57 @@ define([ }); $('button.login').click(function () { - Cryptpad.addLoadingScreen(Messages.login_hashing); - // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password + // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up window.setTimeout(function () { - loginReady(function () { - var uname = $uname.val(); - var passwd = $passwd.val(); - Login.loginOrRegister(uname, passwd, false, function (err, result) { - if (!err) { - var proxy = result.proxy; + Cryptpad.addLoadingScreen(Messages.login_hashing); + // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password + window.setTimeout(function () { + loginReady(function () { + var uname = $uname.val(); + var passwd = $passwd.val(); + Login.loginOrRegister(uname, passwd, false, function (err, result) { + if (!err) { + var proxy = result.proxy; - // successful validation and user already exists - // set user hash in localStorage and redirect to drive - if (proxy && !proxy.login_name) { - proxy.login_name = result.userName; + // successful validation and user already exists + // set user hash in localStorage and redirect to drive + if (proxy && !proxy.login_name) { + proxy.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; } - - 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); - }); - break; - case 'INVAL_PASS': - Cryptpad.removeLoadingScreen(function () { - Cryptpad.alert(Messages.login_invalPass); - }); - break; - default: // UNHANDLED ERROR - Cryptpad.errorLoadingScreen(Messages.login_unhandledError); - } + 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 */ diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 813978b52..67759a15e 100644 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -7,7 +7,9 @@ var map = { 'es': 'Español', 'pl': 'Polski', 'de': 'Deutsch', - 'pt-br': 'Português do Brasil' + 'pt-br': 'Português do Brasil', + 'ro': 'Română', + 'zh': '繁體中文', }; var getStoredLanguage = function () { return localStorage.getItem(LS_LANG); }; diff --git a/customize.dist/openpaas.png b/customize.dist/openpaas.png deleted file mode 100644 index aa91bfe98..000000000 Binary files a/customize.dist/openpaas.png and /dev/null differ diff --git a/customize.dist/openpaasng.png b/customize.dist/openpaasng.png deleted file mode 100644 index 54168332c..000000000 Binary files a/customize.dist/openpaasng.png and /dev/null differ diff --git a/customize.dist/privacy.html b/customize.dist/privacy.html index 00a266236..8218dbad2 100644 --- a/customize.dist/privacy.html +++ b/customize.dist/privacy.html @@ -39,6 +39,9 @@ Blog + + +
@@ -124,7 +127,7 @@
- + 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 4cf4b101d..a43182135 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/fragments/topbar.html b/customize.dist/src/fragments/topbar.html index a6b459b35..c1ed7e24b 100644 --- a/customize.dist/src/fragments/topbar.html +++ b/customize.dist/src/fragments/topbar.html @@ -24,4 +24,7 @@ Blog + + + diff --git a/customize.dist/src/less/cryptpad.less b/customize.dist/src/less/cryptpad.less index db8af27ce..49608df00 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; @@ -326,6 +337,14 @@ noscript { display: inline-block; } + #userForm .extra { + p { + font-size: 28px; + padding: 15px; + text-align: center; + } + } + #data { p { margin: 0; @@ -536,6 +555,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..cc23c49d9 100644 --- a/customize.dist/src/less/loading.less +++ b/customize.dist/src/less/loading.less @@ -36,6 +36,12 @@ left: 0; right: 0; text-align: center; + + transition: opacity 750ms; + transition-delay: 3000ms; + @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 a199730cd..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,6 +45,10 @@ } button { + font: @toolbar-button-font; + * { + font: @toolbar-button-font; + } &#shareButton, &.buttonSuccess { // Bootstrap 4 colors color: #fff; @@ -83,7 +90,7 @@ // Bootstrap 4 colors (btn-secondary) border: 1px solid transparent; border-radius: .25rem; - color: #292b2c; + color: #000; background-color: #fff; border-color: #ccc; &:hover { @@ -98,43 +105,6 @@ vertical-align: top; margin-left: 10px; } - .cryptpad-drive-limit { - 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; - .usage { - height: 24px; - display: inline-block; - background: blue; - position: absolute; - left: 0; - z-index:1; - &.normal { - background: @toolbar-green; - } - &.warning { - background: orange; - } - &.above { - background: red; - } - } - .usageText { - position: relative; - color: black; - text-shadow: 1px 0 2px white, 0 1px 2px white, -1px 0 2px white, 0 -1px 2px white; - z-index: 2; - font-size: 16px; - font-weight: bold; - } - } .cryptpad-limit { box-sizing: border-box; height: 26px; @@ -165,6 +135,7 @@ margin: 3px; vertical-align: top; box-sizing: content-box; + text-align: center; span { display: inline-block; width: 4px; @@ -232,6 +203,7 @@ padding-right: 5px; padding-left: 5px; margin: 3px 2px; + box-sizing: border-box; } .dropdown-bar-content { @@ -442,6 +414,8 @@ margin-bottom: -1px; .cryptpad-dropdown-users { pre { + /* needed for ckeditor */ + white-space: pre; margin: 5px 0px; } } @@ -492,6 +466,7 @@ margin: 8px; line-height: 16px; font-size: 16px; + text-align: center; } .cryptpad-readonly { margin-right: 5px; diff --git a/customize.dist/src/less/topbar.less b/customize.dist/src/less/topbar.less index b10309f61..a394c4eeb 100644 --- a/customize.dist/src/less/topbar.less +++ b/customize.dist/src/less/topbar.less @@ -47,6 +47,24 @@ margin: 0px 10px; line-height: 40px; + .buttonSuccess { + // Bootstrap 4 colors + color: #fff; + background: @toolbar-green; + border-color: @toolbar-green; + &:hover { + color: #fff; + background: #449d44; + border: 1px solid #419641; + } + span { + color: #fff; + } + .large { + margin-left: 5px; + } + } + button { .buttonTitle { .fa-user { 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/terms.html b/customize.dist/terms.html index 68eb51599..bda3dcbb9 100644 --- a/customize.dist/terms.html +++ b/customize.dist/terms.html @@ -39,6 +39,9 @@ Blog + + + @@ -107,7 +110,7 @@
- + diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index d2385dfc1..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,18 +113,23 @@ z-index: 9001; } .cryptpad-toolbar .fa { + font: normal normal normal 14px/1 FontAwesome; font-family: FontAwesome; } .cryptpad-toolbar a { float: right; } .cryptpad-toolbar button { + font: 12px Ubuntu, Arial, sans-serif; border: 1px solid transparent; border-radius: .25rem; - color: #292b2c; + 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; @@ -177,43 +183,6 @@ vertical-align: top; margin-left: 10px; } -.cryptpad-toolbar .cryptpad-drive-limit { - 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; -} -.cryptpad-toolbar .cryptpad-drive-limit .usage { - height: 24px; - display: inline-block; - background: blue; - position: absolute; - left: 0; - z-index: 1; -} -.cryptpad-toolbar .cryptpad-drive-limit .usage.normal { - background: #5cb85c; -} -.cryptpad-toolbar .cryptpad-drive-limit .usage.warning { - background: orange; -} -.cryptpad-toolbar .cryptpad-drive-limit .usage.above { - background: red; -} -.cryptpad-toolbar .cryptpad-drive-limit .usageText { - position: relative; - color: black; - text-shadow: 1px 0 2px white, 0 1px 2px white, -1px 0 2px white, 0 -1px 2px white; - z-index: 2; - font-size: 16px; - font-weight: bold; -} .cryptpad-toolbar .cryptpad-limit { box-sizing: border-box; height: 26px; @@ -239,6 +208,7 @@ margin: 3px; vertical-align: top; box-sizing: content-box; + text-align: center; } .cryptpad-toolbar .cryptpad-lag span { display: inline-block; @@ -314,6 +284,7 @@ padding-right: 5px; padding-left: 5px; margin: 3px 2px; + box-sizing: border-box; } .cryptpad-toolbar .dropdown-bar-content { margin-top: -3px; @@ -518,6 +489,8 @@ margin-bottom: -1px; } .cryptpad-toolbar-leftside .cryptpad-dropdown-users pre { + /* needed for ckeditor */ + white-space: pre; margin: 5px 0px; } .cryptpad-toolbar-leftside button { @@ -566,6 +539,7 @@ 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 9af57694d..a128c2e39 100644 --- a/customize.dist/translations/messages.es.js +++ b/customize.dist/translations/messages.es.js @@ -13,8 +13,7 @@ define(function () { out.type.slide = 'Presentación'; out.type.whiteboard = 'Pizarra'; - out.updated_0_common_connectionLost = "Connexión perdida
El documento está ahora en modo solo lectura hasta que la conexión vuelva."; - out.common_connectionLost = out.updated_0_common_connectionLost; + out.common_connectionLost = "Connexión perdida
El documento está ahora en modo solo lectura hasta que la conexión vuelva."; out.disconnected = "Desconectado"; out.synchronizing = "Sincronización"; @@ -200,7 +199,6 @@ define(function () { out.fm_info_root = "Crea carpetas aquí para organizar tus documentos."; out.fm_info_unsorted = "Contiene todos los documentos que has visitado que no estan organizados en \"Documentos\" o movidos a la \"Papelera\"."; out.fm_info_template = "Contiene todas las plantillas que puedes volver a usar para crear nuevos documentos."; - out.fm_info_trash = "Archivos eliminados de la papelera también se eliminan de \"Todos los archivos\" y es imposible recuparlos desde el explorador."; out.fm_info_allFiles = "Contiene todos los archivos de \"Documentos\", \"Sin organizar\" y \"Papelera\". No puedes mover o eliminar archivos aquí."; out.fm_alert_backupUrl = "Enlace de copia de seguridad para este drive. Te recomendamos muy fuertemente que lo guardes secreto.
Lo puedes usar para recuparar todos tus archivos en el caso que la memoria de tu navegador se borre.
Cualquiera con este enlace puede editar o eliminar todos los archivos en el explorador.
"; out.fm_backup_title = "Enlace de copia de seguridad"; @@ -404,5 +402,60 @@ define(function () { 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.upgradeAccount = "Mejorar cuenta"; + + 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.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."; + + // 1.7.0 - Hodag + out.comingSoon = "Próximamente..."; // "Coming soon..." + out.newVersion = ["CryptPad ha sido actualizado!", + "Puedes ver lo que ha cambiada aquí (en inglés):", + "Notas de versión para CryptPad {0}"].join("
"); + out.pinLimitReachedAlert = ["Has llegado a tu limite de espacio. Nuevos pads no serán guardados en tu CryptDrive.", + "Puedes eliminar pads de tu CryptDrive o suscribirte a una oferta premium para obtener más espacio."].join("
"); + out.pinLimitReachedAlertNoAccounts = "Has llegado a tu limite de espacio"; + out.previewButtonTitle = "Mostrar/esconder la vista previa Markdown"; + out.fm_info_trash = "Vacía tu papelera para liberar espaci en tu CryptDrive."; + out.fm_info_anonymous = "No estás conectado, así que estos pads pueden ser borrados (¿por qué?). Registrate o Inicia sesión para asegurarlos."; + out.fm_alert_anonymous = "Hola, estás usando CryptPad anónimamente. Está bien, pero tus pads pueden ser borrados después de un périodo de inactividad. Hemos desactivado funciones avanzadas de CryptDrive para usuarios anónimos porque queremos ser claros que no es un lugar seguro para almacenar cosas. Puedes leer este articulo (en inglés) sobre por qué hacemos esto y por qué deberías Registrarte e Iniciar sesión."; + out.fm_error_cantPin = "Error del servidor. Por favor, recarga la página e intentalo de nuevo."; + out.upload_notEnoughSpace = "No tienes suficiente espacio para este archivo en tu CryptDrive"; + out.upload_tooLarge = "Este archivo supera el límite de carga."; + out.upload_choose = "Escoge un archivo"; + out.upload_pending = "Esperando"; + out.upload_cancelled = "Cancelado"; + out.upload_name = "Nombre"; + out.upload_size = "Tamaño"; + out.upload_progress = "Progreso"; + out.download_button = "Descifrar y descargar"; + out.warn_notPinned = "Este pad no está en ningun CryptDrive. Expirará después de 3 meses. Acerca de..."; + + out.poll_remove = "Quitar"; + out.poll_edit = "Editar"; + out.poll_locked = "Cerrado"; + out.poll_unlocked = "Abierto"; + return out; }); diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 9b1f9ad3a..70d804652 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -32,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'; @@ -51,17 +52,35 @@ define(function () { out.language = "Langue"; - out.upgrade = "Améliorer"; + 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.upgradeAccount = "Améliorer le compte"; out.MB = "Mo"; + out.GB = "Go"; + out.KB = "Ko"; + + out.supportCryptpad = "Soutenir CryptPad"; + + 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.pinLimitReachedAlert = "Vous avez atteint votre limite de stockage. Les nouveaux pads ne seront pas enregistrés dans votre CrypDrive.
" + - "Pour résoudre ce problème, vous pouvez soit supprimer des pads de votre CryptDrive (y compris la corbeille), soit vous abonner à une offre premium pour augmenter la limite maximale."; + out.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.pinLimitReachedAlertNoAccounts = out.pinLimitReached; out.pinLimitNotPinned = "Vous avez atteint votre limite de stockage.
"+ "Ce pad n'est pas enregistré dans votre CryptDrive."; out.pinLimitDrive = out.pinLimitReached+ ".
" + @@ -94,6 +113,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'; @@ -107,6 +128,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)"; @@ -175,6 +197,11 @@ define(function () { out.poll_titleHint = "Titre"; out.poll_descriptionHint = "Description"; + out.poll_remove = "Supprimer"; + out.poll_edit = "Modifier"; + out.poll_locked = "Verrouillé"; + out.poll_unlocked = "Déverrouillé"; + // Canvas out.canvas_clear = "Nettoyer"; out.canvas_delete = "Supprimer la sélection"; @@ -222,14 +249,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"; @@ -275,6 +310,8 @@ define(function () { out.login_invalPass = 'Mot de passe requis'; out.login_unhandledError = "Une erreur inattendue s'est produite :("; + out.login_notRegistered = 'Pas encore inscrit ?'; + out.register_importRecent = "Importer l'historique (Recommendé)"; out.register_acceptTerms = "J'accepte les conditions d'utilisation"; out.register_passwordsDontMatch = "Les mots de passe doivent être identiques!"; @@ -334,6 +371,23 @@ define(function () { 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.upload_mustLogin = "Vous devez vous connecter pour uploader un fichier"; + 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 @@ -349,15 +403,15 @@ define(function () { out.main_zeroKnowledge_p = "Vous n'avez pas besoin de croire que nous n'allons pas regarder vos pads. Avec la technologie Zero Knowledge de CryptPad, nous ne pouvons pas le faire. Apprenez-en plus sur notre manière de protéger vos données."; out.main_writeItDown = 'Prenez-en note'; out.main_writeItDown_p = "Les plus grands projets naissent des plus petites idées. Prenez note de vos moments d'inspiration et de vos idées inattendues car vous ne savez pas lesquels seront des découvertes capitales."; - out.main_share = 'Partager le lien, partager le pad'; - out.main_share_p = "Faites croître vos idées à plusieurs : réalisez des réunions efficaes, collaborez sur vos listes de tâches et réalisez des présentations rapide avec tous vos amis sur tous vos appareils."; - out.main_organize = 'Soyez organisés'; - out.main_organize_p = "Avec le CryptPad Drive, vous pouvez garder vos vues sur ce qui est important. Les dossiers vous permettent de garder la trace de vos projets et d'avoir une vision globale du travail effectué."; + out.main_share = 'Partagez le lien, partagez le pad'; + out.main_share_p = "Faites croître vos idées à plusieurs : réalisez des réunions efficaces, collaborez sur vos listes de tâches et réalisez des présentations rapides avec tous vos amis sur tous vos appareils."; + out.main_organize = 'Soyez organisé'; + out.main_organize_p = "Avec CryptDrive, vous pouvez garder vos vues sur ce qui est important. Les dossiers vous permettent de garder la trace de vos projets et d'avoir une vision globale du travail effectué."; out.tryIt = 'Essayez-le !'; out.main_richText = 'Éditeur de texte'; out.main_richText_p = 'Éditez des documents texte collaborativement avec notre application CkEditor temps-réel et Zero Knowledge.'; out.main_code = 'Éditeur de code'; - out.main_code_p = 'Modifier votre code collaborativement grâce à notre application CodeMirror temps-réel et Zero Knowledge.'; + out.main_code_p = 'Modifiez votre code collaborativement grâce à notre application CodeMirror temps-réel et Zero Knowledge.'; out.main_slide = 'Présentations'; out.main_slide_p = 'Créez vos présentations en syntaxe Markdown collaborativement de manière sécurisée et affichez les dans votre navigateur.'; out.main_poll = 'Sondages'; @@ -430,11 +484,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 = [ diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index a1eff8fbf..6d4f07c02 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -34,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'; @@ -53,17 +54,35 @@ 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.upgradeAccount = "Upgrade account"; out.MB = "MB"; + out.GB = "GB"; + out.KB = "KB"; + + out.supportCryptpad = "Support CryptPad"; + + 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.pinLimitReachedAlert = "You've reached your storage limit. New pads won't be stored in your CryptDrive.
" + - "To fix this problem, you can either remove pads from your CryptDrive (including the trash) or subscribe to a premium offer to increase your limit."; + out.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.pinLimitReachedAlertNoAccounts = out.pinLimitReached; out.pinLimitNotPinned = "You've reached your storage limit.
"+ "This pad is not stored in your CryptDrive."; out.pinLimitDrive = "You've reached your storage limit.
" + @@ -96,6 +115,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'; @@ -109,6 +130,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)"; @@ -177,6 +199,11 @@ define(function () { out.poll_titleHint = "Title"; out.poll_descriptionHint = "Describe your poll, and use the 'publish' button when you're done. Anyone with the link can change the description, but this is discouraged."; + out.poll_remove = "Remove"; + out.poll_edit = "Edit"; + out.poll_locked = "Locked"; + out.poll_unlocked = "Unlocked"; + // Canvas out.canvas_clear = "Clear"; out.canvas_delete = "Delete selection"; @@ -224,14 +251,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"; @@ -277,6 +312,8 @@ define(function () { out.login_invalPass = 'Password required'; out.login_unhandledError = 'An unexpected error occurred :('; + out.login_notRegistered = 'Not registered?'; + out.register_importRecent = "Import pad history (Recommended)"; out.register_acceptTerms = "I accept the terms of service"; out.register_passwordsDontMatch = "Passwords do not match!"; @@ -339,6 +376,23 @@ define(function () { 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.upload_mustLogin = "You must be logged in to upload files"; + 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 @@ -355,6 +409,7 @@ define(function () { out.main_zeroKnowledge = 'Zero Knowledge'; out.main_zeroKnowledge_p = "You don't have to trust that we won't look at your pads, with CryptPad's revolutionary Zero Knowledge Technology we can't. Learn more about how we protect your Privacy and Security."; out.main_writeItDown = 'Write it down'; + out.main_writeItDown_p = "The greatest projects come from the smallest ideas. Take down the moments of inspiration and unexpected ideas because you never know which one might be a breakthrough."; out.main_share = 'Share the link, share the pad'; out.main_share_p = "Grow your ideas together: conduct efficient meetings, collaborate on TODO lists and make quick presentations with all your friends and all your devices."; @@ -438,11 +493,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..75c1c5852 --- /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 documentele 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/customize.dist/translations/messages.zh.js b/customize.dist/translations/messages.zh.js new file mode 100644 index 000000000..81560c69a --- /dev/null +++ b/customize.dist/translations/messages.zh.js @@ -0,0 +1,544 @@ +define(function () { + var out = {}; + // translations must set this key for their language to be available in + // the language dropdowns that are shown throughout Cryptpad's interface + + out._languageName = 'Chinese'; + + out.main_title = "CryptPad: 零知識, 即時協作編寫"; + out.main_slogan = "團結就是力量 - 合作是關鍵"; // TODO remove? + + out.type = {}; + out.type.pad = '富文本'; + out.type.code = '編碼'; + out.type.poll = '投票'; + out.type.slide = '投影片簡報'; + out.type.drive = '磁碟'; + out.type.whiteboard = '白板'; + out.type.file = '檔案'; + out.type.media = '多媒體'; + + out.button_newpad = '富文件檔案'; + out.button_newcode = '新代碼檔案'; + out.button_newpoll = '新投票調查'; + out.button_newslide = '新簡報'; + out.button_newwhiteboard = '新白板'; + + // NOTE: We want to update the 'common_connectionLost' key. + // Please do not add a new 'updated_common_connectionLostAndInfo' but change directly the value of 'common_connectionLost' + out.updated_0_common_connectionLost = "伺服器連線中斷
現在是唯讀狀態,直到連線恢復正常。"; + out.common_connectionLost = out.updated_0_common_connectionLost; + + out.websocketError = '無法連結上 websocket 伺服器...'; + out.typeError = "這個編輯檔與所選的應用程式並不相容"; + out.onLogout = '你已登出, 點擊這裏 來登入
或按Escape 來以唯讀模型使用你的編輯檔案'; + out.wrongApp = "無法在瀏覽器顯示即時期間的內容,請試著再重新載入本頁。"; + + out.loading = "載入中..."; + out.error = "錯誤"; + out.saved = "儲存"; + out.synced = "所有資料已儲存好了"; + out.deleted = "自 CryptDrive 刪除檔案"; + + out.disconnected = '已斷線'; + out.synchronizing = '同步中'; + out.reconnecting = '重新連結...'; + out.lag = 'Lag'; + out.readonly = '唯讀'; + out.anonymous = "匿名"; + out.yourself = "你自己"; + out.anonymousUsers = "匿名的編輯群"; + out.anonymousUser = "匿名的編輯群者"; + out.users = "用戶"; + out.and = "與"; + out.viewer = "檢視者"; + out.viewers = "檢視群"; + out.editor = "編輯者"; + out.editors = "編輯群"; + + out.language = "語言"; + + out.comingSoon = "即將上市..."; + + out.newVersion = 'CryptPad 已更新!
' + + '檢查最新版本有什麼新功能:
'+ + 'CryptPad新發佈記事 {0}'; + + out.upgrade = "昇級"; + out.upgradeTitle = "昇級帳戶以取得更多的儲存空間"; + out.MB = "MB"; + out.GB = "GB"; + out.KB = "KB"; + + out.formattedMB = "{0} MB"; + out.formattedGB = "{0} GB"; + out.formattedKB = "{0} KB"; + + out.greenLight = "每件事都很順利"; + out.orangeLight = "連線速度慢可能會影響用戶體驗"; + out.redLight = "你這段期間的連線已中斷"; + + out.pinLimitReached = "你已達到儲存容量上限"; + out.updated_0_pinLimitReachedAlert = "你已達到儲存容量上限,新檔案不會儲存到你的 CryptDrive.
" + + '要嘛你可以自 CryptDrive 移除原有文件或是 昇級到付費版增加你的儲存容量。'; + out.pinLimitReachedAlert = out.updated_0_pinLimitReachedAlert; + out.pinLimitNotPinned = "你已達到容量使用上限
"+ + "這個檔案無法儲存到你的 CryptDrive."; + out.pinLimitDrive = "你已達到容量使用上限
" + + "你不能建立新的編輯檔案"; + out.importButtonTitle = '從電腦上傳滙入檔案'; + + out.exportButtonTitle = '將這個檔案滙出到電腦'; + out.exportPrompt = '你希望怎麼命名你的檔案?'; + + out.changeNamePrompt = '更換你的名稱(若留空白則會成為無名氏): '; + out.user_rename = "改變顯示名稱"; + out.user_displayName = "顯示名稱"; + out.user_accountName = "帳號名稱"; + + out.clickToEdit = "點擊以編輯"; + + out.forgetButtonTitle = '將這個檔案移置垃圾筒'; + out.forgetPrompt = '點擊 OK 將把這個檔案移置垃圾筒,確定要這樣做嗎'; + out.movedToTrash = '這個檔案已被移置垃圾筒
讀取我的雲端硬碟'; + + out.shareButton = '分享'; + out.shareSuccess = '複製連結到剪貼版'; + + out.newButton = '新'; + out.newButtonTitle = '建立新的工作檔案'; + + out.saveTemplateButton = "存成模版"; + out.saveTemplatePrompt = "為這個模版選一個標題"; + out.templateSaved = "模版已儲存!"; + out.selectTemplate = "選擇一個模版或是按 escape 跳出"; + + out.previewButtonTitle = "顯示或隱藏 Markdown 預覽模式"; + + out.presentButtonTitle = "輸入簡報模式"; + out.presentSuccess = '按 ESC 以退出簡報模式'; + + out.backgroundButtonTitle = '改變簡報的顏色背景'; + out.colorButtonTitle = '在簡報模式下改變文字顏色'; + + out.printButton = "列印 (enter)"; + out.printButtonTitle = "列印投影片或滙出成 PDF 檔案"; + out.printOptions = "版型選項"; + out.printSlideNumber = "顯示投影片號碼"; + out.printDate = "顯示日期"; + out.printTitle = "顯示檔案標題"; + out.printCSS = "自定風格規則 (CSS):"; + out.printTransition = "啟用轉場動畫"; + + out.slideOptionsTitle = "自定你的投影片"; + out.slideOptionsButton = "儲存 (enter)"; + + out.editShare = "編輯連結"; + out.editShareTitle = "複製所編輯的連結到剪貼版"; + out.editOpen = "在新分頁開啟連結編輯"; + out.editOpenTitle = "在新分頁開啟這個檔案為編輯模式"; + out.viewShare = "唯讀連結"; + out.viewShareTitle = "複製唯讀的連結到剪貼版"; + out.viewOpen = "在新分頁開啟唯讀連結"; + out.viewOpenTitle = "在新分頁開啟這個檔案為唯讀模式"; + + out.notifyJoined = "{0} 已加入此協作期間"; + out.notifyRenamed = "{0} 現在改名為 {1}"; + out.notifyLeft = "{0} 已離開了這個協作期間"; + + out.okButton = 'OK (enter)'; + + out.cancel = "取消"; + out.cancelButton = '取消 (esc)'; + + out.historyButton = "顯示文件歷史"; + out.history_next = "到下一個版本"; + out.history_prev = "到之前的版本"; + out.history_goTo = "到所選擇的版本"; + out.history_close = "回到"; + out.history_closeTitle = "關閉歷史記錄"; + out.history_restore = "重建"; + out.history_restoreTitle = "將此文件重建到所挑選的版本"; + out.history_restorePrompt = "確定要將這個展現的版本來取代現有版本嗎?"; + out.history_restoreDone = "文件已重建"; + out.history_version = "版本:"; + + // Polls + + out.poll_title = "零知識日期挑選"; + out.poll_subtitle = "零知識, 即時 排程"; + + out.poll_p_save = "你的設定會立即更新, 因此從不需要按鍵儲存或擔心遺失。"; + out.poll_p_encryption = "你所有幹入的資料都會予以加密,只有取得連結者才可以讀取它。即便是伺服器也不能看到你作了什麼變動。"; + + out.wizardLog = "點擊左上方的按鍵以回到你的調查"; + out.wizardTitle = "使用精靈來建立調查投票"; + out.wizardConfirm = "你真的要新增這些問題到你的調查中嗎?"; + + out.poll_publish_button = "發佈"; + out.poll_admin_button = "管理者"; + out.poll_create_user = "新增使用者"; + out.poll_create_option = "新增選項"; + out.poll_commit = "投入"; + + out.poll_closeWizardButton = "關閉協助精靈"; + out.poll_closeWizardButtonTitle = "關閉協助精靈"; + out.poll_wizardComputeButton = "計算最適化"; + out.poll_wizardClearButton = "清除表格"; + out.poll_wizardDescription = "透過輸入任何日期或時間分段,可自動建立一些選項"; + out.poll_wizardAddDateButton = "+ 日期"; + out.poll_wizardAddTimeButton = "+ 時間"; + + out.poll_optionPlaceholder = "選項"; + out.poll_userPlaceholder = "你的名稱"; + out.poll_removeOption = "確定要移除這個選項嗎?"; + out.poll_removeUser = "確定要移除這位使用者嗎?"; + + out.poll_titleHint = "標題"; + out.poll_descriptionHint = "請簡述這個調查目的,完成時使用「發佈鍵」。任何知道此調查連結者可以更改這裏的描述內容,但我們不鼓勵這麼做。."; + + // Canvas + out.canvas_clear = "清除"; + out.canvas_delete = "刪除所選"; + out.canvas_disable = "取消繪圖"; + out.canvas_enable = "啟動繪圖"; + out.canvas_width = "寛度"; + out.canvas_opacity = "透明度"; + + // File manager + + out.fm_rootName = "根目錄"; + out.fm_trashName = "垃圾桶"; + out.fm_unsortedName = "未整理的檔案"; + out.fm_filesDataName = "所有檔案"; + out.fm_templateName = "模版"; + out.fm_searchName = "搜尋"; + out.fm_searchPlaceholder = "搜尋..."; + out.fm_newButton = "新的"; + out.fm_newButtonTitle = "建立新工作檔案或資料夾"; + out.fm_newFolder = "新資料夾"; + out.fm_newFile = "新工作檔案"; + out.fm_folder = "資料夾"; + out.fm_folderName = "資料夾名稱"; + out.fm_numberOfFolders = "# 個資料夾"; + out.fm_numberOfFiles = "# 檔案"; + out.fm_fileName = "檔案名"; + out.fm_title = "標題"; + out.fm_type = "類型"; + out.fm_lastAccess = "上回使用"; + out.fm_creation = "創建"; + out.fm_forbidden = "禁止的行為"; + out.fm_originalPath = "原始路徑"; + out.fm_openParent = "顯示在目錄夾中"; + out.fm_noname = "無標題文件"; + out.fm_emptyTrashDialog = "確定要清理垃圾筒嗎?"; + out.fm_removeSeveralPermanentlyDialog = "確定要將這些 {0} 東西永自垃圾筒移除嗎?"; + out.fm_removePermanentlyDialog = "你確定要永久地移除這些項目嗎?"; + out.fm_removeSeveralDialog = "確定要將這些 {0} 東西移至垃圾筒嗎?"; + out.fm_removeDialog = "確定要將移動 {0} 至垃圾筒嗎?"; + out.fm_restoreDialog = "確定要重置 {0} 到它之前的位置嗎?"; + out.fm_unknownFolderError = "所選或上回訪問的目錄不再存在了,正開啟上層目錄中..."; + out.fm_contextMenuError = "無法在此元件下打開文本選單。如果這個問題一直發生,請試著重新載入此頁。"; + out.fm_selectError = "無法選取目標的要素。如果這個問題一直發生,請試著重新載入此頁。"; + out.fm_categoryError = "無法打開所選的類別,正在顯示根目錄。"; + out.fm_info_root = "在此建立任何巢狀目錄夾以便於整理分類你的檔案。"; + out.fm_info_unsorted = '包含所有你曾訪問過的檔案,其尚未被整理在 "根目錄" 或移到到"垃圾筒".'; // "My Documents" should match with the "out.fm_rootName" key, and "Trash" with "out.fm_trashName" + out.fm_info_template = '包含所有工作檔案已存成模版,便於讓你在建立新工作檔案時套用。'; + out.updated_0_fm_info_trash = '清空垃圾筒好讓 CryptDrive 多出一些空間'; + out.fm_info_trash = out.updated_0_fm_info_trash; + out.fm_info_allFiles = '包含在 "根目錄", "未整理的" 和 "垃圾筒" 裏的所有檔案。這裏你無法移動或移除檔案。'; // Same here + out.fm_info_anonymous = '你尚未登入,因此這些工作檔案可能會被刪除。 (了解原因). ' + + '註冊登入以便保留它們。'; + out.fm_alert_backupUrl = "這個雲端硬碟的備份連結
" + + "高度建議把自己的 IP 資訊保留成只有自己知道
" + + "萬一瀏覽器記憶被消除,你可以用它來接收所有的檔案。
" + + "任何知道此連結的人可以編輯或移除你檔案管理底下的所有檔案。
"; + out.fm_alert_anonymous = "嗨你好, 你目前正以匿名方式在使用 CryptPad , 這也沒問題,不過你的東西過一段時間沒動靜後,就會自動被刪除。 " + + "匿名的用戶我們也取消其進階功能,因為我們要明確地讓用戶知道,這裏 " + + '不是一個安全存放東西的地方。你可以 進一步了解 關於 ' + + '為何我們這樣作,以及為何你最好能夠註冊 以及 登錄使用。'; + out.fm_backup_title = '備份連結'; + out.fm_nameFile = '你想要如何來命名這個檔案呢?'; + out.fm_error_cantPin = "內部伺服器出錯,請重新載入本頁並再試一次。"; + // File - Context menu + out.fc_newfolder = "新資料夾"; + out.fc_rename = "重新命名"; + out.fc_open = "打開"; + out.fc_open_ro = "打開 (唯讀)"; + out.fc_delete = "刪除"; + out.fc_restore = "重置"; + out.fc_remove = "永久刪除"; + out.fc_empty = "清理垃圾筒"; + out.fc_prop = "Properties"; + out.fc_sizeInKilobytes = "容量大小 (Kilobytes)"; + // fileObject.js (logs) + out.fo_moveUnsortedError = "你不能移動資料夾到未整理的工作檔案清單"; + out.fo_existingNameError = "名稱已被使用,請選擇其它名稱"; + out.fo_moveFolderToChildError = "你不能移動資料夾到它的子資料夾底下"; + out.fo_unableToRestore = "無法將這個檔案重置到原始的位置。你可以試著將它移動到其它新位置。"; + out.fo_unavailableName = "在新位置裏同名的檔案或資料夾名稱已存在,請重新命名後再試看看。"; + + // login + out.login_login = "登入"; + out.login_makeAPad = '匿名地建立一個工作檔案'; + out.login_nologin = "瀏覽本地的工作檔案"; + out.login_register = "註冊"; + out.logoutButton = "登出"; + out.settingsButton = "設定"; + + out.login_username = "用戶名"; + out.login_password = "密碼"; + out.login_confirm = "確認你的密碼"; + out.login_remember = "記住我"; + + out.login_hashing = "散列你的密碼中,這要花上一點時間"; + + out.login_hello = 'Hello {0},'; // {0} is the username + out.login_helloNoName = 'Hello,'; + out.login_accessDrive = '取用你的磁碟'; + out.login_orNoLogin = '或'; + + out.login_noSuchUser = '無效的用戶名或密碼,請再試一次或重新註冊'; + out.login_invalUser = '要求用戶名'; + out.login_invalPass = '要求密碼'; + out.login_unhandledError = '發生了未預期的錯誤 :('; + + out.register_importRecent = "滙入檔案記錄 (建議)"; + out.register_acceptTerms = "我同意 服務條款"; + out.register_passwordsDontMatch = "密碼不相符!"; + out.register_mustAcceptTerms = "你必須同意我們的服務條款。"; + out.register_mustRememberPass = "如果你忘了密碼,我們也無法為你重置。因此務必自行好好記住! 請在勾選處勾選確認。"; + + out.register_header = "歡迎來到 CryptPad"; + out.register_explanation = [ + "

首先讓我們先了解幾件事

", + "" + ].join(''); + + out.register_writtenPassword = "我已記下了我的用戶名和密碼,請繼續"; + out.register_cancel = "回去"; + + out.register_warning = "零知識表示如果你遺失了密碼,我們也無法還原你的資料"; + + out.register_alreadyRegistered = "這名用戶己存在了,你要登入嗎?"; + + // Settings + out.settings_title = "設定"; + out.settings_save = "儲存"; + out.settings_backupTitle = "備份或重建你所有的資料"; + out.settings_backup = "備份"; + out.settings_restore = "重建"; + out.settings_resetTitle = "清除你的雲端硬碟"; + out.settings_reset = "從你的 CryptDrive 移除所有的檔案和資料夾"; + out.settings_resetPrompt = "這個動作會自你的雲端硬碟中移除所有工作檔案
"+ + "確定要繼續嗎?
" + + "輸入 “I love CryptPad” 來確認。"; + out.settings_resetDone = "你的目錄現已清空!"; + out.settings_resetError = "不正確的認證文字,你的 CryptDrive 並未更改。"; + out.settings_resetTips = "使用 CryptDrive 的竅門"; + out.settings_resetTipsButton = "在 CryptDrive 下重置可用的訣竅"; + out.settings_resetTipsDone = "所有的訣竅現在都可再次看到了。"; + + out.settings_importTitle = "滙入這個瀏覽器近期的工作檔案到我的 CryptDrive"; + out.settings_import = "滙入"; + out.settings_importConfirm = "確定要從這個瀏覽器滙入近期的工作檔案到你的 CryptDrive ?"; + out.settings_importDone = "滙入完成"; + + out.settings_userFeedbackHint1 = "CryptPad 會提供一些基本的反饋到伺服器,以讓我們知道如何改善用戶體驗。"; + out.settings_userFeedbackHint2 = "你的工作檔案內容絕不會被分享到伺服器"; + out.settings_userFeedback = "啟用用戶反饋功能"; + + out.settings_anonymous = "你尚未登入,在此瀏覽器上進行特別設定。"; + out.settings_publicSigningKey = "公開金鑰簽署"; + + out.settings_usage = "用法"; + out.settings_usageTitle = "查看所有置頂的工作檔案所佔的容量"; + out.settings_pinningNotAvailable = "工作檔案置頂功能只開放給已註冊用戶"; + out.settings_pinningError = "有點不對勁"; + out.settings_usageAmount = "你置頂的工作檔案佔了 {0}MB"; + + out.settings_logoutEverywhereTitle = "自所有地點登出"; + out.settings_logoutEverywhere = "自所有其它的網頁期間登出"; + out.settings_logoutEverywhereConfirm = "你確定嗎?你將需要登入到所有用到設置。"; + + out.upload_serverError = "伺服器出錯:本次無法上傳你的檔案"; + out.upload_uploadPending = "你欲上傳檔案正在傳輸中,要取消並上傳新檔案嗎?"; + out.upload_success = "你的檔案 ({0}) 已成功地上傳並放入到你的網路磁碟中。"; + out.upload_notEnoughSpace = "你的 CryptDrive 無足夠空間來存放這個檔案。"; + out.upload_tooLarge = "此檔案超過了上傳單一檔案可允許的容量上限。"; + out.upload_choose = "選擇一個檔案"; + out.upload_pending = "待處理"; + out.upload_cancelled = "已取消的"; + out.upload_name = "檔案名"; + out.upload_size = "大小"; + out.upload_progress = "進度"; + out.download_button = "解密 & 下載"; + + // general warnings + out.warn_notPinned = "這個工作檔案並不在任何人的 CryptDrive 裏,它將在 3 個月到期後刪除。 進一步了解..."; + + // index.html + + + //about.html + out.main_p2 = '本專案使用 CKEditor 視覺編輯器, CodeMirror, 以及 ChainPad 即時引擊。'; + out.main_howitworks_p1 = 'CryptPad 應用一種變體的 操作型變換 Operational transformation 演算法,它利用Nakamoto Blockchain來找到分散的共識, Nakamoto Blockchain 是一種建構當前流行的比特幣。這套演算法可避免需要一個中央的伺服器來解析操作型變換編輯衝突,而無須處理解析衝突,伺服器並不知道哪一個檔案被編輯。'; + + // contact.html + out.main_about_p2 = '若有任何問題和建議, 可以在tweet us, github提出問題, 或是來到 irc (irc.freenode.net)打聲招呼, 再或者 寄封電郵給我們.'; + + out.main_info = "

Collaborate in Confidence


利用共同享文件發嚮點子,透過 零知識 科技確保隱私安全; 對任何網路服務商都要加以提防。"; + + out.main_howitworks = '它如何運作'; + out.main_zeroKnowledge = '零知識'; + out.main_zeroKnowledge_p = "你不必相信我們所說的並不會 察看你的檔案, CryptPad 革命性的零知識技術讓我們 真的不能看到。 進一步了解在這裏,我們如何保護用戶的 隱私和安全。"; + out.main_writeItDown = '寫下它'; + out.main_writeItDown_p = "偉大的專案來自不起眼的小點子。記下靈感與點子的瞬間,因為你從不會知道哪個會帶來重大突破。"; + out.main_share = '分享連結, 分享工作檔案'; + out.main_share_p = "一起來發響想法點子: 在任何設備上,與朋友一起執行有效率的會議, 協作待辦清單與快速製作簡報。"; + out.main_organize = 'Get organized'; + out.main_organize_p = "利用 CryptPad 空間, 你可以保留看管重要的東西。資料夾讓你可以追踪專案和全盤了解事情的走向狀況。"; + out.tryIt = 'Try it out!'; + out.main_richText = '富文字編輯器'; + out.main_richText_p = '利用我們的即時零知識技術,集體協作地編輯富文本檔案 CkEditor 應用程式application.'; + out.main_code = '代碼編輯器'; + out.main_code_p = '利用我們的即時零知識技術,集體協作地編輯程式代碼 CodeMirror 應用程式。'; + out.main_slide = '投影片編輯器'; + out.main_slide_p = '使用 Markdown 語法來建立投影片,並利用瀏覽器來展示投影片。'; + out.main_poll = '調查'; + out.main_poll_p = '規劃會議或活動,或是為問題舉行投最佳方案的投票。'; + out.main_drive = 'CryptDrive'; + + out.footer_applications = "應用程式"; + out.footer_contact = "聯繫"; + out.footer_aboutUs = "關於 Cryptpad"; + + out.about = "關於"; + out.privacy = "隱私"; + out.contact = "聯繫"; + out.terms = "服務條款"; + out.blog = "Blog"; + + // privacy.html + + out.policy_title = 'CryptPad 隱私政策'; + out.policy_whatweknow = '我們會知道哪些關於你的資料'; + out.policy_whatweknow_p1 = '作為一個網頁上的應用程式, CryptPad 可以接取 HTTP 協議所曝露的元數據。 這包括你的 IP 地址、各式其它的 HTTP 標頭,其用於識別你特定的瀏覽器。 你可以訪問 WhatIsMyBrowser.com這個網站,知道你的瀏覽器分享了哪些資訊。'; + out.policy_whatweknow_p2 = '我們使用 Kibana, 它是一個開源的流量數據分析平台, 以更了解用戶。Kibana 讓我們知道你是如何地發現 CryptPad, 是透過直接接入、攑搜尋引擊或是其它網站的介紹如 Reddit 和 Twitter。'; + out.policy_howweuse = '我們如何利用我們知道的東西'; + out.policy_howweuse_p1 = '我們利用這些資訊評估過去成功的效果,以更佳地決定如何推廣 CryptPad。有關你地理位置的資訊讓我們知道是否該提供英語之外的語言版本支援'; + out.policy_howweuse_p2 = "有關你的瀏覽器資訊 (是桌面還是手機操作系統) 有助於讓我們決定要優先哪些功能改善。我們開發團隊人很少,我們試著挑選盡可能地提昇更多用戶的使用體驗。"; + out.policy_whatwetell = '我們可以告訴別人關於你的哪些資料'; + out.policy_whatwetell_p1 = '我們不會給第三人我們所收集的資訊,除非被依法要求配合。'; + out.policy_links = '其它網站連結'; + out.policy_links_p1 = '本站含有其它網站的連結,包括其它組織的産品。我們無法對這些隱私實踐或任何本站以外的內容負責。一般而言,連到外站的連結會另啟新視窗,以明確讓你知道已離開了CryptPad.fr.'; + out.policy_ads = '廣告'; + out.policy_ads_p1 = '我們不會放置任何線上廣告,但會提供一些資助我們研究的機構與團體的網址連結'; + out.policy_choices = '你有的選擇'; + out.policy_choices_open = '我們的代碼是開放的,你可以選擇自行在自己的機器上來架設自己的 CryptPad.'; + out.policy_choices_vpn = '如果你要使用我們架設的服務, 但不希望曝露自己的 IP 地址, 你可以利用Tor 瀏覽器套件來保護隱藏 IP 地址, 或是使用 VPN。'; + out.policy_choices_ads = '如果你只是想要封鎖我們的數據分析器, 你可以使用廣告封鎖工具如 Privacy Badger.'; + + // terms.html + + out.tos_title = "CryptPad 服務條款"; + out.tos_legal = "請不要惡意、濫用或從事非法活動。"; + out.tos_availability = "希望你覺得我們的産品與服務對你有所幫助, 但我們並不能一直百分百保證它的表現穩定與可得性。請記得定期滙出你的資料。"; + out.tos_e2ee = "CryptPad 的內容可以被任何猜出或取得工作檔案分段識別碼的人讀取與修改。我們建議你使用端對端加密 (e2ee) 訊息技術來分享工作檔案連結 以及假設如果一旦連結外漏不會背上任何責任。"; + out.tos_logs = "你的瀏覽器提供給伺服器的元數據,可能會因為維護本服務之效能而被收集記錄。"; + out.tos_3rdparties = "除非法令要求,我們不會提供任何個人資料給第三方。"; + + // BottomBar.html + + out.bottom_france = 'Made with love in France'; + out.bottom_support = 'An XWiki SAS Labs Project with the support of OpenPaaS-ng'; + + // Header.html + + out.header_france = 'With love from France by XWiki SAS'; + + out.header_support = ' OpenPaaS-ng'; + out.header_logoTitle = '回到主頁'; + + // Initial states + + out.initialState = [ + '

', + '這是 CryptPad, 零知識即時協作編輯平台,當你輸入時一切已即存好。', + '
', + '分享這個工作檔案的網址連結給友人或是使用、  分享  按鈕分享唯讀的連結 其只能看不能編寫。', + '

', + + '

', + '來吧, 開始打字輸入吧...', + '

', + '

 

' + ].join(''); + + out.codeInitialState = [ + '# CryptPad 零知識即時協作代碼編輯平台\n', + '\n', + '* 你所輸入的東西會予以加密,僅有知道此網頁連結者可以接取這份文件。\n', + '* 你可以在右上角選擇欲編寫的程式語言以及樣版配色風格。' + ].join(''); + + out.slideInitialState = [ + '# CryptSlide\n', + '* 它是零知識即時協作編輯平台。\n', + '* 你所輸入的東西會予以加密,僅有知道此網頁連結者可以接取這份文件。\n', + '* 即便是本站伺服器也不知道你輸入了什麼內容。\n', + '* 你在這裏看到的、你在這裏聽到的、當你離開本站時,讓它就留在這裏吧。\n', + '\n', + '---', + '\n', + '# 如何使用\n', + '1. 使用 markdown 語法來寫下你的投影片內容\n', + ' - 進一步學習 markdown 語法 [here](http://www.markdowntutorial.com/)\n', + '2. 利用 --- 來區隔不同的投影片\n', + '3. 點擊下方 "Play" 鍵來查看成果', + ' - 你的投影片會即時更新' + ].join(''); + + // Readme + + out.driveReadmeTitle = "什麼是 CryptDrive?"; + out.readme_welcome = "歡迎來到 CryptPad !"; + out.readme_p1 = "歡迎來到 CryptPad, 這裏你可以獨自作個人筆記或是和別人共享協作。"; + out.readme_p2 = "這個工作檔案可以讓你快速地了解如何使用 CryptPad 作筆記,有效地整理管理文件工作檔案。"; + out.readme_cat1 = "認識如何使用 CryptDrive"; + out.readme_cat1_l1 = "建立一個工作檔案: 在 CryptDrive 底下, 點擊 {0} 然後 {1} 這樣就可以建立一個新的工作檔案。"; // 0: New, 1: Rich Text + out.readme_cat1_l2 = "從 CryptDrive 開啟工作檔案: 雙擊工作檔案的圖示來開啟它。"; + out.readme_cat1_l3 = "分類你的工作檔案:登入之後,每一個你能接取使用的工作檔案會顯示在你雲端硬碟中的 {0} 部份。"; // 0: Unsorted files + out.readme_cat1_l3_l1 = "你可以點擊或是拉曳檔案到雲端硬碟 {0} 區,新增資料夾。"; // 0: Documents + out.readme_cat1_l3_l2 = "記得試著點擊圖示,以顯示更多的選項功能。"; + out.readme_cat1_l4 = "把舊的工作檔案放到垃圾筒:點擊或是拉曳檔案到 {0} 如同把它們拉到文件目錄夾一樣的方法。"; // 0: Trash + out.readme_cat2 = "像個專業人士來編寫你的工作檔案"; + out.edit = "編輯"; + out.view = "檢視"; + out.readme_cat2_l1 = "在工作檔案下的 {0} 按鍵可讓其它的協作者接取 {1} 或是 {2} 工作檔案"; // 0: Share, 1: edit, 2: view + out.readme_cat2_l2 = "若要更改工作檔案的名稱,只要點擊右上的鉛筆圖示即可"; + out.readme_cat3 = "發現其它的 CryptPad 應用"; + out.readme_cat3_l1 = "使用 CryptPad 代碼編輯器,你可以和其它人協作各種程式碼,如 Javascript、 markdown、 HTML 等等。"; + out.readme_cat3_l2 = "使用 CryptPad 投影片編輯功能,你可以使用 Markdown 快速製作簡報檔。"; + out.readme_cat3_l3 = "利用 CryptPoll 你可以快速作個線上調查,尤其是調查每個人有空的會議時間。"; + + // Tips + out.tips = {}; + out.tips.lag = "右上角的綠色圖標顯示你連線至 CryptPad 伺服器的連線品質。"; + out.tips.shortcuts = "`ctrl+b`, `ctrl+i` 和 `ctrl+u` 分別是粗體字、斜體、與加底線用法的快速鍵。"; + out.tips.indent = "要使用數字以及符號列表, 可使用 tab 或 shift+tab 快速地增加或滅少縮排指令。"; + out.tips.title = "點擊正上方來設定工作檔案的標題。"; + out.tips.store = "每一回你造訪一個工作檔案, 如果是登入狀態,則這些檔案會自動儲存到你的 CryptDrive."; + out.tips.marker = "在格式下拉選單中使用 \"marker\" 可以標注反亮文字."; + + out.feedback_about = "如果你讀了這裏,也許會好奇為何當你執行某些動作時 CryptPad 會請求網頁資訊。"; + out.feedback_privacy = "我們注重你的隱私,同時也要讓 CryptPad 容易使用。我們利用這個檔案來了解哪一種介面設計為用戶所重視,透過它來請求特別的功能參數。"; + out.feedback_optout = "如果欲退出客戶資料收集, 請到 用戶設定頁, 可以找到勾選項目來啟用或關閉用戶回饋功能。"; + + return out; +}); + diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/cryptpad-docker.md b/docs/cryptpad-docker.md similarity index 100% rename from cryptpad-docker.md rename to docs/cryptpad-docker.md diff --git a/example.nginx.conf b/docs/example.nginx.conf similarity index 89% rename from example.nginx.conf rename to docs/example.nginx.conf index fcb8b7435..37cb0da60 100644 --- a/example.nginx.conf +++ b/docs/example.nginx.conf @@ -1,3 +1,9 @@ +# This file is included strictly as an example of how Nginx can be configured +# to work with CryptPad. This example WILL NOT WORK AS IS. For best results, +# compare the sections of this configuration file against a working CryptPad +# installation (http server by the Nodejs process). If you are using CryptPad +# in production, contact sales@cryptpad.fr + server { listen 443 ssl http2; diff --git a/package.json b/package.json index 696091528..9454404ac 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "1.6.0", + "version": "1.8.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 && lessc ./www/code/code.less > ./www/code/code.css", + "template": "cd customize.dist/src && node build.js" } } diff --git a/pinneddata.js b/pinneddata.js new file mode 100644 index 000000000..8dbbc7fec --- /dev/null +++ b/pinneddata.js @@ -0,0 +1,115 @@ +/* jshint esversion: 6, node: true */ +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, dsFileStats) => { + let sum = 0; + hashes.forEach((h) => { + const s = dsFileStats[h]; + if (typeof(s) !== 'number') { + //console.log('missing ' + h + ' ' + typeof(s)); + } else { + sum += s.size; + } + }); + return sum; +}; + +const sema = Semaphore.create(20); + +let dirList; +const fileList = []; +const dsFileStats = {}; +const out = []; +const pinned = {}; + +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; } + dsFileStats[f.replace(/^.*\/([^\/]*)\.ndjson$/, (all, a) => (a))] = st; + }))); + }); + }); +}).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, dsFileStats); + if (process.argv.indexOf('--unpinned') > -1) { + hashes.forEach((x) => { pinned[x] = 1; }); + } else { + out.push([f, Math.floor(size / (1024 * 1024))]); + } + }))); + }); + }); +}).nThen(() => { + if (process.argv.indexOf('--unpinned') > -1) { + Object.keys(dsFileStats).forEach((f) => { + if (!(f in pinned)) { + console.log("./datastore/" + f.slice(0,2) + "/" + f + ".ndjson " + + dsFileStats[f].size + " " + (+dsFileStats[f].mtime)); + } + }); + } else { + out.sort((a,b) => (a[1] - b[1])); + out.forEach((x) => { console.log(x[0] + ' ' + x[1] + ' MB'); }); + } +}); diff --git a/rpc.js b/rpc.js index db3e98faa..715abd118 100644 --- a/rpc.js +++ b/rpc.js @@ -1,4 +1,5 @@ /*@flow*/ +/*jshint esversion: 6 */ /* Use Nacl for checking signatures of messages */ var Nacl = require("tweetnacl"); @@ -7,13 +8,18 @@ var Nacl = require("tweetnacl"); var Fs = require("fs"); var Path = require("path"); +var Https = require("https"); +const Package = require('./package.json'); 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) { @@ -33,10 +39,10 @@ var uint8ArrayToHex = function (a) { }).join(''); }; -var createChannelId = function () { - var id = uint8ArrayToHex(Nacl.randomBytes(16)); - if (id.length !== 32 || /[^a-f0-9]/.test(id)) { - throw new Error('channel ids must consist of 32 hex characters'); +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; }; @@ -70,12 +76,22 @@ var parseCookie = function (cookie) { return c; }; +var escapeKeyCharacters = function (key) { + return key.replace(/\//g, '-'); +}; + +var unescapeKeyCharacters = function (key) { + return key.replace(/\-/g, '/'); +}; + +// TODO Rename to getSession ? 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() @@ -103,7 +119,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(); } @@ -125,7 +141,7 @@ var isValidCookie = function (Sessions, publicKey, cookie) { return false; } - var user = Sessions[publicKey]; + var user = beginSession(Sessions, publicKey); if (!user) { return false; } var idx = user.tokens.indexOf(parsed.seq); @@ -179,8 +195,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); @@ -197,7 +214,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 { @@ -239,28 +256,58 @@ 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'); } - if (typeof(store.getChannelSize) !== 'function') { - return cb('GET_CHANNEL_SIZE_UNSUPPORTED'); +var makeFilePath = function (root, id) { + if (typeof(id) !== 'string' || id.length <= 2) { return null; } + return Path.join(root, id.slice(0, 2), id); +}; + +var getUploadSize = function (Env, channel, cb) { + var paths = Env.paths; + var path = makeFilePath(paths.blob, channel); + if (!path) { + return cb('INVALID_UPLOAD_ID'); } - return void store.getChannelSize(channel, function (e, size) { - if (e) { return void cb(e.code); } + 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 getMultipleFileSize = function (store, channels, cb) { - - if (!Array.isArray(channels)) { return cb('INVALID_LIST'); } - if (typeof(store.getChannelSize) !== 'function') { +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'); } @@ -273,33 +320,28 @@ var getMultipleFileSize = function (store, channels, cb) { }; channels.forEach(function (channel) { - if (!isValidChannel(channel)) { - counts[channel] = -1; - return done(); - } - store.getChannelSize(channel, function (e, size) { + getFileSize(Env, channel, function (e, size) { if (e) { + console.error(e); counts[channel] = -1; return done(); } - counts[channel] = size; done(); }); }); }; -var getTotalSize = function (pinStore, messageStore, Sessions, publicKey, cb) { +var getTotalSize = function (Env, publicKey, cb) { var bytes = 0; - - return void getChannelList(pinStore, Sessions, publicKey, function (channels) { - if (!channels) { cb('NO_ARRAY'); } // unexpected + 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); } @@ -322,24 +364,124 @@ 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 (config.adminEmail === false) { + if (config.allowSubscriptions === false) { return; } + throw new Error("allowSubscriptions must be false if adminEmail is false"); + } + if (typeof cb !== "function") { cb = function () {}; } -var pinChannel = function (store, Sessions, publicKey, channels, cb) { - if (!channels && channels.filter) { - // expected array - return void cb('[TYPE_ERROR] pin expects channel list argument'); + var defaultLimit = typeof(config.defaultStorageLimit) === 'number'? + config.defaultStorageLimit: DEFAULT_LIMIT; + + var userId; + if (publicKey) { + userId = unescapeKeyCharacters(publicKey); } - getChannelList(store, Sessions, publicKey, function (pinned) { - var session = beginSession(Sessions, publicKey); + var body = JSON.stringify({ + domain: config.myDomain, + subdomain: config.mySubdomain, + adminEmail: config.adminEmail, + version: Package.version + }); + 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 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) { + return void cb('INVALID_PIN_LIST'); + } + + // 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) { @@ -347,28 +489,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) { @@ -376,35 +532,56 @@ 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) { - 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); - getHash(store, Sessions, publicKey, function (e, hash) { - cb(e, hash); + 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); + }); + }); }); }); }; @@ -432,11 +609,6 @@ var isPrivilegedUser = function (publicKey, cb) { cb(list.indexOf(publicKey) !== -1); }); }; - -var getLimit = function (cb) { - cb = cb; // TODO -}; - var safeMkdir = function (path, cb) { Fs.mkdir(path, function (e) { if (!e || e.code === 'EEXIST') { return void cb(); } @@ -444,11 +616,6 @@ var safeMkdir = function (path, cb) { }); }; -var makeFilePath = function (root, id) { - if (typeof(id) !== 'string' || id.length <= 2) { return null; } - return Path.join(root, id.slice(0, 2), id); -}; - var makeFileStream = function (root, id, cb) { var stub = id.slice(0, 2); var full = makeFilePath(root, id); @@ -459,6 +626,7 @@ var makeFileStream = function (root, id, cb) { var stream = Fs.createWriteStream(full, { flags: 'a', encoding: 'binary', + highWaterMark: Math.pow(2, 16), }); stream.on('open', function () { cb(void 0, stream); @@ -469,29 +637,53 @@ var makeFileStream = function (root, id, cb) { }); }; -var upload = function (stagingPath, Sessions, publicKey, content, cb) { - var dec = new Buffer(Nacl.util.decodeBase64(content)); // jshint ignore:line +var upload = function (Env, publicKey, content, cb) { + var paths = Env.paths; + var dec; + try { dec = Buffer.from(content, 'base64'); } + catch (e) { return void cb(e); } + var len = dec.length; + + var session = beginSession(Env.Sessions, publicKey); + + if (typeof(session.currentUploadSize) !== 'number' || + 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'); + } - var session = Sessions[publicKey]; - session.atime = +new Date(); if (!session.blobstage) { - makeFileStream(stagingPath, publicKey, function (e, stream) { + 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 (stagingPath, Sessions, publicKey, cb) { - var path = makeFilePath(stagingPath, publicKey); +var upload_cancel = function (Env, publicKey, cb) { + var paths = Env.paths; + + var session = beginSession(Env.Sessions, publicKey); + delete session.currentUploadSize; + delete session.pendingUploadSize; + if (session.blobstage) { session.blobstage.close(); } + + var path = makeFilePath(paths.staging, publicKey); if (!path) { - console.log(stagingPath, publicKey); + console.log(paths.staging, publicKey); console.log(path); return void cb('NO_FILE'); } @@ -512,24 +704,27 @@ var isFile = function (filePath, cb) { }); }; -var upload_complete = function (stagingPath, storePath, Sessions, publicKey, cb) { - var session = Sessions[publicKey]; +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(stagingPath, publicKey); + var oldPath = makeFilePath(paths.staging, publicKey); var tryRandomLocation = function (cb) { - var id = createChannelId(); + var id = createFileId(); var prefix = id.slice(0, 2); - var newPath = makeFilePath(storePath, id); + var newPath = makeFilePath(paths.blob, id); - safeMkdir(Path.join(storePath, prefix), function (e) { + 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) { @@ -546,24 +741,77 @@ var upload_complete = function (stagingPath, storePath, Sessions, publicKey, cb) }); }; - tryRandomLocation(function (e, 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); + }); }); }; -var upload_status = function (stagingPath, Sessions, publicKey, cb) { - var filePath = makeFilePath(stagingPath, publicKey); - if (!filePath) { return void cb('E_INVALID_PATH'); } - isFile(filePath, function (e, yes) { - cb(e, yes); - }); +var isAuthenticatedCall = function (call) { + return [ + 'COOKIE', + 'RESET', + 'PIN', + 'UNPIN', + 'GET_HASH', + 'GET_TOTAL_SIZE', + 'GET_FILE_SIZE', + 'UPDATE_LIMITS', + 'GET_LIMIT', + 'GET_MULTIPLE_FILE_SIZE', + //'UPLOAD', + 'UPLOAD_COMPLETE', + 'UPLOAD_CANCEL', + ].indexOf(call) !== -1; }; /*::const ConfigType = require('./config.example.js');*/ @@ -571,17 +819,27 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) // load pin-store... console.log('loading rpc module...'); - var Sessions = {}; + var warn = function (e, output) { + if (e && !config.suppressRPCErrors) { + console.error(new Date().toISOString() + ' [' + e + ']', output); + } + }; var keyOrDefaultString = function (key, def) { return typeof(config[key]) === 'string'? config[key]: def; }; - var pinPath = keyOrDefaultString('pinPath', './pins'); - var blobPath = keyOrDefaultString('blobPath', './blob'); - var blobStagingPath = keyOrDefaultString('blobStagingPath', './blobstage'); + var Env = {}; + Env.defaultStorageLimit = config.defaultStorageLimit; - var store; + 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 }*/, @@ -611,7 +869,6 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) beginSession(Sessions, publicKey); var cookie = msg[0]; - if (!isValidCookie(Sessions, publicKey, cookie)) { // no cookie is fine if the RPC is to get a cookie if (msg[1] !== 'COOKIE') { @@ -625,22 +882,27 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY'); } - if (checkSignature(serialized, signature, publicKey) !== true) { - return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY"); + if (isAuthenticatedCall(msg[1])) { + 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. We can safely modify the state for that key + + OR it's an unauthenticated call, which must not modify the state + for that key in a meaningful way. */ // discard validated cookie from message 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(typeof(msg) !== 'undefined' ?msg: [])); }; @@ -653,64 +915,98 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?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': // TODO don't pin if over the limit - // if over, send error E_OVER_LIMIT - return pinChannel(store, Sessions, safeKey, msg[1], function (e, hash) { + case 'PIN': + 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': // TODO cache this, since it will get called quite a bit - return getTotalSize(store, ctx.store, Sessions, safeKey, function (e, size) { - if (e) { return void Respond(e); } + 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); - case 'GET_LIMIT': // TODO implement this and cache it per-user - return void getLimit(function (e, limit) { - limit = limit; - Respond('NOT_IMPLEMENTED'); + 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(ctx.store, msg[1], function (e, dict) { - if (e) { return void Respond(e); } + 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(blobStagingPath, Sessions, safeKey, msg[1], function (e, len) { + return void upload(Env, safeKey, msg[1], function (e, len) { + warn(e, len); Respond(e, len); }); case 'UPLOAD_STATUS': if (!privileged) { return deny(); } - return void upload_status(blobStagingPath, Sessions, safeKey, function (e, stat) { - Respond(e, stat); + 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(blobStagingPath, blobPath, Sessions, safeKey, function (e, hash) { + 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(blobStagingPath, Sessions, safeKey, function (e) { + return void upload_cancel(Env, safeKey, function (e) { + warn(e); Respond(e); }); default: @@ -729,7 +1025,7 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) } // if session has not been authenticated, do so - var session = Sessions[publicKey]; + var session = beginSession(Sessions, safeKey); if (typeof(session.privilege) !== 'boolean') { return void isPrivilegedUser(publicKey, function (yes) { session.privilege = yes; @@ -741,10 +1037,18 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) 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: pinPath, }, function (s) { - store = s; + Env.pinStore = s; safeMkdir(blobPath, function (e) { if (e) { throw e; } diff --git a/server.js b/server.js index d7f5b90fc..eb16fbcf1 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; @@ -82,7 +83,7 @@ var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'cont var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); app.get(mainPagePattern, Express.static(__dirname + '/customize.dist')); -app.use("/blob", Express.static(__dirname + '/blob')); +app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob')))); app.use("/customize", Express.static(__dirname + '/customize')); app.use("/customize", Express.static(__dirname + '/customize.dist')); @@ -120,6 +121,9 @@ app.get('/api/config', function(req, res){ waitSeconds: 60, urlArgs: 'ver=' + Package.version + (DEV_MODE? '-' + (+new Date()): ''), }, + removeDonateButton: (config.removeDonateButton === true), + allowSubscriptions: (config.allowSubscriptions === true), + websocketPath: config.useExternalWebsocket ? undefined : config.websocketPath, websocketURL:'ws' + ((useSecureWebsockets) ? 's' : '') + '://' + host + ':' + websocketPort + '/cryptpad_websocket', diff --git a/www/assert/main.js b/www/assert/main.js index 392392db8..d47ec0210 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -4,7 +4,8 @@ define([ '/bower_components/textpatcher/TextPatcher.amd.js', 'json.sortify', '/common/cryptpad-common.js', -], function ($, Hyperjson, TextPatcher, Sortify, Cryptpad) { + '/common/test.js' +], function ($, Hyperjson, TextPatcher, Sortify, Cryptpad, Test) { window.Hyperjson = Hyperjson; window.TextPatcher = TextPatcher; window.Sortify = Sortify; @@ -15,26 +16,41 @@ define([ var failMessages = []; var ASSERTS = []; - var runASSERTS = function () { + var runASSERTS = function (cb) { + var count = ASSERTS.length; + var successes = 0; + + var done = function (err) { + count--; + if (err) { failMessages.push(err); } + else { successes++; } + if (count === 0) { cb(); } + }; + ASSERTS.forEach(function (f, index) { - f(index); + f(function (err) { + console.log("test " + index); + done(err, index); + }, index); }); }; var assert = function (test, msg) { - ASSERTS.push(function (i) { - var returned = test(); - if (returned === true) { - assertions++; - } else { - failed = true; - failedOn = assertions; - failMessages.push({ - test: i, - message: msg, - output: returned, - }); - } + ASSERTS.push(function (cb, i) { + test(function (result) { + if (result === true) { + assertions++; + cb(); + } else { + failed = true; + failedOn = assertions; + cb({ + test: i, + message: msg, + output: result, + }); + } + }); }); }; @@ -58,7 +74,7 @@ define([ }; var HJSON_equal = function (shjson) { - assert(function () { + assert(function (cb) { // parse your stringified Hyperjson var hjson; @@ -82,10 +98,10 @@ define([ var diff = TextPatcher.format(shjson, op); if (success) { - return true; + return cb(true); } else { - return '

insert: ' + diff.insert + '

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

'; + return cb('

insert: ' + diff.insert + '

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

'); } }, "expected hyperjson equality"); }; @@ -94,7 +110,7 @@ define([ var roundTrip = function (sel) { var target = $(sel)[0]; - assert(function () { + assert(function (cb) { var hjson = Hyperjson.fromDOM(target); var cloned = Hyperjson.toDOM(hjson); var success = cloned.outerHTML === target.outerHTML; @@ -111,7 +127,7 @@ define([ TextPatcher.log(target.outerHTML, op); } - return success; + return cb(success); }, "Round trip serialization introduced artifacts."); }; @@ -125,9 +141,9 @@ define([ var strungJSON = function (orig) { var result; - assert(function () { + assert(function (cb) { result = JSON.stringify(JSON.parse(orig)); - return result === orig; + return cb(result === orig); }, "expected result (" + result + ") to equal original (" + orig + ")"); }; @@ -138,46 +154,56 @@ define([ }); // check that old hashes parse correctly - assert(function () { - var secret = Cryptpad.parseHash('67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy'); - return secret.channel === "67b8385b07352be53e40746d2be6ccd7" && - secret.key === "XAYSuJYYqa9NfmInyHci7LNy" && - secret.version === 0; + assert(function (cb) { + var secret = Cryptpad.parsePadUrl('/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy'); + return cb(secret.hashData.channel === "67b8385b07352be53e40746d2be6ccd7" && + secret.hashData.key === "XAYSuJYYqa9NfmInyHci7LNy" && + secret.hashData.version === 0); }, "Old hash failed to parse"); // make sure version 1 hashes parse correctly - assert(function () { - var secret = Cryptpad.parseHash('/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI'); - return secret.version === 1 && - secret.mode === "edit" && - secret.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && - secret.key === "usn4+9CqVja8Q7RZOGTfRgqI" && - !secret.present; + assert(function (cb) { + var secret = Cryptpad.parsePadUrl('/pad/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI'); + return cb(secret.hashData.version === 1 && + secret.hashData.mode === "edit" && + secret.hashData.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && + secret.hashData.key === "usn4+9CqVja8Q7RZOGTfRgqI" && + !secret.hashData.present); + }, "version 1 hash (without present mode) failed to parse"); + + // test support for present mode in hashes + assert(function (cb) { + var secret = Cryptpad.parsePadUrl('/pad/#/1/edit/CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G/present'); + return cb(secret.hashData.version === 1 + && secret.hashData.mode === "edit" + && secret.hashData.channel === "CmN5+YJkrHFS3NSBg-P7Sg" + && secret.hashData.key === "DNZ2wcG683GscU4fyOyqA87G" + && secret.hashData.present); }, "version 1 hash failed to parse"); // test support for present mode in hashes - assert(function () { - var secret = Cryptpad.parseHash('/1/edit/CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G/present'); - return secret.version === 1 - && secret.mode === "edit" - && secret.channel === "CmN5+YJkrHFS3NSBg-P7Sg" - && secret.key === "DNZ2wcG683GscU4fyOyqA87G" - && secret.present; - }, "version 1 hash failed to parse"); + assert(function (cb) { + var secret = Cryptpad.parsePadUrl('/pad/#/1/edit//CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G//present'); + return cb(secret.hashData.version === 1 + && secret.hashData.mode === "edit" + && secret.hashData.channel === "CmN5+YJkrHFS3NSBg-P7Sg" + && secret.hashData.key === "DNZ2wcG683GscU4fyOyqA87G" + && secret.hashData.present); + }, "Couldn't handle multiple successive slashes"); // test support for trailing slash - assert(function () { - var secret = Cryptpad.parseHash('/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/'); - return secret.version === 1 && - secret.mode === "edit" && - secret.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && - secret.key === "usn4+9CqVja8Q7RZOGTfRgqI" && - !secret.present; + assert(function (cb) { + var secret = Cryptpad.parsePadUrl('/pad/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/'); + return cb(secret.hashData.version === 1 && + secret.hashData.mode === "edit" && + secret.hashData.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && + secret.hashData.key === "usn4+9CqVja8Q7RZOGTfRgqI" && + !secret.hashData.present); }, "test support for trailing slashes in version 1 hash failed to parse"); - assert(function () { + assert(function (cb) { // TODO - return true; + return cb(true); }, "version 2 hash failed to parse correctly"); var swap = function (str, dict) { @@ -194,7 +220,7 @@ define([ return str || ''; }; - var formatFailures = function () { + var formatFailures = function () { var template = multiline(function () { /*

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

{{passedAssertions}} / {{totalAssertions}} test{{plural}} passed. @@ -237,12 +262,19 @@ The test returned: {{previous}} */}), dict); - var report = SUCCESS; + var report = SUCCESS; - return report; + return report; + }); + + var $report = $('.report'); + $report.addClass(failed?'failure':'success'); + + if (failed) { + Test.failed(); + } else { + Test.passed(); + } }); - var $report = $('.report'); - $report.addClass(failed?'failure':'success'); - }); diff --git a/www/auth/main.js b/www/auth/main.js index 032f406ba..577e1e966 100644 --- a/www/auth/main.js +++ b/www/auth/main.js @@ -1,8 +1,9 @@ define([ 'jquery', '/common/cryptpad-common.js', + '/common/test.js', '/bower_components/tweetnacl/nacl-fast.min.js' -], function ($, Cryptpad) { +], function ($, Cryptpad, Test) { var Nacl = window.nacl; var signMsg = function (msg, privKey) { @@ -18,8 +19,16 @@ define([ /^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'); + Test(function () { + // This is only here to maybe trigger an error. + window.drive = Cryptpad.getStore().getProxy().proxy['drive']; + Test.passed(); + }); $(window).on("message", function (jqe) { var evt = jqe.originalEvent; var data = JSON.parse(evt.data); @@ -42,6 +51,11 @@ define([ 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"; } diff --git a/www/code/code.css b/www/code/code.css new file mode 100644 index 000000000..589544493 --- /dev/null +++ b/www/code/code.css @@ -0,0 +1,74 @@ +html, +body { + height: 100%; + width: 100%; + padding: 0px; + margin: 0px; + overflow: hidden; + box-sizing: border-box; + position: relative; +} +body { + display: flex; + flex-flow: column; + max-height: 100%; + min-height: auto; +} +.CodeMirror { + display: inline-block; + height: 100%; + width: 50%; + transition: width 500ms, min-width 500ms, max-width 500ms; + min-width: 20%; + max-width: 80%; + resize: horizontal; +} +.CodeMirror.fullPage { + min-width: 100%; + max-width: 100%; + resize: none; +} +.CodeMirror-focused .cm-matchhighlight { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==); + background-position: bottom; + background-repeat: repeat-x; +} +#editorContainer { + flex: 1; + display: flex; + flex-flow: row; + height: 100%; + overflow: hidden; +} +#previewContainer { + flex: 1; + padding: 5px 20px; + overflow: auto; + display: inline-block; + height: 100%; + border-left: 1px solid black; + box-sizing: border-box; + font-family: Calibri, Ubuntu, sans-serif; + word-wrap: break-word; +} +#preview { + max-width: 40vw; + margin: auto; +} +#preview table { + border-collapse: collapse; +} +#preview table tr th { + border: 3px solid black; + padding: 15px; +} +@media (max-width: 600px) { + .CodeMirror { + flex: 1; + max-width: 100%; + resize: none; + } + #previewContainer { + display: none !important; + } +} diff --git a/www/code/code.less b/www/code/code.less new file mode 100644 index 000000000..04e3ab615 --- /dev/null +++ b/www/code/code.less @@ -0,0 +1,84 @@ +@import "../../customize.dist/src/less/variables.less"; +@import "../../customize.dist/src/less/mixins.less"; + +html, body{ + height: 100%; + width: 100%; + padding: 0px; + margin: 0px; + overflow: hidden; + box-sizing: border-box; + position: relative; +} +body { + display: flex; + flex-flow: column; + max-height: 100%; + min-height: auto; +} + +@slideTime: 500ms; +.CodeMirror { + display: inline-block; + height: 100%; + width: 50%; + transition: width @slideTime, min-width @slideTime, max-width @slideTime; + min-width: 20%; + max-width: 80%; + resize: horizontal; +} +.CodeMirror.fullPage { + min-width: 100%; + max-width: 100%; + resize: none; +} +.CodeMirror-focused .cm-matchhighlight { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==); + background-position: bottom; + background-repeat: repeat-x; +} +#editorContainer { + flex: 1; + display: flex; + flex-flow: row; + height: 100%; + overflow: hidden; +} +#previewContainer { + flex: 1; + padding: 5px 20px; + overflow: auto; + display: inline-block; + height: 100%; + border-left: 1px solid black; + box-sizing: border-box; + font-family: Calibri,Ubuntu,sans-serif; + word-wrap: break-word; +} + +#preview { + max-width: 40vw; + margin: auto; + + table { + border-collapse: collapse; + tr { + th { + border: 3px solid black; + padding: 15px; + } + } + } +} + +@media (max-width: @media-medium-screen) { + .CodeMirror { + flex: 1; + max-width: 100%; + resize: none; + } + #previewContainer { + display: none !important; + } +} + diff --git a/www/code/index.html b/www/code/index.html index 448020d26..4fa11fc33 100644 --- a/www/code/index.html +++ b/www/code/index.html @@ -5,6 +5,7 @@ + + @@ -31,33 +32,13 @@ -
- +
+ +
+
diff --git a/www/code/main.js b/www/code/main.js index c382b38e4..5e9983fe6 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -8,28 +8,38 @@ define([ '/bower_components/chainpad-json-validator/json-ot.js', '/common/cryptpad-common.js', '/common/cryptget.js', - '/common/modes.js', - '/common/themes.js', - '/common/visible.js', - '/common/notify.js', - '/bower_components/file-saver/FileSaver.min.js' -], function ($, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT, Cryptpad, Cryptget, Modes, Themes, Visible, Notify) { - var saveAs = window.saveAs; + '/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; @@ -42,85 +52,22 @@ define([ }; 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); + $iframe.find('.CodeMirror').addClass('fullPage'); + editor = CodeMirror.editor; var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); - var parsedHash = Cryptpad.parsePadUrl(window.location.href); - var defaultName = Cryptpad.getDefaultName(parsedHash); var isHistoryMode = false; - var editor = module.editor = CMeditor.fromTextArea($textarea[0], { - lineNumbers: true, - lineWrapping: true, - autoCloseBrackets: true, - matchBrackets : true, - showTrailingSpace : true, - styleActiveLine : true, - search: true, - highlightSelectionMatches: {showToken: /\w+/}, - extraKeys: {"Shift-Ctrl-R": undefined}, - foldGutter: true, - gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], - mode: "javascript", - readOnly: true - }); - editor.setValue(Messages.codeInitialState); - - var setMode = module.setMode = function (mode, $select) { - module.highlightMode = mode; - if (mode === 'text') { - editor.setOption('mode', 'text'); - return; - } - CodeMirror.autoLoadMode(editor, mode); - editor.setOption('mode', mode); - if ($select) { - var name = $select.find('a[data-value="' + mode + '"]').text() || 'Mode'; - $select.setValue(name); - } - }; - - var setTheme = module.setTheme = (function () { - var path = '/common/theme/'; - - var $head = $(ifrw.document.head); - - var themeLoaded = module.themeLoaded = function (theme) { - return $head.find('link[href*="'+theme+'"]').length; - }; - - var loadTheme = module.loadTheme = function (theme) { - $head.append($('', { - rel: 'stylesheet', - href: path + theme + '.css', - })); - }; - - return function (theme, $select) { - if (!theme) { - editor.setOption('theme', 'default'); - } else { - if (!themeLoaded(theme)) { - loadTheme(theme); - } - editor.setOption('theme', theme); - } - if ($select) { - $select.setValue(theme || 'Theme'); - } - }; - }()); - - var setEditable = module.setEditable = function (bool) { + var setEditable = APP.setEditable = function (bool) { if (readOnly && bool) { return; } editor.setOption('readOnly', !bool); }; + var Title; var UserList; + var Metadata; var config = { initialState: '{}', @@ -144,11 +91,6 @@ define([ } }; -/* var isDefaultTitle = function () { - var parsed = Cryptpad.parsePadUrl(window.location.href); - return Cryptpad.isDefaultName(parsed, document.title); - };*/ - var initializing = true; var stringifyInner = function (textValue) { @@ -156,19 +98,31 @@ define([ content: textValue, metadata: { users: UserList.userData, - defaultTitle: defaultName + 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; } @@ -176,173 +130,52 @@ define([ 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 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 onModeChanged = function (mode) { + var $codeMirror = $iframe.find('.CodeMirror'); + if (mode === "markdown") { + APP.$previewButton.show(); + Cryptpad.getPadAttribute('previewMode', function (e, data) { + if (e) { return void console.error(e); } + if (data !== false) { + $previewContainer.show(); + $codeMirror.removeClass('fullPage'); + } }); - }; - 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; + return; } - - 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(); + APP.$previewButton.hide(); + $previewContainer.hide(); + $codeMirror.addClass('fullPage'); }; - var renameCb = function (err, title) { - if (err) { return; } - document.title = title; - onLocal(); - }; - - var updateTitle = function (newTitle) { - if (newTitle === document.title) { return; } - // Change the title now, and set it back to the old value if there is an error - var oldTitle = document.title; - document.title = newTitle; - Cryptpad.renamePad(newTitle, function (err, data) { - if (err) { - console.log("Couldn't set pad title"); - console.error(err); - document.title = oldTitle; - return; - } - document.title = data; - $bar.find('.' + Toolbar.constants.title).find('span.title').text(data); - $bar.find('.' + Toolbar.constants.title).find('input').val(data); - }); - }; - - var updateDefaultTitle = function (defaultTitle) { - defaultName = defaultTitle; - $bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName); - }; - - 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 - UserList.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); - } - }; - - config.onInit = function (info) { + config.onInit = function (info) { UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad); + var titleCfg = { getHeadingText: CodeMirror.getHeadingText }; + Title = Cryptpad.createTitle(titleCfg, config.onLocal, Cryptpad); + + Metadata = Cryptpad.createMetadata(UserList, Title); + var configTb = { - displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'], + displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], userList: UserList.getToolbarConfig(), share: { secret: secret, channel: info.channel }, - title: { - onRename: renameCb, - defaultName: defaultName, - suggestName: suggestName - }, + title: Title.getTitleConfig(), common: Cryptpad, readOnly: readOnly, ifrw: ifrw, @@ -350,7 +183,10 @@ define([ network: info.network, $container: $bar }; - toolbar = module.toolbar = Toolbar.create(configTb); + toolbar = APP.toolbar = Toolbar.create(configTb); + + Title.setToolbar(toolbar); + CodeMirror.init(config.onLocal, Title, toolbar); var $rightside = toolbar.$rightside; @@ -360,34 +196,17 @@ define([ } /* add a history button */ - var histConfig = {}; - histConfig.onRender = function (val) { - if (typeof val === "undefined") { return; } - try { - var hjson = JSON.parse(val || '{}'); - var remoteDoc = hjson.content; + var histConfig = { + onLocal: config.onLocal, + onRemote: config.onRemote, + setHistory: setHistory, + applyVal: function (val) { + var remoteDoc = JSON.parse(val || '{}').content; editor.setValue(remoteDoc || ''); editor.save(); - } catch (e) { - // Probably a parse error - console.error(e); - } + }, + $toolbar: $bar }; - histConfig.onClose = function () { - // Close button clicked - setHistory(false, true); - }; - histConfig.onRevert = function () { - // Revert button clicked - setHistory(false, false); - config.onLocal(); - config.onRemote(); - }; - histConfig.onReady = function () { - // Called when the history is loaded and the UI displayed - setHistory(true); - }; - histConfig.$toolbar = $bar; var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); $rightside.append($hist); @@ -396,24 +215,20 @@ define([ var templateObj = { rt: info.realtime, Crypt: Cryptget, - getTitle: function () { return document.title; } + getTitle: Title.getTitle }; var $templateButton = Cryptpad.createButton('template', true, templateObj); $rightside.append($templateButton); } /* add an export button */ - var $export = Cryptpad.createButton('export', true, {}, exportText); + var $export = Cryptpad.createButton('export', true, {}, CodeMirror.exportText); $rightside.append($export); if (!readOnly) { /* add an import button */ - var $import = Cryptpad.createButton('import', true, {}, importText); + var $import = Cryptpad.createButton('import', true, {}, CodeMirror.importText); $rightside.append($import); - - /* add a rename button */ - //var $setTitle = Cryptpad.createButton('rename', true, {suggestName: suggestName}, renameCb); - //$rightside.append($setTitle); } /* add a forget button */ @@ -424,108 +239,54 @@ define([ 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 $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')) { + forceDrawPreview(); + $codeMirror.removeClass('fullPage'); + Cryptpad.setPadAttribute('previewMode', true, function (e) { + if (e) { return console.log(e); } }); - }); - 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); - $block.find('a').click(function () { - 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 + } else { + $codeMirror.addClass('fullPage'); + Cryptpad.setPadAttribute('previewMode', false, function (e) { + if (e) { return console.log(e); } }); - }); - 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); - - setTheme(lastTheme, $block); - - $block.find('a').click(function () { - var theme = $(this).attr('data-value'); - setTheme(theme, $block); - localStorage.setItem(themeKey, theme); - }); - - $rightside.append($block); - }; + } + }); + $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); } }; - 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); - } - }; - config.onReady = function (info) { - if (module.realtime !== info.realtime) { - var realtime = module.realtime = info.realtime; - module.patchText = TextPatcher.create({ + 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; } @@ -543,32 +304,32 @@ 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 (Cryptpad.initialName && Title.isDefaultTitle()) { + Title.updateTitle(Cryptpad.initialName); } - if (Visible.isSupported()) { - Visible.onChange(function (yes) { - if (yes) { unnotify(); } - }); - } + Cryptpad.getPadAttribute('previewMode', function (e, data) { + if (e) { return void console.error(e); } + if (data === false && APP.$previewButton) { + APP.$previewButton.click(); + } + }); Cryptpad.removeLoadingScreen(); setEditable(true); @@ -576,90 +337,44 @@ define([ onLocal(); // push local state to avoid parse errors later. - if (readOnly) { return; } - UserList.getLastName(toolbar.$userNameButton, isNew); - }; - - 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; - }; - - 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; + UserList.getLastName(toolbar.$userNameButton, isNew); }; config.onRemote = function () { if (initializing) { return; } if (isHistoryMode) { return; } - var scroll = editor.getScrollInfo(); - 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(); } }; config.onAbort = function () { @@ -683,7 +398,7 @@ define([ config.onError = onConnectError; - module.realtime = Realtime.start(config); + APP.realtime = Realtime.start(config); editor.on('change', onLocal); diff --git a/www/common/boot2.js b/www/common/boot2.js index 599875a93..76ce9bcf9 100644 --- a/www/common/boot2.js +++ b/www/common/boot2.js @@ -7,8 +7,18 @@ define([], function () { // 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" + "json.sortify": "/bower_components/json.sortify/dist/JSON.sortify", + "pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf", + "pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker" } }); + + // 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/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 index d4c2bb112..5de76ab99 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -1,8 +1,9 @@ define([ '/common/common-util.js', + '/common/common-interface.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/tweetnacl/nacl-fast.min.js' -], function (Util, Crypto) { +], function (Util, UI, Crypto) { var Nacl = window.nacl; var Hash = {}; @@ -32,9 +33,68 @@ define([ return '/1/view/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.viewKeyStr)+'/'; }; var getFileHashFromKeys = Hash.getFileHashFromKeys = function (fileKey, cryptKey) { - return '/2/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(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; @@ -43,19 +103,24 @@ define([ if (!href) { return ret; } if (href.slice(-1) !== '/') { href += '/'; } + var idx; + if (!/^https*:\/\//.test(href)) { - var idx = href.indexOf('/#'); + idx = href.indexOf('/#'); ret.type = href.slice(1, idx); ret.hash = href.slice(idx + 2); + ret.hashData = parseTypeHash(ret.type, ret.hash); return ret; } - var hash = href.replace(patt, function (a, domain, type) { + href.replace(patt, function (a, domain, type) { ret.domain = domain; ret.type = type; return ''; }); - ret.hash = hash.replace(/#/g, ''); + idx = href.indexOf('/#'); + ret.hash = href.slice(idx + 2); + ret.hashData = parseTypeHash(ret.type, ret.hash); return ret; }; @@ -71,7 +136,7 @@ define([ * - 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 (secretHash) { + Hash.getSecrets = function (type, secretHash) { var secret = {}; var generate = function () { secret.keys = Crypto.createEditCryptor(); @@ -81,50 +146,56 @@ define([ generate(); return secret; } else { - var hash = secretHash || window.location.hash.slice(1); + 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 (hash.slice(0,1) !== '/' && hash.length >= 56) { + if (parsed.version === 0) { // Old hash - secret.channel = hash.slice(0, 32); - secret.key = hash.slice(32); + secret.channel = parsed.channel; + secret.key = parsed.key; } - else { + else if (parsed.version === 1) { // New hash - var hashArray = hash.split('/'); - if (hashArray.length < 4) { - Hash.alert("Unable to parse the key"); - throw new Error("Unable to parse the key"); - } - var version = hashArray[1]; - if (version === "1") { - var mode = hashArray[2]; - if (mode === 'edit') { - secret.channel = base64ToHex(hashArray[3]); - var keys = Crypto.createEditCryptor(hashArray[4].replace(/-/g, '/')); - secret.keys = keys; - secret.key = keys.editKeyStr; + 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) { - Hash.alert("The channel key and/or the encryption key is invalid"); + 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 (mode === 'view') { - secret.channel = base64ToHex(hashArray[3]); - secret.keys = Crypto.createViewCryptor(hashArray[4].replace(/-/g, '/')); + else if (parsed.mode === 'view') { + secret.keys = Crypto.createViewCryptor(parsed.key); if (secret.channel.length !== 32) { - Hash.alert("The channel key is invalid"); + UI.alert("The channel key is invalid"); throw new Error("The channel key is invalid"); } } - } else if (version === "2") { + } else if (parsed.type === "file") { // version 2 hashes are to be used for encrypted blobs - secret.channel = hashArray[2].replace(/-/g, '/'); - secret.keys = { fileKeyStr: hashArray[3].replace(/-/g, '/') }; + 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)"); } } } @@ -161,42 +232,6 @@ define([ return '/1/edit/' + [channelId, key].join('/') + '/'; }; -/* -Version 0 - /pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy -Version 1 - /code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI -Version 2 - /file/#/2/// - /file/#/2/K6xWU-LT9BJHCQcDCT-DcQ/ajExFODrFH4lVBwxxsrOKw/image-png -*/ - var parseHash = Hash.parseHash = function (hash) { - var parsed = {}; - if (hash.slice(0,1) !== '/' && hash.length >= 56) { - // Old hash - parsed.channel = hash.slice(0, 32); - parsed.key = hash.slice(32); - parsed.version = 0; - return parsed; - } - var hashArr = hash.split('/'); - if (hashArr[1] && hashArr[1] === '1') { - parsed.version = 1; - parsed.mode = hashArr[2]; - parsed.channel = hashArr[3]; - parsed.key = hashArr[4]; - parsed.present = typeof(hashArr[5]) === "string" && hashArr[5] === 'present'; - return parsed; - } - if (hashArr[1] && hashArr[1] === '2') { - parsed.version = 2; - parsed.channel = hashArr[2].replace(/-/g, '/'); - parsed.key = hashArr[3].replace(/-/g, '/'); - return parsed; - } - return; - }; - // STORAGE Hash.findWeaker = function (href, recents) { var rHref = href || getRelativeHref(window.location.href); @@ -207,9 +242,13 @@ Version 2 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 = parseHash(p.hash); - var parsedHash = parseHash(parsed.hash); + 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') { @@ -229,9 +268,13 @@ Version 2 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 = parseHash(p.hash); - var parsedHash = parseHash(parsed.hash); + 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') { @@ -250,8 +293,7 @@ Version 2 var parsed = Hash.parsePadUrl(href); if (!parsed || !parsed.hash) { return; } - parsed = Hash.parseHash(parsed.hash); - + parsed = parsed.hashData; if (parsed.version === 0) { return parsed.channel; } else if (parsed.version !== 1 && parsed.version !== 2) { diff --git a/www/common/common-history.js b/www/common/common-history.js index 1cd0e6555..48b210eb3 100644 --- a/www/common/common-history.js +++ b/www/common/common-history.js @@ -24,7 +24,6 @@ define([ var wcId = common.hrefToHexChannelId(config.href || window.location.href); - console.log(wcId); var createRealtime = function () { return ChainPad.create({ userName: 'history', @@ -36,8 +35,8 @@ define([ }; var realtime = createRealtime(); - var hash = config.href ? common.parsePadUrl(config.href).hash : undefined; - var secret = common.getSecrets(hash); + 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 () { @@ -80,11 +79,32 @@ define([ if (History.loading) { return void console.error("History is already being loaded..."); } History.loading = true; var $toolbar = config.$toolbar; - var noFunc = function () {}; - var render = config.onRender || noFunc; - var onClose = config.onClose || noFunc; - var onRevert = config.onRevert || noFunc; - var onReady = config.onReady || noFunc; + + if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) { + throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory"); + } + + // config.setHistory(bool, bool) + // - bool1: history value + // - bool2: reset old content? + var render = function (val) { + if (typeof val === "undefined") { return; } + try { + config.applyVal(val); + } catch (e) { + // Probably a parse error + console.error(e); + } + }; + var onClose = function () { config.setHistory(false, true); }; + var onRevert = function () { + config.setHistory(false, false); + config.onLocal(); + config.onRemote(); + }; + var onReady = function () { + config.setHistory(true); + }; var Messages = common.Messages; diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 979ffcf67..5d1e01cc8 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -3,8 +3,10 @@ define([ '/customize/messages.js', '/common/common-util.js', '/customize/application_config.js', - '/bower_components/alertifyjs/dist/js/alertify.js' -], function ($, Messages, Util, AppConfig, Alertify) { + '/bower_components/alertifyjs/dist/js/alertify.js', + '/common/notify.js', + '/common/visible.js' +], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible) { var UI = {}; @@ -141,7 +143,7 @@ define([ return { show: function () { - $target.show(); + $target.css('display', 'inline'); return this; }, hide: function () { @@ -192,10 +194,17 @@ define([ }; UI.removeLoadingScreen = function (cb) { $('#' + LOADING).fadeOut(750, cb); - $('#loadingTip').css('top', ''); - window.setTimeout(function () { - $('#loadingTip').fadeOut(750); - }, 3000); + var $tip = $('#loadingTip').css('top', '') + // loading.less sets transition-delay: $wait-time + // and transition: opacity $fadeout-time + .css({ + 'opacity': 0, + 'pointer-events': 'none', + }); + setTimeout(function () { + $tip.remove(); + }, 3750); + // jquery.fadeout can get stuck }; UI.errorLoadingScreen = function (error, transparent) { if (!$('#' + LOADING).is(':visible')) { UI.addLoadingScreen(undefined, true); } @@ -204,6 +213,28 @@ define([ $('#' + LOADING).find('p').html(error || Messages.error); }; + // Notify + var notify = {}; + UI.unnotify = function () { + if (notify.tabNotification && + typeof(notify.tabNotification.cancel) === 'function') { + notify.tabNotification.cancel(); + } + }; + + UI.notify = function () { + if (Visible.isSupported() && !Visible.currently()) { + UI.unnotify(); + notify.tabNotification = Notify.tab(1000, 10); + } + }; + + if (Visible.isSupported()) { + Visible.onChange(function (yes) { + if (yes) { UI.unnotify(); } + }); + } + UI.importContent = function (type, f) { return function () { var $files = $('').click(); diff --git a/www/common/common-metadata.js b/www/common/common-metadata.js new file mode 100644 index 000000000..d5487901c --- /dev/null +++ b/www/common/common-metadata.js @@ -0,0 +1,51 @@ +define(function () { + var module = {}; + + module.create = function (UserList, Title, cfg) { + var exp = {}; + + exp.update = function (shjson) { + // Extract the user list (metadata) from the hyperjson + var json = (!shjson || typeof shjson !== "string") ? "" : JSON.parse(shjson); + var titleUpdated = false; + var metadata; + if (Array.isArray(json)) { + metadata = json[3] && json[3].metadata; + } else { + metadata = json.metadata; + } + if (typeof metadata === "object") { + if (metadata.users) { + var userData = metadata.users; + // Update the local user data + UserList.addToUserData(userData); + } + if (metadata.defaultTitle) { + Title.updateDefaultTitle(metadata.defaultTitle); + } + if (typeof metadata.title !== "undefined") { + Title.updateTitle(metadata.title || Title.defaultTitle); + titleUpdated = true; + } + if (metadata.slideOptions && cfg.slideOptions) { + cfg.slideOptions(metadata.slideOptions); + } + if (metadata.color && cfg.slideColors) { + cfg.slideColors(metadata.color, metadata.backColor); + } + if (typeof(metadata.palette) !== 'undefined' && cfg.updatePalette) { + cfg.updatePalette(metadata.palette); + } + } + if (!titleUpdated) { + Title.updateTitle(Title.defaultTitle); + } + }; + + return exp; + }; + + return module; +}); + + diff --git a/www/common/common-title.js b/www/common/common-title.js new file mode 100644 index 000000000..f5485d86d --- /dev/null +++ b/www/common/common-title.js @@ -0,0 +1,84 @@ +define(function () { + var module = {}; + + module.create = function (cfg, onLocal, Cryptpad) { + var exp = {}; + + var parsed = exp.parsedHref = Cryptpad.parsePadUrl(window.location.href); + exp.defaultTitle = Cryptpad.getDefaultName(parsed); + + exp.title = document.title; // TOOD slides + + cfg = cfg || {}; + + var getHeadingText = cfg.getHeadingText || function () { return; }; + var updateLocalTitle = function (newTitle) { + exp.title = newTitle; + if (typeof cfg.updateLocalTitle === "function") { + cfg.updateLocalTitle(newTitle); + } else { + document.title = newTitle; + } + }; + + var $title; + exp.setToolbar = function (toolbar) { + $title = toolbar && toolbar.title; + }; + + exp.getTitle = function () { return exp.title; }; + var isDefaultTitle = exp.isDefaultTitle = function (){return exp.title === exp.defaultTitle;}; + + var suggestTitle = exp.suggestTitle = function (fallback) { + if (isDefaultTitle()) { + return getHeadingText() || fallback || ""; + } else { + return exp.title || getHeadingText() || exp.defaultTitle; + } + }; + + var renameCb = function (err, newTitle) { + if (err) { return; } + updateLocalTitle(newTitle); + console.log('here'); + onLocal(); + }; + + exp.updateTitle = function (newTitle) { + if (newTitle === exp.title) { return; } + // Change the title now, and set it back to the old value if there is an error + var oldTitle = exp.title; + Cryptpad.renamePad(newTitle, function (err, data) { + if (err) { + console.log("Couldn't set pad title"); + console.error(err); + updateLocalTitle(oldTitle); + return; + } + updateLocalTitle(data); + if (!$title) { return; } + $title.find('span.title').text(data); + $title.find('input').val(data); + }); + }; + + exp.updateDefaultTitle = function (newDefaultTitle) { + exp.defaultTitle = newDefaultTitle; + if (!$title) { return; } + $title.find('input').attr("placeholder", exp.defaultTitle); + }; + + exp.getTitleConfig = function () { + return { + onRename: renameCb, + suggestName: suggestTitle, + defaultName: exp.defaultTitle + }; + }; + + return exp; + }; + + return module; +}); + diff --git a/www/common/common-util.js b/www/common/common-util.js index 2542e0a22..0d0d4c776 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -21,7 +21,7 @@ define([], function () { .replace(/ +$/, "") .split(" "); var byteString = String.fromCharCode.apply(null, hexArray); - return window.btoa(byteString).replace(/\//g, '-').slice(0,-2); + return window.btoa(byteString).replace(/\//g, '-').replace(/=+$/, ''); }; Util.base64ToHex = function (b64String) { @@ -81,12 +81,58 @@ define([], function () { .replace(/_+/g, '_'); }; + var oneKilobyte = 1024; + var oneMegabyte = 1024 * oneKilobyte; + var oneGigabyte = 1024 * oneMegabyte; + + Util.bytesToGigabytes = function (bytes) { + return Math.ceil(bytes / oneGigabyte * 100) / 100; + }; + Util.bytesToMegabytes = function (bytes) { - return Math.floor((bytes / (1024 * 1024) * 100)) / 100; + return Math.ceil(bytes / oneMegabyte * 100) / 100; }; Util.bytesToKilobytes = function (bytes) { - return Math.floor(bytes / 1024 * 100) / 100; + return Math.ceil(bytes / oneKilobyte * 100) / 100; + }; + + Util.magnitudeOfBytes = function (bytes) { + if (bytes >= oneGigabyte) { return 'GB'; } + else if (bytes >= oneMegabyte) { return 'MB'; } + }; + + Util.fetch = function (src, cb) { + var done = false; + var CB = function (err, res) { + if (done) { return; } + done = true; + cb(err, res); + }; + + var xhr = new XMLHttpRequest(); + xhr.open("GET", src, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function () { + if (/^4/.test(''+this.status)) { + return CB('XHR_ERROR'); + } + return void CB(void 0, new Uint8Array(xhr.response)); + }; + xhr.send(null); + }; + + Util.throttle = function (f, ms) { + var to; + var g = function () { + window.clearTimeout(to); + to = window.setTimeout(f, ms); + }; + return g; + }; + + Util.createRandomInteger = function () { + return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); }; return Util; diff --git a/www/common/cryptget.js b/www/common/cryptget.js index 382ab023c..cde78d298 100644 --- a/www/common/cryptget.js +++ b/www/common/cryptget.js @@ -22,7 +22,8 @@ define([ }; var makeConfig = function (hash) { - var secret = Cryptpad.getSecrets(hash); + // We can't use cryptget with a file or a user so we can use 'pad' as hash type + var secret = Cryptpad.getSecrets('pad', hash); if (!secret.keys) { secret.keys = secret.key; } // support old hashses var config = { websocketURL: Cryptpad.getWebsocketURL(), diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index b58209029..86ff53c14 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -8,11 +8,14 @@ define([ '/common/common-interface.js', '/common/common-history.js', '/common/common-userlist.js', + '/common/common-title.js', + '/common/common-metadata.js', + '/common/common-codemirror.js', '/common/clipboard.js', '/common/pinpad.js', '/customize/application_config.js' -], function ($, Config, Messages, Store, Util, Hash, UI, History, UserList, Clipboard, Pinpad, AppConfig) { +], function ($, Config, Messages, Store, Util, Hash, UI, History, UserList, Title, Metadata, CodeMirror, Clipboard, Pinpad, AppConfig) { /* This file exposes functionality which is specific to Cryptpad, but not to any particular pad type. This includes functions for committing metadata @@ -20,10 +23,12 @@ define([ Additionally, there is some basic functionality for import/export. */ - var common = window.Cryptpad = { Messages: Messages, - Clipboard: Clipboard + Clipboard: Clipboard, + donateURL: 'https://accounts.cryptpad.fr/#/donate?on=' + window.location.hostname, + upgradeURL: 'https://accounts.cryptpad.fr/#/?on=' + window.location.hostname, + account: {}, }; // constants @@ -53,6 +58,8 @@ define([ common.addLoadingScreen = UI.addLoadingScreen; common.removeLoadingScreen = UI.removeLoadingScreen; common.errorLoadingScreen = UI.errorLoadingScreen; + common.notify = UI.notify; + common.unnotify = UI.unnotify; // import common utilities for export common.find = Util.find; @@ -66,18 +73,23 @@ define([ common.fixFileName = Util.fixFileName; common.bytesToMegabytes = Util.bytesToMegabytes; common.bytesToKilobytes = Util.bytesToKilobytes; + common.fetch = Util.fetch; + common.throttle = Util.throttle; + common.createRandomInteger = Util.createRandomInteger; // import hash utilities for export var createRandomHash = common.createRandomHash = Hash.createRandomHash; + common.parseTypeHash = Hash.parseTypeHash; var parsePadUrl = common.parsePadUrl = Hash.parsePadUrl; var isNotStrongestStored = common.isNotStrongestStored = Hash.isNotStrongestStored; var hrefToHexChannelId = common.hrefToHexChannelId = Hash.hrefToHexChannelId; - var parseHash = common.parseHash = Hash.parseHash; var getRelativeHref = common.getRelativeHref = Hash.getRelativeHref; common.getBlobPathFromHex = Hash.getBlobPathFromHex; common.getEditHashFromKeys = Hash.getEditHashFromKeys; common.getViewHashFromKeys = Hash.getViewHashFromKeys; + common.getFileHashFromKeys = Hash.getFileHashFromKeys; + common.getUserHrefFromKeys = Hash.getUserHrefFromKeys; common.getSecrets = Hash.getSecrets; common.getHashes = Hash.getHashes; common.createChannelId = Hash.createChannelId; @@ -88,6 +100,15 @@ define([ // Userlist common.createUserList = UserList.create; + // Title + common.createTitle = Title.create; + + // Metadata + common.createMetadata = Metadata.create; + + // CodeMirror + common.createCodemirror = CodeMirror.create; + // History common.getHistory = function (config) { return History.create(common, config); }; @@ -197,6 +218,7 @@ define([ userNameKey, userHashKey, 'loginToken', + 'plan', ].forEach(function (k) { sessionStorage.removeItem(k); localStorage.removeItem(k); @@ -225,6 +247,11 @@ define([ var getUserHash = common.getUserHash = function () { var hash = localStorage[userHashKey]; + if (['undefined', 'undefined/'].indexOf(hash) !== -1) { + localStorage.removeItem(userHashKey); + return; + } + if (hash) { var sHash = common.serializeHash(hash); if (sHash !== hash) { localStorage[userHashKey] = sHash; } @@ -270,27 +297,22 @@ define([ if (!pad.title) { pad.title = common.getDefaultname(parsed); } - return parsed.hash; + return parsed.hashData; }; // Migrate from legacy store (localStorage) var migrateRecentPads = common.migrateRecentPads = function (pads) { return pads.map(function (pad) { - var hash; + var parsedHash; if (Array.isArray(pad)) { // TODO DEPRECATE_F - var href = pad[0]; - href.replace(/\#(.*)$/, function (a, h) { - hash = h; - }); - return { href: pad[0], atime: pad[1], - title: pad[2] || hash && hash.slice(0,8), + title: pad[2] || '', ctime: pad[1], }; } else if (pad && typeof(pad) === 'object') { - hash = checkObjectData(pad); - if (!hash || !common.parseHash(hash)) { return; } + parsedHash = checkObjectData(pad); + if (!parsedHash || !parsedHash.type) { return; } return pad; } else { console.error("[Cryptpad.migrateRecentPads] pad had unexpected value"); @@ -303,8 +325,8 @@ define([ var checkRecentPads = common.checkRecentPads = function (pads) { pads.forEach(function (pad, i) { if (pad && typeof(pad) === 'object') { - var hash = checkObjectData(pad); - if (!hash || !common.parseHash(hash)) { + var parsedHash = checkObjectData(pad); + if (!parsedHash || !parsedHash.type) { console.error("[Cryptpad.checkRecentPads] pad had unexpected value", pad); getStore().removeData(i); return; @@ -434,6 +456,7 @@ define([ Crypt.put(p.hash, val, function () { common.findOKButton().click(); common.removeLoadingScreen(); + common.feedback('TEMPLATE_USED'); }); }); }).appendTo($p); @@ -522,6 +545,7 @@ define([ common.setPadTitle = function (name, cb) { var href = window.location.href; var parsed = parsePadUrl(href); + if (!parsed.hash) { return; } href = getRelativeHref(href); // getRecentPads return the array from the drive, not a copy // We don't have to call "set..." at the end, everything is stored with listmap @@ -542,8 +566,8 @@ define([ // Version 1 : we have up to 4 differents hash for 1 pad, keep the strongest : // Edit > Edit (present) > View > View (present) - var pHash = parseHash(p.hash); - var parsedHash = parseHash(parsed.hash); + var pHash = p.hashData; + var parsedHash = parsed.hashData; if (!pHash) { return; } // We may have a corrupted pad in our storage, abort here in that case @@ -584,7 +608,7 @@ define([ var data = makePad(href, name); getStore().pushData(data, function (e) { if (e) { - if (e === 'E_OVER_LIMIT' && AppConfig.enablePinLimit) { + if (e === 'E_OVER_LIMIT') { common.alert(Messages.pinLimitNotPinned, null, true); return; } @@ -645,7 +669,8 @@ define([ var userHash = localStorage && localStorage.User_hash; if (!userHash) { return null; } - var userChannel = common.parseHash(userHash).channel; + var userParsedHash = common.parseTypeHash('drive', userHash); + var userChannel = userParsedHash && userParsedHash.channel; if (!userChannel) { return null; } var list = fo.getFiles([fo.FILES_DATA]).map(hrefToHexChannelId) @@ -728,29 +753,119 @@ define([ }); }; + common.updatePinLimit = function (cb) { + if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); } + rpc.updatePinLimits(function (e, limit, plan, note) { + if (e) { return cb(e); } + cb(e, limit, plan, note); + }); + }; + common.getPinLimit = function (cb) { - cb(void 0, typeof(AppConfig.pinLimit) === 'number'? AppConfig.pinLimit: 1000); + if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); } + rpc.getLimit(function (e, limit, plan, note) { + if (e) { return cb(e); } + cb(void 0, limit, plan, note); + }); }; common.isOverPinLimit = function (cb) { - if (!common.isLoggedIn() || !AppConfig.enablePinLimit) { return void cb(null, false); } + if (!common.isLoggedIn()) { return void cb(null, false); } var usage; - var andThen = function (e, limit) { + var andThen = function (e, limit, plan) { if (e) { return void cb(e); } - var data = {usage: usage, limit: limit}; + var data = {usage: usage, limit: limit, plan: plan}; if (usage > limit) { return void cb (null, true, data); } return void cb (null, false, data); }; var todo = function (e, used) { - usage = common.bytesToMegabytes(used); + usage = used; //common.bytesToMegabytes(used); if (e) { return void cb(e); } common.getPinLimit(andThen); }; common.getPinnedUsage(todo); }; + common.uploadComplete = function (cb) { + if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); } + rpc.uploadComplete(cb); + }; + + common.uploadStatus = function (size, cb) { + if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); } + rpc.uploadStatus(size, cb); + }; + + common.uploadCancel = function (cb) { + if (!pinsReady()) { return void cb('[RPC_NOT_READY]'); } + rpc.uploadCancel(cb); + }; + + var LIMIT_REFRESH_RATE = 30000; // milliseconds + common.createUsageBar = function (cb, alwaysDisplayUpgrade) { + var todo = function (err, state, data) { + var $container = $('', {'class':'limit-container'}); + if (!data) { + return void window.setTimeout(function () { + common.isOverPinLimit(todo); + }, LIMIT_REFRESH_RATE); + } + + var unit = Util.magnitudeOfBytes(data.limit); + + var usage = unit === 'GB'? Util.bytesToGigabytes(data.usage): + Util.bytesToMegabytes(data.usage); + var limit = unit === 'GB'? Util.bytesToGigabytes(data.limit): + Util.bytesToMegabytes(data.limit); + + var $limit = $('', {'class': 'cryptpad-limit-bar'}).appendTo($container); + var quota = usage/limit; + var width = Math.floor(Math.min(quota, 1)*200); // the bar is 200px width + var $usage = $('', {'class': 'usage'}).css('width', width+'px'); + + if (Config.noSubscriptionButton !== true && + (quota >= 0.8 || alwaysDisplayUpgrade) && + data.plan !== "power") + { + var origin = encodeURIComponent(window.location.hostname); + var $upgradeLink = $('', { + href: "https://accounts.cryptpad.fr/#!on=" + origin, + rel: "noreferrer noopener", + target: "_blank", + }).appendTo($container); + $(' +
+

+ +
diff --git a/www/login/main.js b/www/login/main.js index 973cd985f..caa77fe4b 100644 --- a/www/login/main.js +++ b/www/login/main.js @@ -13,6 +13,14 @@ define([ $sel.find('button').addClass('btn').addClass('btn-secondary'); $sel.show(); + // User admin menu + var $userMenu = $('#user-menu'); + var userMenuCfg = { + $initBlock: $userMenu + }; + var $userAdmin = Cryptpad.createUserAdminMenu(userMenuCfg); + $userAdmin.find('button').addClass('btn').addClass('btn-secondary'); + $(window).click(function () { $('.cryptpad-dropdown').hide(); }); @@ -57,65 +65,79 @@ define([ }); $('button.login').click(function () { - Cryptpad.addLoadingScreen(Messages.login_hashing); - // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password + // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up window.setTimeout(function () { - loginReady(function () { - var uname = $uname.val(); - var passwd = $passwd.val(); - Login.loginOrRegister(uname, passwd, false, function (err, result) { - if (!err) { - var proxy = result.proxy; + Cryptpad.addLoadingScreen(Messages.login_hashing); + // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password + window.setTimeout(function () { + loginReady(function () { + var uname = $uname.val(); + var passwd = $passwd.val(); + Login.loginOrRegister(uname, passwd, false, function (err, result) { + if (!err) { + var proxy = result.proxy; - // successful validation and user already exists - // set user hash in localStorage and redirect to drive - if (!proxy.login_name) { - result.proxy.login_name = result.userName; - } + // successful validation and user already exists + // set user hash in localStorage and redirect to drive + if (!proxy.login_name) { + result.proxy.login_name = result.userName; + } - proxy.edPrivate = result.edPrivate; - proxy.edPublic = result.edPublic; + proxy.edPrivate = result.edPrivate; + proxy.edPublic = result.edPublic; - Cryptpad.feedback('LOGIN', true); - Cryptpad.whenRealtimeSyncs(result.realtime, function() { - Cryptpad.login(result.userHash, result.userName, function () { - if (sessionStorage.redirectTo) { - var h = sessionStorage.redirectTo; - var parser = document.createElement('a'); - parser.href = h; - if (parser.origin === window.location.origin) { - delete sessionStorage.redirectTo; - window.location.href = h; - return; + Cryptpad.feedback('LOGIN', true); + Cryptpad.whenRealtimeSyncs(result.realtime, function() { + Cryptpad.login(result.userHash, result.userName, function () { + if (sessionStorage.redirectTo) { + var h = sessionStorage.redirectTo; + var parser = document.createElement('a'); + parser.href = h; + if (parser.origin === window.location.origin) { + delete sessionStorage.redirectTo; + window.location.href = h; + return; + } } - } - window.location.href = '/drive/'; + window.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); - }); - 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); + }); + $('#register').on('click', function () { + if (sessionStorage) { + if ($uname.val()) { + sessionStorage.login_user = $uname.val(); + } + if ($passwd.val()) { + sessionStorage.login_pass = $passwd.val(); + } + } + window.location.href = '/register/'; }); }); }); diff --git a/www/media/inner.html b/www/media/inner.html index bc5b96ae0..46e5cea4a 100644 --- a/www/media/inner.html +++ b/www/media/inner.html @@ -16,6 +16,8 @@ } media-tag * { max-width: 100%; + margin: auto; + display: block; } diff --git a/www/media/main.js b/www/media/main.js index 9e8127a62..bc861d699 100644 --- a/www/media/main.js +++ b/www/media/main.js @@ -6,6 +6,8 @@ define([ '/common/cryptpad-common.js', //'/common/visible.js', //'/common/notify.js', + 'pdfjs-dist/build/pdf', + 'pdfjs-dist/build/pdf.worker', '/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/file-saver/FileSaver.min.js', ], function ($, Crypto, realtimeInput, Toolbar, Cryptpad /*, Visible, Notify*/) { @@ -28,7 +30,7 @@ define([ var cryptKey = secret.keys && secret.keys.fileKeyStr; var fileId = secret.channel; var hexFileName = Cryptpad.base64ToHex(fileId); - var type = "image/png"; + // var type = "image/png"; var parsed = Cryptpad.parsePadUrl(window.location.href); var defaultName = Cryptpad.getDefaultName(parsed); @@ -41,16 +43,9 @@ define([ }; var updateTitle = function (newTitle) { - Cryptpad.renamePad(newTitle, function (err, data) { - if (err) { - console.log("Couldn't set pad title"); - console.error(err); - return; - } - document.title = newTitle; - $bar.find('.' + Toolbar.constants.title).find('span.title').text(data); - $bar.find('.' + Toolbar.constants.title).find('input').val(data); - }); + var title = document.title = newTitle; + $bar.find('.' + Toolbar.constants.title).find('span.title').text(title); + $bar.find('.' + Toolbar.constants.title).find('input').val(title); }; var suggestName = function () { @@ -64,13 +59,27 @@ define([ var $mt = $iframe.find('#encryptedFile'); $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); $mt.attr('data-crypto-key', 'cryptpad:'+cryptKey); - $mt.attr('data-type', type); + // $mt.attr('data-type', type); - window.onMediaMetadata = function (metadata) { + $(window.document).on('decryption', function (e) { + var decrypted = e.originalEvent; + var metadata = decrypted.metadata; + + if (decrypted.callback) { decrypted.callback(); } + //console.log(metadata); + //console.log(defaultName); if (!metadata || metadata.name !== defaultName) { return; } var title = document.title = metadata.name; updateTitle(title || defaultName); - }; + }) + .on('decryptionError', function (e) { + var error = e.originalEvent; + Cryptpad.alert(error.message); + }) + .on('decryptionProgress', function (e) { + var progress = e.originalEvent; + console.log(progress.percent); + }); require(['/common/media-tag.js'], function (MediaTag) { var configTb = { @@ -91,6 +100,30 @@ define([ updateTitle(Cryptpad.initialName || getTitle() || defaultName); + /** + * Allowed mime types that have to be set for a rendering after a decryption. + * + * @type {Array} + */ + var allowedMediaTypes = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'audio/mp3', + 'audio/ogg', + 'audio/wav', + 'audio/webm', + 'video/mp4', + 'video/ogg', + 'video/webm', + 'application/pdf', + 'application/dash+xml', + 'download' + ]; + + MediaTag.CryptoFilter.setAllowedMediaTypes(allowedMediaTypes); + MediaTag($mt[0]); Cryptpad.removeLoadingScreen(); diff --git a/www/pad/index.html b/www/pad/index.html index 4688001f6..3095c137f 100644 --- a/www/pad/index.html +++ b/www/pad/index.html @@ -4,6 +4,7 @@ CryptPad + + About diff --git a/www/register/main.js b/www/register/main.js index 6e97e2a93..bdaca5197 100644 --- a/www/register/main.js +++ b/www/register/main.js @@ -2,8 +2,9 @@ define([ 'jquery', '/common/login.js', '/common/cryptpad-common.js', - '/common/credential.js' // preloaded for login.js -], function ($, Login, Cryptpad) { + '/common/test.js', + '/common/credential.js', // preloaded for login.js +], function ($, Login, Cryptpad, Test) { var Messages = Cryptpad.Messages; $(function () { @@ -15,6 +16,14 @@ define([ $sel.find('button').addClass('btn').addClass('btn-secondary'); $sel.show(); + // User admin menu + var $userMenu = $('#user-menu'); + var userMenuCfg = { + $initBlock: $userMenu + }; + var $userAdmin = Cryptpad.createUserAdminMenu(userMenuCfg); + $userAdmin.find('button').addClass('btn').addClass('btn-secondary'); + $(window).click(function () { $('.cryptpad-dropdown').hide(); }); @@ -55,6 +64,11 @@ define([ var $register = $('button#register'); var logMeIn = function (result) { + if (Test.testing) { + Test.passed(); + window.alert("Test passed!"); + return; + } localStorage.User_hash = result.userHash; var proxy = result.proxy; @@ -101,57 +115,66 @@ define([ function (yes) { if (!yes) { return; } - Cryptpad.addLoadingScreen(Messages.login_hashing); - Login.loginOrRegister(uname, passwd, true, function (err, result) { - var proxy = result.proxy; + // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up + window.setTimeout(function () { + Cryptpad.addLoadingScreen(Messages.login_hashing); + // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password + window.setTimeout(function () { + Login.loginOrRegister(uname, passwd, true, function (err, result) { + var proxy = result.proxy; - if (err) { - switch (err) { - case 'NO_SUCH_USER': - Cryptpad.removeLoadingScreen(function () { - Cryptpad.alert(Messages.login_noSuchUser); - }); - break; - case 'INVAL_USER': - Cryptpad.removeLoadingScreen(function () { - Cryptpad.alert(Messages.login_invalUser); - }); - break; - case 'INVAL_PASS': - Cryptpad.removeLoadingScreen(function () { - Cryptpad.alert(Messages.login_invalPass); - }); - break; - case 'ALREADY_REGISTERED': - Cryptpad.removeLoadingScreen(function () { - Cryptpad.confirm(Messages.register_alreadyRegistered, function (yes) { - if (!yes) { return; } - proxy.login_name = uname; + if (err) { + switch (err) { + case 'NO_SUCH_USER': + Cryptpad.removeLoadingScreen(function () { + Cryptpad.alert(Messages.login_noSuchUser); + }); + break; + case 'INVAL_USER': + Cryptpad.removeLoadingScreen(function () { + Cryptpad.alert(Messages.login_invalUser); + }); + break; + case 'INVAL_PASS': + Cryptpad.removeLoadingScreen(function () { + Cryptpad.alert(Messages.login_invalPass); + }); + break; + case 'ALREADY_REGISTERED': + Cryptpad.removeLoadingScreen(function () { + Cryptpad.confirm(Messages.register_alreadyRegistered, function (yes) { + if (!yes) { return; } + proxy.login_name = uname; - if (!proxy[Cryptpad.displayNameKey]) { - proxy[Cryptpad.displayNameKey] = uname; - } - Cryptpad.eraseTempSessionValues(); - logMeIn(result); - }); - }); - break; - default: // UNHANDLED ERROR - Cryptpad.errorLoadingScreen(Messages.login_unhandledError); - } - return; - } - Cryptpad.eraseTempSessionValues(); - if (shouldImport) { - sessionStorage.migrateAnonDrive = 1; - } + if (!proxy[Cryptpad.displayNameKey]) { + proxy[Cryptpad.displayNameKey] = uname; + } + Cryptpad.eraseTempSessionValues(); + logMeIn(result); + }); + }); + break; + default: // UNHANDLED ERROR + Cryptpad.errorLoadingScreen(Messages.login_unhandledError); + } + return; + } - proxy.login_name = uname; - proxy[Cryptpad.displayNameKey] = uname; - sessionStorage.createReadme = 1; + if (Test.testing) { return void logMeIn(result); } - logMeIn(result); - }); + Cryptpad.eraseTempSessionValues(); + if (shouldImport) { + sessionStorage.migrateAnonDrive = 1; + } + + proxy.login_name = uname; + proxy[Cryptpad.displayNameKey] = uname; + sessionStorage.createReadme = 1; + + logMeIn(result); + }); + }, 0); + }, 100); }, { ok: Messages.register_writtenPassword, cancel: Messages.register_cancel, @@ -162,5 +185,18 @@ define([ $dialog.find('> div').addClass('half'); }); }); + + Test(function () { + $uname.val('test' + Math.random()); + $passwd.val('test'); + $confirm.val('test'); + $checkImport[0].checked = true; + $checkAcceptTerms[0].checked = true; + $register.click(); + + window.setTimeout(function () { + Cryptpad.findOKButton().click(); + }, 1000); + }); }); }); diff --git a/www/settings/index.html b/www/settings/index.html index 752d04468..28b36675e 100644 --- a/www/settings/index.html +++ b/www/settings/index.html @@ -40,6 +40,9 @@ Blog + + + @@ -97,7 +100,7 @@
- + diff --git a/www/settings/main.js b/www/settings/main.js index 7abff2e85..c7379db3a 100644 --- a/www/settings/main.js +++ b/www/settings/main.js @@ -3,7 +3,8 @@ define([ '/common/cryptpad-common.js', '/common/cryptget.js', '/common/mergeDrive.js', - '/bower_components/file-saver/FileSaver.min.js' + '/bower_components/file-saver/FileSaver.min.js', + '/customize/header.js', ], function ($, Cryptpad, Crypt, Merge) { var saveAs = window.saveAs; @@ -49,17 +50,17 @@ define([ var publicKey = obj.edPublic; if (publicKey) { + var userHref = Cryptpad.getUserHrefFromKeys(accountName, publicKey); var $pubLabel = $('', {'class': 'label'}) .text(Messages.settings_publicSigningKey + ':'); var $pubKey = $('', {type: 'text', readonly: true}) .css({ width: '28em' }) - .val(publicKey); + .val(userHref); $div.append('
').append($pubLabel).append($pubKey); } - return $div; }; @@ -222,33 +223,16 @@ define([ return $div; }; - var createUsageButton = function (obj) { - var proxy = obj.proxy; - + var createUsageButton = function () { var $div = $('
', { 'class': 'pinned-usage' }) .text(Messages.settings_usageTitle) .append('
'); - $(' + +
+ + + + + + + + + + + diff --git a/www/user/main.css b/www/user/main.css new file mode 100644 index 000000000..5cfc2ce85 --- /dev/null +++ b/www/user/main.css @@ -0,0 +1,14 @@ +.cp #mainBlock { + z-index: 1; + width: 1000px; + max-width: 90%; + margin: auto; + display: flex; + align-items: center; + justify-content: center; +} +.cp #mainBlock #container { + text-align: center; + font-size: 25px; +} + diff --git a/www/user/main.js b/www/user/main.js new file mode 100644 index 000000000..82f16682b --- /dev/null +++ b/www/user/main.js @@ -0,0 +1,63 @@ +define([ + 'jquery', + '/common/cryptpad-common.js', +], function ($, Cryptpad) { + + var APP = window.APP = { + Cryptpad: Cryptpad, + _onRefresh: [] + }; + + var Messages = Cryptpad.Messages; + + var comingSoon = function () { + var $div = $('
', { 'class': 'coming-soon' }) + .text(Messages.comingSoon) + .append('
'); + console.log($div); + return $div; + }; + + var andThen = function () { + console.log(APP.$container); + APP.$container.append(comingSoon()); + }; + + $(function () { + var $main = $('#mainBlock'); + // Language selector + var $sel = $('#language-selector'); + Cryptpad.createLanguageSelector(undefined, $sel); + $sel.find('button').addClass('btn').addClass('btn-secondary'); + $sel.show(); + + // User admin menu + var $userMenu = $('#user-menu'); + var userMenuCfg = { + $initBlock: $userMenu + }; + var $userAdmin = Cryptpad.createUserAdminMenu(userMenuCfg); + $userAdmin.find('button').addClass('btn').addClass('btn-secondary'); + + $(window).click(function () { + $('.cryptpad-dropdown').hide(); + }); + + // main block is hidden in case javascript is disabled + $main.removeClass('hidden'); + + APP.$container = $('#container'); + + Cryptpad.ready(function () { + //if (!Cryptpad.getUserHash()) { return redirectToMain(); } + + //var storeObj = Cryptpad.getStore().getProxy && Cryptpad.getStore().getProxy().proxy + // ? Cryptpad.getStore().getProxy() : undefined; + + //andThen(storeObj); + andThen(); + Cryptpad.reportAppUsage(); + }); + }); + +}); diff --git a/www/whiteboard/main.js b/www/whiteboard/main.js index a67229231..dce656473 100644 --- a/www/whiteboard/main.js +++ b/www/whiteboard/main.js @@ -10,12 +10,10 @@ define([ '/common/cryptpad-common.js', '/common/cryptget.js', '/whiteboard/colors.js', - '/common/visible.js', - '/common/notify.js', '/customize/application_config.js', '/bower_components/secure-fabric.js/dist/fabric.min.js', '/bower_components/file-saver/FileSaver.min.js', -], function ($, Config, Realtime, Crypto, Toolbar, TextPatcher, JSONSortify, JsonOT, Cryptpad, Cryptget, Colors, Visible, Notify, AppConfig) { +], function ($, Config, Realtime, Crypto, Toolbar, TextPatcher, JSONSortify, JsonOT, Cryptpad, Cryptget, Colors, AppConfig) { var saveAs = window.saveAs; var Messages = Cryptpad.Messages; @@ -212,9 +210,10 @@ window.canvas = canvas; var initializing = true; var $bar = $('#toolbar'); - var parsedHash = Cryptpad.parsePadUrl(window.location.href); - var defaultName = Cryptpad.getDefaultName(parsedHash); + + var Title; var UserList; + var Metadata; var config = module.config = { initialState: '{}', @@ -253,28 +252,14 @@ window.canvas = canvas; $colors.append($color); }; - var updatePalette = function (newPalette) { + var metadataCfg = {}; + var updatePalette = metadataCfg.updatePalette = function (newPalette) { palette = newPalette; $colors.html(''); palette.forEach(addColorToPalette); }; updatePalette(palette); - var suggestName = function (fallback) { - if (document.title === defaultName) { - return fallback || ""; - } else { - return document.title || defaultName; - } - }; - - var renameCb = function (err, title) { - if (err) { return; } - document.title = title; - config.onLocal(); - }; - - var makeColorButton = function ($container) { var $testColor = $('', { type: 'color', value: '!' }); @@ -305,18 +290,19 @@ window.canvas = canvas; config.onInit = function (info) { UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad); + + Title = Cryptpad.createTitle({}, config.onLocal, Cryptpad); + + Metadata = Cryptpad.createMetadata(UserList, Title, metadataCfg); + var configTb = { - displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'], + displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], userList: UserList.getToolbarConfig(), share: { secret: secret, channel: info.channel }, - title: { - onRename: renameCb, - defaultName: defaultName, - suggestName: suggestName - }, + title: Title.getTitleConfig(), common: Cryptpad, readOnly: readOnly, ifrw: window, @@ -327,6 +313,8 @@ window.canvas = canvas; toolbar = module.toolbar = Toolbar.create(configTb); + Title.setToolbar(toolbar); + var $rightside = toolbar.$rightside; /* save as template */ @@ -371,75 +359,11 @@ window.canvas = canvas; }; }; - var updateTitle = function (newTitle) { - if (newTitle === document.title) { return; } - // Change the title now, and set it back to the old value if there is an error - var oldTitle = document.title; - document.title = newTitle; - Cryptpad.renamePad(newTitle, function (err, data) { - if (err) { - console.log("Couldn't set pad title"); - console.error(err); - document.title = oldTitle; - return; - } - document.title = data; - $bar.find('.' + Toolbar.constants.title).find('span.title').text(data); - $bar.find('.' + Toolbar.constants.title).find('input').val(data); - }); - }; - - var updateDefaultTitle = function (defaultTitle) { - defaultName = defaultTitle; - $bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName); - }; - - - var updateMetadata = function(shjson) { - // Extract the user list (metadata) from the hyperjson - var json = (shjson === "") ? "" : JSON.parse(shjson); - var titleUpdated = false; - if (json && json.metadata) { - if (json.metadata.users) { - var userData = json.metadata.users; - // Update the local user data - UserList.addToUserData(userData); - } - if (json.metadata.defaultTitle) { - updateDefaultTitle(json.metadata.defaultTitle); - } - if (typeof json.metadata.title !== "undefined") { - updateTitle(json.metadata.title || defaultName); - titleUpdated = true; - } - if (typeof(json.metadata.palette) !== 'undefined') { - updatePalette(json.metadata.palette); - } - } - if (!titleUpdated) { - updateTitle(defaultName); - } - }; - - var unnotify = function () { - if (module.tabNotification && - typeof(module.tabNotification.cancel) === 'function') { - module.tabNotification.cancel(); - } - }; - - var notify = function () { - if (Visible.isSupported() && !Visible.currently()) { - unnotify(); - module.tabNotification = Notify.tab(1000, 10); - } - }; - var onRemote = config.onRemote = Catch(function () { if (initializing) { return; } var userDoc = module.realtime.getUserDoc(); - updateMetadata(userDoc); + Metadata.update(userDoc); var json = JSON.parse(userDoc); var remoteDoc = json.content; @@ -449,7 +373,7 @@ window.canvas = canvas; canvas.renderAll(); var content = canvas.toDatalessJSON(); - if (content !== remoteDoc) { notify(); } + if (content !== remoteDoc) { Cryptpad.notify(); } if (readOnly) { setEditable(false); } }); setEditable(false); @@ -460,11 +384,11 @@ window.canvas = canvas; metadata: { users: UserList.userData, palette: palette, - defaultTitle: defaultName + defaultTitle: Title.defaultTitle } }; if (!initializing) { - obj.metadata.title = document.title; + obj.metadata.title = Title.title; } // stringify the json and send it into chainpad return JSONSortify(obj); @@ -495,10 +419,6 @@ window.canvas = canvas; initializing = false; onRemote(); - if (Visible.isSupported()) { - Visible.onChange(function (yes) { if (yes) { unnotify(); } }); - } - /* TODO: restore palette from metadata.palette */ if (readOnly) { return; }