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

pull/1/head
yflory 8 years ago
commit df5cbfb53e

1
.gitignore vendored

@ -13,4 +13,5 @@ data
npm-debug.log
pins/
blob/
blobstage/
privileged.conf

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

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

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

@ -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,
/* 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
/* it is recommended that you serve CryptPad over https
* the filepaths below are used to configure your certificates
*/
//privKeyAndCertFiles: [

@ -1,16 +0,0 @@
<!-- This is an HTML fragment which is included into the bottom toolbar -->
<div>
<div class="bottom-bar">
<div class="bottom-bar-left">
<span class="bottom-bar-language">
<select id="language-selector"></select>
</span>
<p data-localization="bottom_france">
</p>
</div>
<div class="bottom-bar-right">
<p data-localization="bottom_support">
</p>
</div>
</div>
</div>

@ -39,6 +39,9 @@
<span class="link right">
<a href="https://blog.cryptpad.fr/" data-localization="blog">Blog</a>
</span>
<span class="link right">
<button id="upgrade" class="upgrade btn buttonSuccess" style="display: none;"></button>
</span>
</div>
@ -106,7 +109,7 @@
<div class="col">
<ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li>
<li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://riot.im/app/#/room/#cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -114,7 +117,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
<div class="version-footer">CryptPad v1.8.0 (Igopogo)</div>
</footer>
</body>

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

@ -39,6 +39,9 @@
<span class="link right">
<a href="https://blog.cryptpad.fr/" data-localization="blog">Blog</a>
</span>
<span class="link right">
<button id="upgrade" class="upgrade btn buttonSuccess" style="display: none;"></button>
</span>
</div>
@ -103,7 +106,7 @@
<div class="col">
<ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li>
<li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://riot.im/app/#/room/#cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -111,7 +114,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
<div class="version-footer">CryptPad v1.8.0 (Igopogo)</div>
</footer>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 749 B

@ -39,6 +39,9 @@
<span class="link right">
<a href="https://blog.cryptpad.fr/" data-localization="blog">Blog</a>
</span>
<span class="link right">
<button id="upgrade" class="upgrade btn buttonSuccess" style="display: none;"></button>
</span>
</div>
@ -225,7 +228,7 @@
<div class="col">
<ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li>
<li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://riot.im/app/#/room/#cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -233,7 +236,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
<div class="version-footer">CryptPad v1.8.0 (Igopogo)</div>
</footer>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

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

@ -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 = $('<div>', {'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;
// successful validation and user already exists
// set user hash in localStorage and redirect to drive
if (proxy && !proxy.login_name) {
proxy.login_name = result.userName;
}
proxy.edPrivate = result.edPrivate;
proxy.edPublic = result.edPublic;
Cryptpad.whenRealtimeSyncs(result.realtime, function () {
Cryptpad.login(result.userHash, result.userName, function () {
document.location.href = '/drive/';
});
});
return;
}
switch (err) {
case 'NO_SUCH_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser);
});
break;
case 'INVAL_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalUser);
});
break;
case 'INVAL_PASS':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalPass);
Cryptpad.addLoadingScreen(Messages.login_hashing);
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
window.setTimeout(function () {
loginReady(function () {
var uname = $uname.val();
var passwd = $passwd.val();
Login.loginOrRegister(uname, passwd, false, function (err, result) {
if (!err) {
var proxy = result.proxy;
// successful validation and user already exists
// set user hash in localStorage and redirect to drive
if (proxy && !proxy.login_name) {
proxy.login_name = result.userName;
}
proxy.edPrivate = result.edPrivate;
proxy.edPublic = result.edPublic;
Cryptpad.whenRealtimeSyncs(result.realtime, function () {
Cryptpad.login(result.userHash, result.userName, function () {
document.location.href = '/drive/';
});
});
break;
default: // UNHANDLED ERROR
Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
}
return;
}
switch (err) {
case 'NO_SUCH_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser);
});
break;
case 'INVAL_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalUser);
});
break;
case 'INVAL_PASS':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalPass);
});
break;
default: // UNHANDLED ERROR
Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
}
});
});
});
}, 0);
}, 0);
}, 100);
});
/* End Log in UI */

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

@ -39,6 +39,9 @@
<span class="link right">
<a href="https://blog.cryptpad.fr/" data-localization="blog">Blog</a>
</span>
<span class="link right">
<button id="upgrade" class="upgrade btn buttonSuccess" style="display: none;"></button>
</span>
</div>
@ -124,7 +127,7 @@
<div class="col">
<ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li>
<li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://riot.im/app/#/room/#cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -132,7 +135,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
<div class="version-footer">CryptPad v1.8.0 (Igopogo)</div>
</footer>
</body>

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

@ -31,7 +31,7 @@
<div class="col">
<ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li>
<li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://riot.im/app/#/room/#cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -39,5 +39,5 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
<div class="version-footer">CryptPad v1.8.0 (Igopogo)</div>
</footer>

@ -24,4 +24,7 @@
<span class="link right">
<a href="https://blog.cryptpad.fr/" data-localization="blog">Blog</a>
</span>
<span class="link right">
<button id="upgrade" class="upgrade btn buttonSuccess" style="display: none;"></button>
</span>
</div>

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

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

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

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

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

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

@ -39,6 +39,9 @@
<span class="link right">
<a href="https://blog.cryptpad.fr/" data-localization="blog">Blog</a>
</span>
<span class="link right">
<button id="upgrade" class="upgrade btn buttonSuccess" style="display: none;"></button>
</span>
</div>
@ -107,7 +110,7 @@
<div class="col">
<ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li>
<li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://riot.im/app/#/room/#cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -115,7 +118,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
<div class="version-footer">CryptPad v1.8.0 (Igopogo)</div>
</footer>
</body>

@ -7,6 +7,7 @@
font-family: FontAwesome;
}
.dropdown-bar button .fa-caret-down {
margin-right: 0px;
margin-left: 5px;
}
.dropdown-bar .dropdown-bar-content {
@ -112,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;

@ -13,8 +13,7 @@ define(function () {
out.type.slide = 'Presentación';
out.type.whiteboard = 'Pizarra';
out.updated_0_common_connectionLost = "<b>Connexión perdida</b><br>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 = "<b>Connexión perdida</b><br>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 <strong>muy fuertemente</strong> que lo guardes secreto.<br>Lo puedes usar para recuparar todos tus archivos en el caso que la memoria de tu navegador se borre.<br>Cualquiera con este enlace puede editar o eliminar todos los archivos en el explorador.<br>";
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.<br>Este pad no estará presente en tu CryptDrive.";
out.pinLimitDrive = "Has llegado al limite de espacio.<br>No puedes crear nuevos pads.";
out.printTransition = "Activar transiciones";
out.history_version = "Versión: ";
out.settings_logoutEverywhereTitle = "Cerrar sessión en todas partes";
out.settings_logoutEverywhere = "Cerrar todas las otras sessiones";
out.settings_logoutEverywhereConfirm = "¿Estás seguro? Tendrás que volver a iniciar sessión con todos tus dispositivos.";
out.upload_serverError = "Error: no pudimos subir tu archivo.";
out.upload_uploadPending = "Ya tienes una subida en progreso. ¿Cancelar y subir el nuevo archivo?";
out.upload_success = "Tu archivo ({0}) ha sido subido con éxito y fue añadido a tu drive.";
// 1.7.0 - Hodag
out.comingSoon = "Próximamente..."; // "Coming soon..."
out.newVersion = ["<b>CryptPad ha sido actualizado!</b>",
"Puedes ver lo que ha cambiada aquí (en inglés):",
"<a href=\"https://github.com/xwiki-labs/cryptpad/releases/tag/{0}\" target=\"_blank\">Notas de versión para CryptPad {0}</a>"].join("<br>");
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 <a href=\"https://accounts.cryptpad.fr/#!on={0}\" target=\"_blank\">suscribirte a una oferta premium</a> para obtener más espacio."].join("<br>");
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 (<a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">¿por qué?</a>). <a href=\"/register/\">Registrate</a> o <a href=\"/login/\">Inicia sesión</a> 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 <a href=\"https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/\" target=\"_blank\">leer este articulo</a> (en inglés) sobre por qué hacemos esto y por qué deberías <a href=\"/register/\">Registrarte</a> e <a href=\"/login/\">Iniciar sesión</a>.";
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. <a href='/about.html#pinning'>Acerca de...</a>";
out.poll_remove = "Quitar";
out.poll_edit = "Editar";
out.poll_locked = "Cerrado";
out.poll_unlocked = "Abierto";
return out;
});

@ -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 = '<b>CryptPad a été mis à jour !</b><br>' +
'Découvrez les nouveautés de la dernière version :<br>'+
'<a href="https://github.com/xwiki-labs/cryptpad/releases/tag/{0}" target="_blank">Notes de version pour CryptPad {0}</a>';
out.upgrade = "Augmenter votre limite";
out.upgradeTitle = "Améliorer votre compte pour augmenter la limite de stockage";
out.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.<br>" +
"Pour résoudre ce problème, vous pouvez soit supprimer des pads de votre CryptDrive (y compris la corbeille), soit vous abonner à une offre premium pour augmenter la limite maximale.";
out.updated_0_pinLimitReachedAlert = "Vous avez atteint votre limite de stockage. Les nouveaux pads ne seront pas enregistrés dans votre CryptDrive.<br>" +
'Vous pouvez soit supprimer des pads de votre CryptDrive, soit vous <a href="https://accounts.cryptpad.fr/#!on={0}" target="_blank">abonner à une offre premium</a> pour augmenter la limite maximale.';
out.pinLimitReachedAlert = out.updated_0_pinLimitReachedAlert;
out.pinLimitReachedAlertNoAccounts = out.pinLimitReached;
out.pinLimitNotPinned = "Vous avez atteint votre limite de stockage.<br>"+
"Ce pad n'est pas enregistré dans votre CryptDrive.";
out.pinLimitDrive = out.pinLimitReached+ ".<br>" +
@ -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 (<a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">découvrez pourquoi</a>). ' +
'<a href="/register/">Inscrivez-vous</a> ou <a href="/login/">connectez-vous</a> pour les maintenir en vie.';
out.fm_alert_backupUrl = "Lien de secours pour ce disque.<br>" +
"Il est <strong>fortement recommandé</strong> de garder ce lien pour vous-même.<br>" +
"Elle vous servira en cas de perte des données de votre navigateur afin de retrouver vos fichiers.<br>" +
"Quiconque se trouve en possession de celle-ci peut modifier ou supprimer tous les fichiers de ce gestionnaire.<br>";
out.fm_alert_anonymous = "Bonjour ! Vous utilisez actuellement Cryptpad de manière anonyme, ce qui ne pose pas de problème mais vos pads peuvent être supprimés après un certain temps " +
"d'inactivité. Nous avons désactivé certaines fonctionnalités avancées de CryptDrive pour les utilisateurs anonymes afin de rendre clair le fait que ce n'est pas " +
'un endroit sûr pour le stockage des documents. Vous pouvez <a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">en lire plus</a> concernant ' +
'nos raisons pour ces changements et pourquoi vous devriez vraiment <a href="/register/">vous enregistrer</a> et <a href="/login/">vous connecter</a>.';
out.fm_backup_title = 'Lien de secours';
out.fm_nameFile = 'Comment souhaitez-vous nommer ce fichier ?';
out.fm_error_cantPin = "Erreur interne du serveur. Veuillez recharger la page et essayer de nouveau.";
// File - Context menu
out.fc_newfolder = "Nouveau dossier";
out.fc_rename = "Renommer";
@ -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 <a href='/terms.html'>les conditions d'utilisation</a>";
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é. <a href='/about.html#pinning'>En savoir plus...</a>";
// index.html
//about.html
@ -349,15 +403,15 @@ define(function () {
out.main_zeroKnowledge_p = "Vous n'avez pas besoin de croire que nous n'<em>allons</em> pas regarder vos pads. Avec la technologie Zero Knowledge de CryptPad, nous ne <em>pouvons</em> pas le faire. Apprenez-en plus sur notre manière de <a href=\"privacy.html\" title='Protection des données'>protéger vos données</a>.";
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 <a href="http://ckeditor.com" target="_blank">CkEditor</a> temps-réel et Zero Knowledge.';
out.main_code = 'Éditeur de code';
out.main_code_p = 'Modifier votre code collaborativement grâce à notre application <a href="https://www.codemirror.net" target="_blank">CodeMirror</a> temps-réel et Zero Knowledge.';
out.main_code_p = 'Modifiez votre code collaborativement grâce à notre application <a href="https://www.codemirror.net" target="_blank">CodeMirror</a> 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 = [

@ -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 = '<b>CryptPad has been updated!</b><br>' +
'Check out what\'s new in the latest version:<br>'+
'<a href="https://github.com/xwiki-labs/cryptpad/releases/tag/{0}" target="_blank">Release notes for CryptPad {0}</a>';
out.upgrade = "Upgrade";
out.upgradeTitle = "Upgrade your account to increase the storage limit";
out.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.<br>" +
"To fix this problem, you can either remove pads from your CryptDrive (including the trash) or subscribe to a premium offer to increase your limit.";
out.updated_0_pinLimitReachedAlert = "You've reached your storage limit. New pads won't be stored in your CryptDrive.<br>" +
'You can either remove pads from your CryptDrive or <a href="https://accounts.cryptpad.fr/#!on={0}" target="_blank">subscribe to a premium offer</a> to increase your limit.';
out.pinLimitReachedAlert = out.updated_0_pinLimitReachedAlert;
out.pinLimitReachedAlertNoAccounts = out.pinLimitReached;
out.pinLimitNotPinned = "You've reached your storage limit.<br>"+
"This pad is not stored in your CryptDrive.";
out.pinLimitDrive = "You've reached your storage limit.<br>" +
@ -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 (<a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">find out why</a>). ' +
'<a href="/register/">Sign up</a> or <a href="/login/">Log in</a> to keep them alive.';
out.fm_alert_backupUrl = "Backup link for this drive.<br>" +
"It is <strong>highly recommended</strong> that you keep ip for yourself only.<br>" +
"You can use it to retrieve all your files in case your browser memory got erased.<br>" +
"Anybody with that link can edit or remove all the files in your file manager.<br>";
out.fm_alert_anonymous = "Hello there, you are currently using CryptPad anonymously, that's ok but your pads may be deleted after a period of " +
"inactivity. We have disabled advanced features of the drive for anonymous users because we want to be clear that it is " +
'not a safe place to store things. You can <a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">read more</a> about ' +
'why we are doing this and why you really should <a href="/register/">Sign up</a> and <a href="/login/">Log in</a>.';
out.fm_backup_title = 'Backup link';
out.fm_nameFile = 'How would you like to name that file?';
out.fm_error_cantPin = "Internal server error. Please reload the page and try again.";
// File - Context menu
out.fc_newfolder = "New folder";
out.fc_rename = "Rename";
@ -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 <a href='/terms.html'>the terms of service</a>";
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. <a href='/about.html#pinning'>Learn more...</a>";
// 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 <em>won't</em> look at your pads, with CryptPad's revolutionary Zero Knowledge Technology we <em>can't</em>. Learn more about how we protect your <a href=\"/privacy.html\" title='Privacy'>Privacy and Security</a>.";
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 = [

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

@ -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 = "<b>伺服器連線中斷</b><br>現在是唯讀狀態,直到連線恢復正常。";
out.common_connectionLost = out.updated_0_common_connectionLost;
out.websocketError = '無法連結上 websocket 伺服器...';
out.typeError = "這個編輯檔與所選的應用程式並不相容";
out.onLogout = '你已登出, <a href="/" target="_blank">點擊這裏</a> 來登入<br>或按<em>Escape</em> 來以唯讀模型使用你的編輯檔案';
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 = '<b>CryptPad 已更新!</b><br>' +
'檢查最新版本有什麼新功能:<br>'+
'<a href="https://github.com/xwiki-labs/cryptpad/releases/tag/{0}" target="_blank">CryptPad新發佈記事 {0}</a>';
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.<br>" +
'要嘛你可以自 CryptDrive 移除原有文件或是 <a href="https://accounts.cryptpad.fr/#!on={0}" target="_blank">昇級到付費版</a>增加你的儲存容量。';
out.pinLimitReachedAlert = out.updated_0_pinLimitReachedAlert;
out.pinLimitNotPinned = "你已達到容量使用上限<br>"+
"這個檔案無法儲存到你的 CryptDrive.";
out.pinLimitDrive = "你已達到容量使用上限<br>" +
"你不能建立新的編輯檔案";
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 = '這個檔案已被移置垃圾筒<br><a href="/drive/">讀取我的雲端硬碟</a>';
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 = "零知識, <em>即時</em> 排程";
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 = '你尚未登入,因此這些工作檔案可能會被刪除。 (<a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">了解原因</a>). ' +
'<a href="/register/">註冊</a>或<a href="/login/">登入</a>以便保留它們。';
out.fm_alert_backupUrl = "這個雲端硬碟的備份連結<br>" +
"<strong>高度建議</strong>把自己的 IP 資訊保留成只有自己知道<br>" +
"萬一瀏覽器記憶被消除,你可以用它來接收所有的檔案。<br>" +
"任何知道此連結的人可以編輯或移除你檔案管理底下的所有檔案。<br>";
out.fm_alert_anonymous = "嗨你好, 你目前正以匿名方式在使用 CryptPad , 這也沒問題,不過你的東西過一段時間沒動靜後,就會自動被刪除。 " +
"匿名的用戶我們也取消其進階功能,因為我們要明確地讓用戶知道,這裏 " +
'不是一個安全存放東西的地方。你可以 <a href="https://blog.cryptpad.fr/2017/05/17/You-gotta-log-in/" target="_blank">進一步了解 </a> 關於 ' +
'為何我們這樣作,以及為何你最好能夠<a href="/register/">註冊</a> 以及 <a href="/login/">登錄</a>使用。';
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 = "我同意 <a href='/terms.html'>服務條款</a>";
out.register_passwordsDontMatch = "密碼不相符!";
out.register_mustAcceptTerms = "你必須同意我們的服務條款。";
out.register_mustRememberPass = "如果你忘了密碼,我們也無法為你重置。因此務必自行好好記住! 請在勾選處勾選確認。";
out.register_header = "歡迎來到 CryptPad";
out.register_explanation = [
"<p>首先讓我們先了解幾件事</p>",
"<ul>",
"<li>你的密碼是你用來加密所有工作檔案的密鑰。一旦遺失它,我們也沒辦法幫你恢復你的資料。</li>",
"<li>你可以滙入近期在瀏覽器下檢視的工作檔案到你的雲端硬碟裏。</li>",
"<li>如果你使用的是公用分享電腦,你需要在完成工作後進行登出,只是關閉分頁是不夠的。</li>",
"</ul>"
].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 = "這個動作會自你的雲端硬碟中移除所有工作檔案<br>"+
"確定要繼續嗎?<br>" +
"輸入 “<em>I love CryptPad</em>” 來確認。";
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 個月到期後刪除。 <a href='/about.html#pinning'>進一步了解...</a>";
// index.html
//about.html
out.main_p2 = '本專案使用 <a href="http://ckeditor.com/">CKEditor</a> 視覺編輯器, <a href="https://codemirror.net/">CodeMirror</a>, 以及 <a href="https://github.com/xwiki-contrib/chainpad">ChainPad</a> 即時引擊。';
out.main_howitworks_p1 = 'CryptPad 應用一種變體的 <a href="https://en.wikipedia.org/wiki/Operational_transformation">操作型變換 Operational transformation</a> 演算法,它利用<a href="https://bitcoin.org/bitcoin.pdf">Nakamoto Blockchain</a>來找到分散的共識, Nakamoto Blockchain 是一種建構當前流行的<a href="https://en.wikipedia.org/wiki/Bitcoin">比特幣</a>。這套演算法可避免需要一個中央的伺服器來解析操作型變換編輯衝突,而無須處理解析衝突,伺服器並不知道哪一個檔案被編輯。';
// contact.html
out.main_about_p2 = '若有任何問題和建議, 可以在<a href="https://twitter.com/cryptpad">tweet us</a>, <a href="https://github.com/xwiki-labs/cryptpad/issues/" title="our issue tracker">github</a>提出問題, 或是來到 irc (<a href="http://webchat.freenode.net?channels=%23cryptpad&uio=MT1mYWxzZSY5PXRydWUmMTE9Mjg3JjE1PXRydWUe7" title="freenode webchat">irc.freenode.net</a>)打聲招呼, 再或者 <a href="mailto:research@xwiki.com">寄封電郵給我們</a>.';
out.main_info = "<h1>Collaborate in Confidence</h1><br> 利用共同享文件發嚮點子,透過 <strong>零知識 </strong> 科技確保隱私安全; 對任何網路服務商都要加以提防。";
out.main_howitworks = '它如何運作';
out.main_zeroKnowledge = '零知識';
out.main_zeroKnowledge_p = "你不必相信我們所說的<em>並不會</em> 察看你的檔案, CryptPad 革命性的零知識技術讓我們 <em>真的不能看到</em>。 進一步了解在這裏,我們如何保護用戶的 <a href=\"/privacy.html\" title='Privacy'>隱私和安全</a>。";
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 = '利用我們的即時零知識技術,集體協作地編輯富文本檔案 <a href="http://ckeditor.com" target="_blank">CkEditor</a> 應用程式application.';
out.main_code = '代碼編輯器';
out.main_code_p = '利用我們的即時零知識技術,集體協作地編輯程式代碼 <a href="https://www.codemirror.net" target="_blank">CodeMirror</a> 應用程式。';
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 標頭,其用於識別你特定的瀏覽器。 你可以訪問 <a target="_blank" rel="noopener noreferrer" href="https://www.whatismybrowser.com/detect/what-http-headers-is-my-browser-sending" title="what http headers is my browser sending">WhatIsMyBrowser.com</a>這個網站,知道你的瀏覽器分享了哪些資訊。';
out.policy_whatweknow_p2 = '我們使用 <a href="https://www.elastic.co/products/kibana" target="_blank" rel="noopener noreferrer" title="open source analytics platform">Kibana</a>, 它是一個開源的流量數據分析平台, 以更了解用戶。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 地址, 你可以利用<a href="https://www.torproject.org/projects/torbrowser.html.en" title="downloads from the Tor project" target="_blank" rel="noopener noreferrer">Tor 瀏覽器套件</a>來保護隱藏 IP 地址, 或是使用 <a href="https://riseup.net/en/vpn" title="VPNs provided by Riseup" target="_blank" rel="noopener noreferrer">VPN</a>。';
out.policy_choices_ads = '如果你只是想要封鎖我們的數據分析器, 你可以使用廣告封鎖工具如 <a hre="https://www.eff.org/privacybadger" title="download privacy badger" target="_blank" rel="noopener noreferrer">Privacy Badger</a>.';
// 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 = '<a href="http://www.xwiki.com/" target="_blank" rel="noopener noreferrer">Made with <img class="bottom-bar-heart" src="/customize/heart.png" alt="love" /> in <img class="bottom-bar-fr" src="/customize/fr.png" alt="France" /></a>';
out.bottom_support = '<a href="http://labs.xwiki.com/" title="XWiki Labs" target="_blank" rel="noopener noreferrer">An <img src="/customize/logo-xwiki2.png" alt="XWiki SAS" class="bottom-bar-xwiki"/> Labs Project </a> with the support of <a href="http://ng.open-paas.org/" title="OpenPaaS::ng" target="_blank" rel="noopener noreferrer"> <img src="/customize/openpaasng.png" alt="OpenPaaS-ng" class="bottom-bar-openpaas" /></a>';
// Header.html
out.header_france = '<a href="http://www.xwiki.com/" target="_blank" rel="noopener noreferrer">With <img class="bottom-bar-heart" src="/customize/heart.png" alt="love" /> from <img class="bottom-bar-fr" src="/customize/fr.png" title="France" alt="France"/> by <img src="/customize/logo-xwiki.png" alt="XWiki SAS" class="bottom-bar-xwiki"/></a>';
out.header_support = '<a href="http://ng.open-paas.org/" title="OpenPaaS::ng" target="_blank" rel="noopener noreferrer"> <img src="/customize/openpaasng.png" alt="OpenPaaS-ng" class="bottom-bar-openpaas" /></a>';
out.header_logoTitle = '回到主頁';
// Initial states
out.initialState = [
'<span style="font-size:16px;"><p>',
'這是&nbsp;<strong>CryptPad</strong>, 零知識即時協作編輯平台,當你輸入時一切已即存好。',
'<br>',
'分享這個工作檔案的網址連結給友人或是使用、 <span style="background-color:#5cb85c;color:#ffffff;">&nbsp;分享&nbsp;</span> 按鈕分享<em>唯讀的連結</em>&nbsp;其只能看不能編寫。',
'</p>',
'<p><span style="color:#808080;"><em>',
'來吧, 開始打字輸入吧...',
'</em></span></p></span>',
'<p>&nbsp;<br></p>'
].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 = "如果欲退出客戶資料收集, 請到 <a href='/settings/'>用戶設定頁</a>, 可以找到勾選項目來啟用或關閉用戶回饋功能。";
return out;
});

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

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

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

574
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); }
cb(void 0, size);
Fs.stat(path, function (err, stats) {
if (err) { return void cb(err); }
cb(void 0, stats.size);
});
};
var getMultipleFileSize = function (store, channels, cb) {
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);
});
};
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) {
var defaultLimit = typeof(config.defaultStorageLimit) === 'number'?
config.defaultStorageLimit: DEFAULT_LIMIT;
var userId;
if (publicKey) {
userId = unescapeKeyCharacters(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) {
// expected array
return void cb('[TYPE_ERROR] pin expects channel list argument');
return void cb('INVALID_PIN_LIST');
}
getChannelList(store, Sessions, publicKey, function (pinned) {
var session = beginSession(Sessions, publicKey);
// get channel list ensures your session has a cached channel list
getChannelList(Env, publicKey, function (pinned) {
var session = beginSession(Env.Sessions, publicKey);
// only pin channels which are not already pinned
var toStore = channels.filter(function (channel) {
@ -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);
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(store, Sessions, publicKey, function (e, hash) {
cb(e, hash);
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,42 +741,105 @@ 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 (stagingPath, Sessions, publicKey, cb) {
var filePath = makeFilePath(stagingPath, publicKey);
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'); }
isFile(filePath, function (e, yes) {
cb(e, yes);
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 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');*/
RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) {
// load pin-store...
console.log('loading rpc module...');
var Sessions = {};
var warn = function (e, output) {
if (e && !config.suppressRPCErrors) {
console.error(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;
Env.maxUploadSize = config.maxUploadSize || (20 * 1024 * 1024);
var store;
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; }

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

@ -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 '<br><br>insert: ' + diff.insert + '<br><br>' +
'remove: ' + diff.remove + '<br><br>';
return cb('<br><br>insert: ' + diff.insert + '<br><br>' +
'remove: ' + diff.remove + '<br><br>');
}
}, "expected hyperjson equality");
};
@ -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;
}, "version 1 hash failed to parse");
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 () {
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;
assert(function (cb) {
var secret = Cryptpad.parsePadUrl('/pad/#/1/edit/CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G/present');
return cb(secret.hashData.version === 1
&& secret.hashData.mode === "edit"
&& secret.hashData.channel === "CmN5+YJkrHFS3NSBg-P7Sg"
&& secret.hashData.key === "DNZ2wcG683GscU4fyOyqA87G"
&& secret.hashData.present);
}, "version 1 hash failed to parse");
// test support for present mode in hashes
assert(function (cb) {
var secret = Cryptpad.parsePadUrl('/pad/#/1/edit//CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G//present');
return cb(secret.hashData.version === 1
&& secret.hashData.mode === "edit"
&& secret.hashData.channel === "CmN5+YJkrHFS3NSBg-P7Sg"
&& secret.hashData.key === "DNZ2wcG683GscU4fyOyqA87G"
&& secret.hashData.present);
}, "Couldn't handle multiple successive slashes");
// test support for trailing slash
assert(function () {
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 () { /*
<p class="error">
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(){/*
<div class="report">{{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');
var $report = $('.report');
$report.addClass(failed?'failure':'success');
if (failed) {
Test.failed();
} else {
Test.passed();
}
});
});

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

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

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

@ -5,6 +5,7 @@
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<meta name="referrer" content="no-referrer" />
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<link rel="icon" type="image/png"
href="/customize/main-favicon.png"

@ -8,6 +8,7 @@
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/dialog/dialog.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/fold/foldgutter.css" />
<link rel="stylesheet" href="/code/code.css" />
<script src="/bower_components/codemirror/mode/javascript/javascript.js"></script>
<script src="/bower_components/codemirror/addon/mode/loadmode.js"></script>
<script src="/bower_components/codemirror/mode/meta.js"></script>
@ -31,33 +32,13 @@
<script src="/bower_components/codemirror/addon/fold/markdown-fold.js"></script>
<script src="/bower_components/codemirror/addon/fold/comment-fold.js"></script>
<script src="/bower_components/codemirror/addon/display/placeholder.js"></script>
<style>
html, body{
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
overflow: hidden;
box-sizing: border-box;
position: relative;
}
body {
display: flex;
flex-flow: column;
}
.CodeMirror {
height: 100%;
}
.CodeMirror-focused .cm-matchhighlight {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
background-position: bottom;
background-repeat: repeat-x;
}
</style>
</head>
<body>
<div id="cme_toolbox" class="toolbar-container"></div>
<textarea id="editor1" name="editor1"></textarea>
<div id="editorContainer">
<textarea id="editor1" name="editor1"></textarea>
<div id="previewContainer"><div id="preview"></div></div>
</div>
</body>
</html>

@ -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($('<link />', {
rel: 'stylesheet',
href: path + theme + '.css',
}));
};
return function (theme, $select) {
if (!theme) {
editor.setOption('theme', 'default');
} else {
if (!themeLoaded(theme)) {
loadTheme(theme);
}
editor.setOption('theme', theme);
}
if ($select) {
$select.setValue(theme || 'Theme');
}
};
}());
var setEditable = module.setEditable = function (bool) {
var setEditable = 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;
}
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');
return;
}
editor.setValue(content);
onLocal();
};
var renameCb = function (err, title) {
if (err) { return; }
document.title = title;
onLocal();
};
var updateTitle = function (newTitle) {
if (newTitle === document.title) { return; }
// Change the title now, and set it back to the old value if there is an error
var oldTitle = document.title;
document.title = newTitle;
Cryptpad.renamePad(newTitle, function (err, data) {
if (err) {
console.log("Couldn't set pad title");
console.error(err);
document.title = oldTitle;
return;
}
document.title = data;
$bar.find('.' + Toolbar.constants.title).find('span.title').text(data);
$bar.find('.' + Toolbar.constants.title).find('input').val(data);
});
APP.$previewButton.hide();
$previewContainer.hide();
$codeMirror.addClass('fullPage');
};
var updateDefaultTitle = function (defaultTitle) {
defaultName = defaultTitle;
$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName);
};
config.onInit = function (info) {
UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, Cryptpad);
var updateMetadata = function(shjson) {
// Extract the user list (metadata) from the hyperjson
var json = (shjson === "") ? "" : JSON.parse(shjson);
var titleUpdated = false;
if (json && json.metadata) {
if (json.metadata.users) {
var userData = json.metadata.users;
// Update the local user data
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);
}
};
var titleCfg = { getHeadingText: CodeMirror.getHeadingText };
Title = Cryptpad.createTitle(titleCfg, config.onLocal, Cryptpad);
config.onInit = function (info) {
UserList = Cryptpad.createUserList(info, config.onLocal, Cryptget, 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);
}
};
histConfig.onClose = function () {
// Close button clicked
setHistory(false, true);
};
histConfig.onRevert = function () {
// Revert button clicked
setHistory(false, false);
config.onLocal();
config.onRemote();
};
histConfig.onReady = function () {
// Called when the history is loaded and the UI displayed
setHistory(true);
},
$toolbar: $bar
};
histConfig.$toolbar = $bar;
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist);
@ -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);
}
//get old cursor here
var oldCursor = {};
oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc);
oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc);
editor.setValue(remoteDoc);
editor.save();
var op = TextPatcher.diff(oldDoc, remoteDoc);
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
return TextPatcher.transformCursor(oldCursor[attr], op);
});
if(selects[0] === selects[1]) {
editor.setCursor(posToCursor(selects[0], remoteDoc));
}
else {
editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc));
if (highlightMode && highlightMode !== APP.highlightMode) {
CodeMirror.setMode(highlightMode, onModeChanged);
}
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);

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

@ -0,0 +1,300 @@
define([
'jquery',
'/common/modes.js',
'/common/themes.js',
'/bower_components/file-saver/FileSaver.min.js'
], function ($, Modes, Themes) {
var saveAs = window.saveAs;
var module = {};
module.create = function (CMeditor, ifrw, Cryptpad) {
var exp = {};
var Messages = Cryptpad.Messages;
var CodeMirror = exp.CodeMirror = CMeditor;
CodeMirror.modeURL = "/bower_components/codemirror/mode/%N/%N.js";
var $pad = $('#pad-iframe');
var $textarea = exp.$textarea = $pad.contents().find('#editor1');
var Title;
var onLocal = function () {};
var $rightside;
exp.init = function (local, title, toolbar) {
if (typeof local === "function") {
onLocal = local;
}
Title = title;
$rightside = toolbar.$rightside;
};
var editor = exp.editor = CMeditor.fromTextArea($textarea[0], {
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets : true,
showTrailingSpace : true,
styleActiveLine : true,
search: true,
highlightSelectionMatches: {showToken: /\w+/},
extraKeys: {"Shift-Ctrl-R": undefined},
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
mode: "javascript",
readOnly: true
});
editor.setValue(Messages.codeInitialState);
var setMode = exp.setMode = function (mode, cb) {
exp.highlightMode = mode;
if (mode === 'text') {
editor.setOption('mode', 'text');
if (cb) { cb('text'); }
return;
}
CMeditor.autoLoadMode(editor, mode);
editor.setOption('mode', mode);
if (exp.$language) {
var name = exp.$language.find('a[data-value="' + mode + '"]').text() || 'Mode';
exp.$language.setValue(name);
}
if(cb) { cb(mode); }
};
var setTheme = exp.setTheme = (function () {
var path = '/common/theme/';
var $head = $(ifrw.document.head);
var themeLoaded = exp.themeLoaded = function (theme) {
return $head.find('link[href*="'+theme+'"]').length;
};
var loadTheme = exp.loadTheme = function (theme) {
$head.append($('<link />', {
rel: 'stylesheet',
href: path + theme + '.css',
}));
};
return function (theme, $select) {
if (!theme) {
editor.setOption('theme', 'default');
} else {
if (!themeLoaded(theme)) {
loadTheme(theme);
}
editor.setOption('theme', theme);
}
if ($select) {
$select.setValue(theme || 'Theme');
}
};
}());
exp.getHeadingText = function () {
var lines = editor.getValue().split(/\n/);
var text = '';
lines.some(function (line) {
// lisps?
var lispy = /^\s*(;|#\|)(.*?)$/;
if (lispy.test(line)) {
line.replace(lispy, function (a, one, two) {
text = two;
});
return true;
}
// lines beginning with a hash are potentially valuable
// works for markdown, python, bash, etc.
var hash = /^#(.*?)$/;
if (hash.test(line)) {
line.replace(hash, function (a, one) {
text = one;
});
return true;
}
// lines including a c-style comment are also valuable
var clike = /^\s*(\/\*|\/\/)(.*)?(\*\/)*$/;
if (clike.test(line)) {
line.replace(clike, function (a, one, two) {
if (!(two && two.replace)) { return; }
text = two.replace(/\*\/\s*$/, '').trim();
});
return true;
}
// TODO make one more pass for multiline comments
});
return text.trim();
};
exp.configureLanguage = function (cb, onModeChanged) {
var options = [];
Modes.list.forEach(function (l) {
options.push({
tag: 'a',
attributes: {
'data-value': l.mode,
'href': '#',
},
content: l.language // Pretty name of the language value
});
});
var dropdownConfig = {
text: 'Mode', // Button initial text
options: options, // Entries displayed in the menu
left: true, // Open to the left of the button
isSelect: true,
};
var $block = exp.$language = Cryptpad.createDropdown(dropdownConfig);
$block.find('a').click(function () {
setMode($(this).attr('data-value'), onModeChanged);
onLocal();
});
if ($rightside) { $rightside.append($block); }
if (cb) { cb(); }
};
exp.configureTheme = function (cb) {
/* Remember the user's last choice of theme using localStorage */
var themeKey = 'CRYPTPAD_CODE_THEME';
var lastTheme = localStorage.getItem(themeKey) || 'default';
var options = [];
Themes.forEach(function (l) {
options.push({
tag: 'a',
attributes: {
'data-value': l.name,
'href': '#',
},
content: l.name // Pretty name of the language value
});
});
var dropdownConfig = {
text: 'Theme', // Button initial text
options: options, // Entries displayed in the menu
left: true, // Open to the left of the button
isSelect: true,
initialValue: lastTheme
};
var $block = exp.$theme = Cryptpad.createDropdown(dropdownConfig);
setTheme(lastTheme, $block);
$block.find('a').click(function () {
var theme = $(this).attr('data-value');
setTheme(theme, $block);
localStorage.setItem(themeKey, theme);
});
if ($rightside) { $rightside.append($block); }
if (cb) { cb(); }
};
exp.exportText = function () {
var text = editor.getValue();
var ext = Modes.extensionOf(exp.highlightMode);
var title = Cryptpad.fixFileName(Title ? Title.suggestTitle('cryptpad') : "?") + (ext || '.txt');
Cryptpad.prompt(Messages.exportPrompt, title, function (filename) {
if (filename === null) { return; }
var blob = new Blob([text], {
type: 'text/plain;charset=utf-8'
});
saveAs(blob, filename);
});
};
exp.importText = function (content, file) {
var $bar = ifrw.$('#cme_toolbox');
var mode;
var mime = CodeMirror.findModeByMIME(file.type);
if (!mime) {
var ext = /.+\.([^.]+)$/.exec(file.name);
if (ext[1]) {
mode = CMeditor.findModeByExtension(ext[1]);
}
} else {
mode = mime && mime.mode || null;
}
if (mode && Modes.list.some(function (o) { return o.mode === mode; })) {
setMode(mode);
$bar.find('#language-mode').val(mode);
} else {
console.log("Couldn't find a suitable highlighting mode: %s", mode);
setMode('text');
$bar.find('#language-mode').val('text');
}
editor.setValue(content);
onLocal();
};
var cursorToPos = function(cursor, oldText) {
var cLine = cursor.line;
var cCh = cursor.ch;
var pos = 0;
var textLines = oldText.split("\n");
for (var line = 0; line <= cLine; line++) {
if(line < cLine) {
pos += textLines[line].length+1;
}
else if(line === cLine) {
pos += cCh;
}
}
return pos;
};
var posToCursor = function(position, newText) {
var cursor = {
line: 0,
ch: 0
};
var textLines = newText.substr(0, position).split("\n");
cursor.line = textLines.length - 1;
cursor.ch = textLines[cursor.line].length;
return cursor;
};
exp.setValueAndCursor = function (oldDoc, remoteDoc, TextPatcher) {
var scroll = editor.getScrollInfo();
//get old cursor here
var oldCursor = {};
oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc);
oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc);
editor.setValue(remoteDoc);
editor.save();
var op = TextPatcher.diff(oldDoc, remoteDoc);
var selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
return TextPatcher.transformCursor(oldCursor[attr], op);
});
if(selects[0] === selects[1]) {
editor.setCursor(posToCursor(selects[0], remoteDoc));
}
else {
editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc));
}
editor.scrollTo(scroll.left, scroll.top);
};
return exp;
};
return module;
});

@ -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 = parsed.channel;
secret.keys = { fileKeyStr: parsed.key };
} else if (parsed.type === "user") {
// version 2 hashes are to be used for encrypted blobs
secret.channel = hashArray[2].replace(/-/g, '/');
secret.keys = { fileKeyStr: hashArray[3].replace(/-/g, '/') };
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/<fileId>/<cryptKey>/<contentType>
/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) {

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

@ -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 = $('<input type="file">').click();

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

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

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

@ -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(),

@ -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 = $('<span>', {'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 = $('<span>', {'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 = $('<span>', {'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 = $('<a>', {
href: "https://accounts.cryptpad.fr/#!on=" + origin,
rel: "noreferrer noopener",
target: "_blank",
}).appendTo($container);
$('<button>', {
'class': 'upgrade buttonSuccess',
title: Messages.upgradeTitle
}).text(Messages.upgrade).appendTo($upgradeLink);
}
var prettyUsage;
var prettyLimit;
if (unit === 'GB') {
prettyUsage = Messages._getKey('formattedGB', [usage]);
prettyLimit = Messages._getKey('formattedGB', [limit]);
} else {
prettyUsage = Messages._getKey('formattedMB', [usage]);
prettyLimit = Messages._getKey('formattedMB', [limit]);
}
if (quota < 0.8) { $usage.addClass('normal'); }
else if (quota < 1) { $usage.addClass('warning'); }
else { $usage.addClass('above'); }
var $text = $('<span>', {'class': 'usageText'});
$text.text(usage + ' / ' + prettyLimit);
$limit.append($usage).append($text);
window.setTimeout(function () {
common.isOverPinLimit(todo);
}, LIMIT_REFRESH_RATE);
cb(err, $container);
};
common.isOverPinLimit(todo);
};
common.createButton = function (type, rightside, data, callback) {
var button;
var size = "17px";
@ -816,6 +931,7 @@ define([
common.addTemplate(makePad(href, title));
whenRealtimeSyncs(getStore().getProxy().info.realtime, function () {
common.alert(Messages.templateSaved);
common.feedback('TEMPLATE_CREATED');
});
});
};
@ -838,7 +954,8 @@ define([
if (callback) {
button.click(function() {
var href = window.location.href;
common.confirm(Messages.forgetPrompt, function (yes) {
var msg = isLoggedIn() ? Messages.forgetPrompt : Messages.fm_removePermanentlyDialog;
common.confirm(msg, function (yes) {
if (!yes) { return; }
common.forgetPad(href, function (err) {
if (err) {
@ -857,7 +974,8 @@ define([
} else {
callback();
}
common.alert(Messages.movedToTrash, undefined, true);
var cMsg = isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
common.alert(cMsg, undefined, true);
return;
});
});
@ -1207,6 +1325,27 @@ define([
return $userAdmin;
};
var CRYPTPAD_VERSION = 'cryptpad-version';
var updateLocalVersion = function () {
// Check for CryptPad updates
var urlArgs = Config.requireConf ? Config.requireConf.urlArgs : null;
if (!urlArgs) { return; }
var arr = /ver=([0-9.]+)(-[0-9]*)?/.exec(urlArgs);
var ver = arr[1];
if (!ver) { return; }
var verArr = ver.split('.');
verArr[2] = 0;
if (verArr.length !== 3) { return; }
var stored = localStorage[CRYPTPAD_VERSION] || '0.0.0';
var storedArr = stored.split('.');
storedArr[2] = 0;
var shouldUpdate = parseInt(verArr[0]) > parseInt(storedArr[0]) ||
(parseInt(verArr[0]) === parseInt(storedArr[0]) &&
parseInt(verArr[1]) > parseInt(storedArr[1]));
if (!shouldUpdate) { return; }
common.alert(Messages._getKey('newVersion', [verArr.join('.')]), null, true);
localStorage[CRYPTPAD_VERSION] = ver;
};
common.ready = (function () {
var env = {};
@ -1224,6 +1363,9 @@ define([
block--;
if (!block) {
initialized = true;
updateLocalVersion();
f(void 0, env);
}
};
@ -1247,7 +1389,7 @@ define([
feedback("NO_PROXIES");
}
if (typeof(Array.isArray) !== 'function') {
if (/CRYPTPAD_SHIM/.test(Array.isArray.toString())) {
feedback("NO_ISARRAY");
}
@ -1257,20 +1399,21 @@ define([
UI.Alertify.reset();
// Load the new pad when the hash has changed
var oldHash = document.location.hash.slice(1);
var oldHref = document.location.href;
window.onhashchange = function () {
var newHash = document.location.hash.slice(1);
var parsedOld = parseHash(oldHash);
var parsedNew = parseHash(newHash);
var newHref = document.location.href;
var parsedOld = parsePadUrl(oldHref).hashData;
var parsedNew = parsePadUrl(newHref).hashData;
if (parsedOld && parsedNew && (
parsedOld.channel !== parsedNew.channel
parsedOld.type !== parsedNew.type
|| parsedOld.channel !== parsedNew.channel
|| parsedOld.mode !== parsedNew.mode
|| parsedOld.key !== parsedNew.key)) {
document.location.reload();
return;
}
if (parsedNew) {
oldHash = newHash;
oldHref = newHref;
}
};
@ -1287,6 +1430,14 @@ define([
console.log('RPC handshake complete');
rpc = common.rpc = env.rpc = call;
common.getPinLimit(function (e, limit, plan, note) {
if (e) { return void console.error(e); }
common.account.limit = limit;
localStorage.plan = common.account.plan = plan;
common.account.note = note;
cb();
});
common.arePinsSynced(function (err, yes) {
if (!yes) {
common.resetPins(function (err) {
@ -1295,7 +1446,6 @@ define([
});
}
});
cb();
});
} else if (PINNING_ENABLED) {
console.log('not logged in. pads will not be pinned');

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

@ -173,6 +173,10 @@ define([
proxy[tokenKey] = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);
}
// copy User_hash into sessionStorage because cross-domain iframes
// on safari replaces localStorage with sessionStorage or something
if (sessionStorage) { sessionStorage.setItem('User_hash', localStorage.getItem('User_hash')); }
var localToken = tryParsing(localStorage.getItem(tokenKey));
if (localToken === null) {
// if that number hasn't been set to localStorage, do so.
@ -222,7 +226,7 @@ define([
if (!hash) {
throw new Error('[Store.init] Unable to find or create a drive hash. Aborting...');
}
var secret = Cryptpad.getSecrets(hash);
var secret = Cryptpad.getSecrets('drive', hash);
var listmapConfig = {
data: {},
websocketURL: Cryptpad.getWebsocketURL(),

File diff suppressed because one or more lines are too long

@ -188,6 +188,10 @@ define([
newRecentPads.push(data);
}
});
if (!proxy.FS_hashes || !Array.isArray(proxy.FS_hashes)) {
proxy.FS_hashes = [];
}
proxy.FS_hashes.push(localStorage.FS_hash);
}
if (typeof(cb) === "function") { cb(); }
};

@ -77,6 +77,13 @@ define([
return;
}
rpc.send('RESET', channels, function (e, response) {
if (e) {
return void cb(e);
}
if (!response.length) {
console.log(response);
return void cb('INVALID_RESPONSE');
}
cb(e, response[0]);
});
};
@ -121,6 +128,63 @@ define([
});
};
// Update the limit value for all the users and return the limit for your publicKey
exp.updatePinLimits = function (cb) {
rpc.send('UPDATE_LIMITS', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && typeof(response[0]) === "number") {
cb (void 0, response[0], response[1], response[2]);
} else {
cb('INVALID_RESPONSE');
}
});
};
// Get the storage limit associated with your publicKey
exp.getLimit = function (cb) {
rpc.send('GET_LIMIT', undefined, function (e, response) {
if (e) { return void cb(e); }
if (response && response.length && typeof(response[0]) === "number") {
cb (void 0, response[0], response[1], response[2]);
} else {
cb('INVALID_RESPONSE');
}
});
};
exp.uploadComplete = function (cb) {
rpc.send('UPLOAD_COMPLETE', null, function (e, res) {
if (e) { return void cb(e); }
var id = res[0];
if (typeof(id) !== 'string') {
return void cb('INVALID_ID');
}
cb(void 0, id);
});
};
exp.uploadStatus = function (size, cb) {
if (typeof(size) !== 'number') {
return void window.setTimeout(function () {
cb('INVALID_SIZE');
});
}
rpc.send('UPLOAD_STATUS', size, function (e, res) {
if (e) { return void cb(e); }
var pending = res[0];
if (typeof(pending) !== 'boolean') {
return void cb('INVALID_RESPONSE');
}
cb(void 0, pending);
});
};
exp.uploadCancel = function (cb) {
rpc.send('UPLOAD_CANCEL', void 0, function (e) {
if (e) { return void cb(e); }
cb();
});
};
cb(e, exp);
});
};

@ -129,6 +129,24 @@ types of messages:
return sendMsg(ctx, data, cb);
};
send.unauthenticated = function (type, msg, cb) {
if (!ctx.connected) {
return void window.setTimeout(function () {
cb('DISCONNECTED');
});
}
// construct an unsigned message
var data = [null, edPublicKey, null, type, msg];
if (ctx.cookie && ctx.cookie.join) {
data[2] = ctx.cookie.join('|');
} else {
data[2] = ctx.cookie;
}
return sendMsg(ctx, data, cb);
};
network.on('message', function (msg) {
onMsg(ctx, msg);
});

@ -0,0 +1,75 @@
define([], function () {
var out = function () { };
out.passed = out.failed = out;
if (window.location.hash.indexOf("?test=test") > -1) {
var cpt = window.__CRYPTPAD_TEST__ = {
data: [],
getData: function () {
var data = JSON.stringify(cpt.data);
cpt.data = [];
return data;
}
};
// jshint -W103
var errProto = (new Error()).__proto__;
var doLog = function (o) {
var s;
if (typeof(o) === 'object' && o.__proto__ === errProto) {
s = JSON.stringify([ o.message, o.stack ]);
} else if (typeof(s) !== 'string') {
try {
s = JSON.stringify(o);
} catch (e) {
s = String(o);
}
}
var out = [s];
try { throw new Error(); } catch (e) { out.push(e.stack.split('\n')[3]); }
cpt.data.push({ type: 'log', val: out.join('') });
};
window.console._error = window.console.error;
window.console._log = window.console.log;
window.console.error = function (e) { window.console._error(e); doLog(e); };
window.console.log = function (l) { window.console._log(l); doLog(l); };
window.onerror = function (msg, url, lineNo, columnNo, e) {
cpt.data.push({
type: 'report',
val: 'failed',
error: {
message: e ? e.message : msg,
stack: e ? e.stack : (url + ":" + lineNo)
}
});
};
require.onError = function (e) {
cpt.data.push({
type: 'report',
val: 'failed',
error: { message: e.message, stack: e.stack }
});
};
out = function (f) { f(); };
out.testing = true;
out.passed = function () {
cpt.data.push({
type: 'report',
val: 'passed'
});
};
out.failed = function (reason) {
var e;
try { throw new Error(reason); } catch (err) { e = err; }
cpt.data.push({
type: 'report',
val: 'failed',
error: { message: e.message, stack: e.stack }
});
};
} else {
out.testing = false;
}
return out;
});

@ -500,8 +500,12 @@ define([
var todo = function (e, overLimit) {
if (e) { return void console.error("Unable to get the pinned usage"); }
if (overLimit) {
var message = Messages.pinLimitReachedAlert;
if (ApiConfig.noSubscriptionButton === true) {
message = Messages.pinLimitReachedAlertNoAccounts;
}
$limit.show().click(function () {
Cryptpad.alert(Messages.pinLimitReachedAlert, null, true);
Cryptpad.alert(message, null, true);
});
}
};

@ -10,7 +10,7 @@ define([
constants: {},
};
var SPINNER_DISAPPEAR_TIME = 3000;
var SPINNER_DISAPPEAR_TIME = 1000;
// Toolbar parts
var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar';
@ -33,6 +33,7 @@ define([
var LIMIT_CLS = Bar.constants.lag = 'cryptpad-limit';
var TITLE_CLS = Bar.constants.title = "cryptpad-title";
var NEWPAD_CLS = Bar.constants.newpad = "cryptpad-newpad";
var UPGRADE_CLS = Bar.constants.upgrade = "cryptpad-upgrade";
// User admin menu
var USERADMIN_CLS = Bar.constants.user = 'cryptpad-user-dropdown';
@ -70,6 +71,7 @@ define([
var $userContainer = $('<span>', {
'class': USER_CLS
}).appendTo($topContainer);
$('<button>', {'class': UPGRADE_CLS + ' buttonSuccess'}).hide().appendTo($userContainer);
$('<span>', {'class': SPINNER_CLS}).hide().appendTo($userContainer);
$('<span>', {'class': STATE_CLS}).hide().appendTo($userContainer);
$('<span>', {'class': LAG_CLS}).hide().appendTo($userContainer);
@ -367,7 +369,7 @@ define([
return "Loading share button";
};
var createFileShare = function () {
var createFileShare = function (toolbar) {
if (!window.location.hash) {
throw new Error("Unable to display the share button: hash required in the URL");
}
@ -380,6 +382,7 @@ define([
if (success) { Cryptpad.log(Messages.shareSuccess); }
});
toolbar.$leftside.append($button);
return $button;
};
@ -594,7 +597,6 @@ define([
'class': 'synced fa fa-check',
title: Messages.synced
}).appendTo($spin);
toolbar.$userAdmin.prepend($spin);
if (config.realtime) {
config.realtime.onPatch(ks(toolbar, config));
config.realtime.onMessage(ks(toolbar, config, true));
@ -615,8 +617,12 @@ define([
var todo = function (e, overLimit) {
if (e) { return void console.error("Unable to get the pinned usage"); }
if (overLimit) {
var key = 'pinLimitReachedAlert';
if (ApiConfig.noSubscriptionButton === true) {
key = 'pinLimitReachedAlertNoAccounts';
}
$limit.show().click(function () {
Cryptpad.alert(Messages.pinLimitReachedAlert, null, true);
Cryptpad.alert(Messages._getKey(key, [encodeURIComponent(window.location.hostname)]), null, true);
});
}
};
@ -630,6 +636,8 @@ define([
var pads_options = [];
Config.availablePadTypes.forEach(function (p) {
if (p === 'drive') { return; }
if (!Cryptpad.isLoggedIn() && Config.registeredOnlyTypes &&
Config.registeredOnlyTypes.indexOf(p) !== -1) { return; }
pads_options.push({
tag: 'a',
attributes: {
@ -691,6 +699,33 @@ define([
return $userAdmin;
};
var createUpgrade = function (toolbar) {
if (ApiConfig.removeDonateButton) { return; }
if (Cryptpad.account.plan) { return; }
var text;
var feedback;
var url;
if (ApiConfig.allowSubscriptions && Cryptpad.isLoggedIn()) {
text = Messages.upgradeAccount;
feedback = "UPGRADE_ACCOUNT";
url = Cryptpad.upgradeURL;
} else {
text = Messages.supportCryptpad;
feedback = "SUPPORT_CRYPTPAD";
url = Cryptpad.donateURL;
}
var $upgrade = toolbar.$top.find('.' + UPGRADE_CLS).attr({
'title': Messages.supportCryptpad
}).text(text).show()
.click(function () {
Cryptpad.feedback(feedback);
window.open(url,'_blank');
});
return $upgrade;
};
// Events
var initClickEvents = function (toolbar, config) {
var removeDropdowns = function () {
@ -848,11 +883,11 @@ define([
tb['spinner'] = createSpinner;
tb['state'] = createState;
tb['limit'] = createLimit;
tb['upgrade'] = createUpgrade;
tb['newpad'] = createNewPad;
tb['useradmin'] = createUserAdmin;
var addElement = function (arr, additionnalCfg, init) {
var addElement = toolbar.addElement = function (arr, additionnalCfg, init) {
if (typeof additionnalCfg === "object") { $.extend(true, config, additionnalCfg); }
arr.forEach(function (el) {
if (typeof el !== "string" || !el.trim()) { return; }

@ -61,9 +61,7 @@ define([
if (!isFile(element)) { return false; }
var parsed = Cryptpad.parsePadUrl(element);
if (!parsed) { return false; }
var hash = parsed.hash;
var pHash = Cryptpad.parseHash(hash);
if (pHash && !pHash.mode) { return; }
var pHash = parsed.hashData;
return pHash && pHash.mode === 'view';
};
@ -541,6 +539,7 @@ define([
// ADD
var add = exp.add = function (data, path) {
if (!Cryptpad.isLoggedIn()) { return; }
if (!data || typeof(data) !== "object") { return; }
var href = data.href;
var name = data.title;
@ -576,7 +575,10 @@ define([
atime: +new Date(),
ctime: +new Date()
}, function (err) {
if (err) { return void cb(err); }
if (err) {
logError(err);
return void cb(err);
}
parentEl[fileName] = href;
var newPath = filePath.slice();
newPath.push(fileName);
@ -598,6 +600,18 @@ define([
// FORGET (move with href not path)
exp.forget = function (href) {
if (!Cryptpad.isLoggedIn()) {
// delete permanently
var data = getFileData(href);
if (data) {
var i = find([FILES_DATA]).indexOf(data);
if (i !== -1) {
exp.removePadAttribute(href);
spliceFileData(i);
}
}
return;
}
var paths = findFile(href);
move(paths, [TRASH]);
};
@ -605,7 +619,7 @@ define([
// DELETE
// Permanently delete multiple files at once using a list of paths
// NOTE: We have to be careful when removing elements from arrays (trash root, unsorted or template)
var removePadAttribute = function (f) {
var removePadAttribute = exp.removePadAttribute = function (f) {
if (typeof(f) !== 'string') {
console.error("Can't find pad attribute for an undefined pad");
return;
@ -621,7 +635,7 @@ define([
};
var checkDeletedFiles = function () {
// Nothing in FILES_DATA for workgroups
if (workgroup) { return; }
if (workgroup || !Cryptpad.isLoggedIn()) { return; }
var filesList = getFiles([ROOT, 'hrefArray', TRASH]);
var toRemove = [];
@ -656,6 +670,23 @@ define([
var hrefPaths = paths.filter(function(x) { return isPathIn(x, ['hrefArray']); });
var rootPaths = paths.filter(function(x) { return isPathIn(x, [ROOT]); });
var trashPaths = paths.filter(function(x) { return isPathIn(x, [TRASH]); });
var allFilesPaths = paths.filter(function(x) { return isPathIn(x, [FILES_DATA]); });
if (!Cryptpad.isLoggedIn()) {
var toSplice = [];
allFilesPaths.forEach(function (path) {
var el = find(path);
toSplice.push(el);
});
toSplice.forEach(function (el) {
var i = find([FILES_DATA]).indexOf(el);
if (i === -1) { return; }
removePadAttribute(el.href);
console.log(el.href);
spliceFileData(i);
});
return;
}
var hrefs = [];
hrefPaths.forEach(function (path) {
@ -884,7 +915,7 @@ define([
toClean.push(el);
return;
}
if (rootFiles.indexOf(el.href) === -1) {
if (Cryptpad.isLoggedIn() && rootFiles.indexOf(el.href) === -1) {
debug("An element in filesData was not in ROOT, TEMPLATE or TRASH.", el);
var name = el.title || NEW_FILE_NAME;
var newName = getAvailableName(root, name);

@ -154,6 +154,9 @@ span.fa-folder-open {
min-width: 30px;
cursor: pointer;
}
#tree #allfilesTree {
margin-top: 0;
}
#tree #searchContainer {
text-align: center;
padding: 5px 0;
@ -256,6 +259,9 @@ span.fa-folder-open {
margin-left: 10px;
float: right;
}
#content .info-box.noclose {
padding-right: 10px;
}
#content li {
cursor: default;
}
@ -444,7 +450,7 @@ span.fa-folder-open {
#driveToolbar {
background: #ddd;
color: #555;
height: 40px;
height: 30px;
display: flex;
flex-flow: row;
border-top: 1px solid #ccc;
@ -452,6 +458,7 @@ span.fa-folder-open {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 100;
box-sizing: content-box;
padding: 0 6px;
/* The container <div> - needed to position the dropdown content */
}
#driveToolbar .newPadContainer {
@ -459,24 +466,32 @@ span.fa-folder-open {
height: 100%;
}
#driveToolbar button {
height: 30px;
height: 24px;
font: 12px Ubuntu, Arial, sans-serif;
}
#driveToolbar button span {
font: 12px Ubuntu, Arial, sans-serif;
}
#driveToolbar button .fa,
#driveToolbar button.fa {
font-family: FontAwesome;
}
#driveToolbar button.element {
border-radius: 2px;
background: #888;
color: #eee;
font-size: 16px;
border: none;
font-size: 14px;
border: 1px solid #888;
font-weight: bold;
}
#driveToolbar button.element:hover {
box-shadow: 0px 0px 2px #000;
background: #777;
}
#driveToolbar button.new {
padding: 0 5px;
}
#driveToolbar .dropdown-bar {
margin: 5px 5px;
margin: 2px 2px;
line-height: 1em;
position: relative;
display: inline-block;
@ -510,7 +525,7 @@ span.fa-folder-open {
#driveToolbar .path {
display: inline-block;
height: 100%;
line-height: 40px;
line-height: 30px;
cursor: default;
width: auto;
overflow: hidden;
@ -532,7 +547,7 @@ span.fa-folder-open {
}
#driveToolbar #contextButtonsContainer {
float: right;
margin: 5px;
margin: 2px;
}
#driveToolbar #contextButtonsContainer button {
vertical-align: top;

@ -1,3 +1,5 @@
@import "../../customize.dist/src/less/variables.less";
@tree-bg: #fff;
@tree-fg: #000;
@tree-lines-col: #888;
@ -17,6 +19,8 @@
@toolbar-fg: #555;
@toolbar-border-col: #ccc;
@toolbar-button-bg: #888;
@toolbar-button-border: #888;
@toolbar-button-bg-hover: #777;
@toolbar-button-fg: #eee;
@toolbar-path-bg: #fff;
@toolbar-path-border: #888;
@ -194,6 +198,9 @@ span {
}
}
}
#allfilesTree {
margin-top: 0;
}
#searchContainer {
text-align: center;
padding: 5px 0;
@ -300,6 +307,9 @@ span {
margin-left: 10px;
float: right;
}
&.noclose {
padding-right: 10px;
}
}
li {
cursor: default;
@ -509,7 +519,7 @@ span {
#driveToolbar {
background: @toolbar-bg;
color: @toolbar-fg;
height: 40px;
height: 30px;
display: flex;
flex-flow: row;
border-top: 1px solid @toolbar-border-col;
@ -517,6 +527,7 @@ span {
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
z-index: 100;
box-sizing: content-box;
padding: 0 6px;
.newPadContainer {
display: inline-block;
@ -524,16 +535,23 @@ span {
}
button {
height: 30px;
height: 24px;
font: @toolbar-button-font;
span {
font: @toolbar-button-font;
}
.fa, &.fa {
font-family: FontAwesome;
}
&.element {
border-radius: 2px;
background: @toolbar-button-bg;
color: @toolbar-button-fg;
font-size: 16px;
border: none;
font-size: 14px;
border: 1px solid @toolbar-button-border;
font-weight: bold;
&:hover {
box-shadow: 0px 0px 2px #000;
background: @toolbar-button-bg-hover;
}
}
&.new {
@ -542,7 +560,7 @@ span {
}
/* The container <div> - needed to position the dropdown content */
.dropdown-bar {
margin: 5px 5px;
margin: 2px 2px;
line-height: 1em;
position: relative;
display: inline-block;
@ -577,7 +595,7 @@ span {
.path {
display: inline-block;
height: 100%;
line-height: 40px;
line-height: 30px;
cursor: default;
width: auto;
overflow: hidden;
@ -599,7 +617,7 @@ span {
}
#contextButtonsContainer {
float: right;
margin: 5px;
margin: 2px;
button {
vertical-align: top;
}

@ -33,7 +33,7 @@
<li><a tabindex="-1" data-icon="fa-file-code-o" class="newdoc own editable dropdown-item" data-type="code" data-localization="button_newcode">New code</a></li>
<li><a tabindex="-1" data-icon="fa-file-powerpoint-o" class="newdoc own editable dropdown-item" data-type="slide" data-localization="button_newslide">New slide</a></li>
<li><a tabindex="-1" data-icon="fa-calendar" class="newdoc own editable dropdown-item" data-type="poll" data-localization="button_newpoll">New poll</a></li>
<li><a tabindex="-1" data-icon="fa-calendar" class="newdoc own editable dropdown-item" data-type="whiteboard" data-localization="button_newwhiteboard">New whiteboard</a></li>
<li><a tabindex="-1" data-icon="fa-paint-brush" class="newdoc own editable dropdown-item" data-type="whiteboard" data-localization="button_newwhiteboard">New whiteboard</a></li>
</ul>
</div>
<div id="defaultContextMenu" class="contextMenu dropdown clearfix">

@ -37,7 +37,6 @@ define([
return JSONSortify(obj);
};
var LIMIT_REFRESH_RATE = 30000; // milliseconds
var E_OVER_LIMIT = 'E_OVER_LIMIT';
var SEARCH = "search";
@ -205,7 +204,6 @@ define([
var $trashTreeContextMenu = $iframe.find("#trashTreeContextMenu");
var $trashContextMenu = $iframe.find("#trashContextMenu");
// TOOLBAR
/* add a "change username" button */
@ -227,10 +225,19 @@ define([
if (AppConfig.enableTemplates) { displayedCategories.push(TEMPLATE); }
if (isWorkgroup()) { displayedCategories = [ROOT, TRASH, SEARCH]; }
if (!APP.loggedIn) {
displayedCategories = [FILES_DATA];
currentPath = [FILES_DATA];
$tree.hide();
if (Object.keys(files.root).length && !proxy.anonymousAlert) {
Cryptpad.alert(Messages.fm_alert_anonymous, null, true);
proxy.anonymousAlert = true;
}
}
if (!APP.readOnly) {
setEditable(true);
}
var appStatus = {
isReady: true,
_onReady: [],
@ -371,6 +378,7 @@ define([
e.stopPropagation();
});
// Arrow keys to modify the selection
$(ifrw).keydown(function (e) {
var $searchBar = $tree.find('#searchInput');
if ($searchBar.is(':focus') && $searchBar.val()) { return; }
@ -381,6 +389,7 @@ define([
if (e.ctrlKey) { ev.ctrlKey = true; }
if (e.shiftKey) { ev.shiftKey = true; }
var click = function (el) {
if (!el) { return; }
module.onElementClick(ev, $(el));
};
@ -402,6 +411,7 @@ define([
// [Left, Up, Right, Down]
if ([37, 38, 39, 40].indexOf(e.which) === -1) { return; }
e.preventDefault();
var $selection = $content.find('.element.selected');
if ($selection.length === 0) { return void click($elements.first()[0]); }
@ -523,6 +533,10 @@ define([
placeholder: name,
value: name
}).data('path', path);
// Stop propagation on keydown to avoid issues with arrow keys
$input.on('keydown', function (e) { e.stopPropagation(); });
$input.on('keyup', function (e) {
if (e.which === 13) {
removeInput(true);
@ -597,7 +611,6 @@ define([
}
hasFolder = true;
hide.push($menu.find('a.open_ro'));
// TODO: folder properties in the future?
hide.push($menu.find('a.properties'));
}
// If we're in the trash, hide restore and properties for non-root elements
@ -711,6 +724,21 @@ define([
updatePathSize();
};
var scrollTo = function ($element) {
// Current scroll position
var st = $content.scrollTop();
// Block height
var h = $content.height();
// Current top position of the element relative to the scroll position
var pos = Math.round($element.offset().top - $content.position().top);
// Element height
var eh = $element.outerHeight();
// New scroll value
var v = st + pos + eh - h;
// If the element is completely visile, don't change the scroll position
if (pos+eh <= h && pos >= 0) { return; }
$content.scrollTop(v);
};
// Add the "selected" class to the "li" corresponding to the clicked element
var onElementClick = module.onElementClick = function (e, $element) {
// If "Ctrl" is pressed, do not remove the current selection
@ -726,6 +754,7 @@ define([
log(Messages.fm_selectError);
return;
}
scrollTo($element);
// Add the selected class to the clicked / right-clicked element
// Remove the class if it already has it
// If ctrlKey, add to the selection
@ -766,6 +795,7 @@ define([
var displayMenu = function (e, $menu) {
$menu.css({ display: "block" });
if (APP.mobile()) { return; }
var h = $menu.outerHeight();
var w = $menu.outerWidth();
var wH = window.innerHeight;
@ -1079,7 +1109,7 @@ define([
var type = Messages.type[hrefData.type] || hrefData.type;
var $title = $('<span>', {'class': 'title listElement', title: data.title}).text(data.title);
var $type = $('<span>', {'class': 'type listElement', title: type}).text(type);
if (hrefData.hash && Cryptpad.parseHash(hrefData.hash) && Cryptpad.parseHash(hrefData.hash).mode === 'view') {
if (hrefData.hashData && hrefData.hashData.mode === 'view') {
$type.append(' (' + Messages.readonly+ ')');
}
var $adate = $('<span>', {'class': 'atime listElement', title: getDate(data.atime)}).text(getDate(data.atime));
@ -1265,6 +1295,12 @@ define([
default:
msg = undefined;
}
if (!APP.loggedIn) {
msg = Messages.fm_info_anonymous;
$box.html(msg);
$box.addClass('noclose');
return $box;
}
if (!msg || Cryptpad.getLSAttribute('hide-info-' + path[0]) === '1') {
$box.hide();
} else {
@ -1334,6 +1370,10 @@ define([
}
AppConfig.availablePadTypes.forEach(function (type) {
if (type === 'drive') { return; }
if (!Cryptpad.isLoggedIn() && AppConfig.registeredOnlyTypes &&
AppConfig.registeredOnlyTypes.indexOf(type) !== -1) {
return;
}
var attributes = {
'class': 'newdoc',
'data-type': type,
@ -1361,8 +1401,11 @@ define([
// Handlers
if (isInRoot) {
var onCreated = function (err, info) {
if (err && err === E_OVER_LIMIT) {
return void Cryptpad.alert(Messages.pinLimitDrive, null, true);
if (err) {
if (err === E_OVER_LIMIT) {
return void Cryptpad.alert(Messages.pinLimitDrive, null, true);
}
return void Cryptpad.alert(Messages.fm_error_cantPin);
}
module.newFolder = info.newPath;
refresh();
@ -1372,6 +1415,12 @@ define([
});
$block.find('a.newdoc').click(function () {
var type = $(this).attr('data-type') || 'pad';
// We can't create a hash for files before uploading the file
if (type === 'file') { // TODO: remove when filename are gone?
sessionStorage[Cryptpad.newPadPathKey] = filesOp.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
window.open('/' + type + '/');
return;
}
var name = Cryptpad.getDefaultName({type: type});
filesOp.addFile(currentPath, name, type, onCreated);
});
@ -1732,7 +1781,8 @@ define([
if (parentPath) {
$a = $('<a>').text(Messages.fm_openParent).click(function (e) {
e.preventDefault();
parentPath.pop();
if (filesOp.isInTrashRoot(parentPath)) { parentPath = [TRASH]; }
else { parentPath.pop(); }
module.displayDirectory(parentPath);
});
}
@ -1784,13 +1834,20 @@ define([
displayDirectory(parentPath, true);
return;
}
if (!isSearch) { delete APP.Search.oldLocation; }
module.resetTree();
// in history mode we want to focus the version number input
if (!history.isHistoryMode) { $tree.find('#searchInput').focus(); }
$tree.find('#searchInput')[0].selectionStart = getSearchCursor();
$tree.find('#searchInput')[0].selectionEnd = getSearchCursor();
if (displayedCategories.indexOf(SEARCH) !== -1) {
// in history mode we want to focus the version number input
if (!history.isHistoryMode && !APP.mobile()) {
var st = $tree.scrollTop() || 0;
$tree.find('#searchInput').focus();
$tree.scrollTop(st);
}
$tree.find('#searchInput')[0].selectionStart = getSearchCursor();
$tree.find('#searchInput')[0].selectionEnd = getSearchCursor();
}
if (!isWorkgroup()) {
setLastOpenedFolder(path);
@ -2062,6 +2119,7 @@ define([
if (!filesOp.comparePath(newLocation, currentPath.slice())) { displayDirectory(newLocation); }
return;
}
if (APP.mobile()) { return; }
search.to = window.setTimeout(function () {
if (!isInSearchTmp) { search.oldLocation = currentPath.slice(); }
var newLocation = [SEARCH, $input.val()];
@ -2074,12 +2132,14 @@ define([
};
module.resetTree = function () {
var s = $tree.scrollTop() || 0;
$tree.html('');
if (displayedCategories.indexOf(SEARCH) !== -1) { createSearch($tree); }
if (displayedCategories.indexOf(ROOT) !== -1) { createTree($tree, [ROOT]); }
if (displayedCategories.indexOf(TEMPLATE) !== -1) { createTemplate($tree, [TEMPLATE]); }
if (displayedCategories.indexOf(FILES_DATA) !== -1) { createAllFiles($tree, [FILES_DATA]); }
if (displayedCategories.indexOf(TRASH) !== -1) { createTrash($tree, [TRASH]); }
$tree.scrollTop(s);
};
module.hideMenu = function () {
@ -2124,9 +2184,9 @@ define([
var getReadOnlyUrl = APP.getRO = function (href) {
if (!filesOp.isFile(href)) { return; }
var i = href.indexOf('#') + 1;
var hash = href.slice(i);
var parsed = Cryptpad.parsePadUrl(href);
var base = href.slice(0, i);
var hrefsecret = Cryptpad.getSecrets(hash);
var hrefsecret = Cryptpad.getSecrets(parsed.type, parsed.hash);
if (!hrefsecret.keys) { return; }
var viewHash = Cryptpad.getViewHashFromKeys(hrefsecret.channel, hrefsecret.keys);
return base + viewHash;
@ -2156,15 +2216,18 @@ define([
.click(function () { $(this).select(); })
.appendTo($d);
}
var roLink = ro ? base + el : getReadOnlyUrl(base + el);
if (roLink) {
$('<label>', {'for': 'propROLink'}).text(Messages.viewShare).appendTo($d);
$('<input>', {'id': 'propROLink', 'readonly': 'readonly', 'value': roLink})
.click(function () { $(this).select(); })
.appendTo($d);
var parsed = Cryptpad.parsePadUrl(el);
if (parsed.hashData && parsed.hashData.type === 'pad') {
var roLink = ro ? base + el : getReadOnlyUrl(base + el);
if (roLink) {
$('<label>', {'for': 'propROLink'}).text(Messages.viewShare).appendTo($d);
$('<input>', {'id': 'propROLink', 'readonly': 'readonly', 'value': roLink})
.click(function () { $(this).select(); })
.appendTo($d);
}
}
if (Cryptpad.isLoggedIn() && AppConfig.enablePinning) {
if (APP.loggedIn && AppConfig.enablePinning) {
// check the size of this file...
Cryptpad.getFileSize(el, function (e, bytes) {
if (e) {
@ -2279,6 +2342,18 @@ define([
else if ($(this).hasClass('delete')) {
var pathsList = [];
paths.forEach(function (p) { pathsList.push(p.path); });
if (!APP.loggedIn) {
var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]);
if (paths.length === 1) {
msg = Messages.fm_removePermanentlyDialog;
}
Cryptpad.confirm(msg, function(res) {
$(ifrw).focus();
if (!res) { return; }
filesOp.delete(pathsList, refresh);
});
return;
}
moveElements(pathsList, [TRASH], false, refresh);
}
else if ($(this).hasClass("properties")) {
@ -2297,9 +2372,12 @@ define([
e.stopPropagation();
var path = $(this).data('path');
var onCreated = function (err, info) {
if (err && err === E_OVER_LIMIT) {
if (err === E_OVER_LIMIT) {
return void Cryptpad.alert(Messages.pinLimitDrive, null, true);
}
if (err) {
return void Cryptpad.alert(Messages.fm_error_cantPin);
}
module.newFolder = info.newPath;
refresh();
};
@ -2383,12 +2461,12 @@ define([
$appContainer.on('mouseup', function (e) {
if (sel.down) { return; }
if (e.which !== 1) { return ; }
removeSelected(e);
module.hideMenu(e);
//removeSelected(e);
});
$appContainer.on('click', function (e) {
if (e.which !== 1) { return ; }
removeInput();
module.hideMenu(e);
hideNewButton();
});
$appContainer.on('drag drop', function (e) {
@ -2401,7 +2479,9 @@ define([
$appContainer.on('keydown', function (e) {
// "Del"
if (e.which === 46) {
if (filesOp.isPathIn(currentPath, [FILES_DATA])) { return; } // We can't remove elements directly from filesData
if (filesOp.isPathIn(currentPath, [FILES_DATA]) && APP.loggedIn) {
return; // We can't remove elements directly from filesData
}
var $selected = $iframe.find('.selected');
if (!$selected.length) { return; }
var paths = [];
@ -2411,7 +2491,7 @@ define([
paths.push($(elmt).data('path'));
});
// If we are in the trash or anon pad or if we are holding the "shift" key, delete permanently,
if (isTrash || e.shiftKey) {
if (!APP.loggedIn || isTrash || e.shiftKey) {
var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]);
if (paths.length === 1) {
msg = Messages.fm_removePermanentlyDialog;
@ -2529,7 +2609,6 @@ define([
setEditable(!bool);
if (!bool && update) {
history.onLeaveHistory();
//init(); //TODO real proxy here
}
};
@ -2566,7 +2645,7 @@ define([
// don't initialize until the store is ready.
Cryptpad.ready(function () {
Cryptpad.reportAppUsage();
if (!Cryptpad.isLoggedIn()) { Cryptpad.feedback('ANONYMOUS_DRIVE'); }
if (!APP.loggedIn) { Cryptpad.feedback('ANONYMOUS_DRIVE'); }
APP.$bar = $iframe.find('#toolbar');
var storeObj = Cryptpad.getStore().getProxy && Cryptpad.getStore().getProxy().proxy ? Cryptpad.getStore().getProxy() : undefined;
@ -2578,7 +2657,7 @@ define([
}
var hash = window.location.hash.slice(1) || Cryptpad.getUserHash() || localStorage.FS_hash;
var secret = Cryptpad.getSecrets(hash);
var secret = Cryptpad.getSecrets('drive', hash);
var readOnly = APP.readOnly = secret.keys && !secret.keys.editKeyStr;
var listmapConfig = module.config = {
@ -2643,74 +2722,30 @@ define([
}
/* add the usage */
if (AppConfig.enablePinLimit) {
var todo = function (err, state, data) {
$leftside.html('');
if (!data) {
return void window.setTimeout(function () {
Cryptpad.isOverPinLimit(todo);
}, LIMIT_REFRESH_RATE);
}
var usage = data.usage;
var limit = data.limit;
var unit = Messages.MB;
var $limit = $('<span>', {'class': 'cryptpad-drive-limit'}).appendTo($leftside);
var quota = usage/limit;
var width = Math.floor(Math.min(quota, 1)*$limit.width());
var $usage = $('<span>', {'class': 'usage'}).css('width', width+'px');
if (quota >= 0.8) {
$('<button>', {
'class': 'upgrade buttonSuccess',
title: Messages.upgradeTitle
}).text(Messages.upgrade).click(function () {
// TODO
}).appendTo($leftside);
}
if (quota < 0.8) { $usage.addClass('normal'); }
else if (quota < 1) { $usage.addClass('warning'); }
else { $usage.addClass('above'); }
var $text = $('<span>', {'class': 'usageText'});
$text.text(usage + ' / ' + limit + ' ' + unit);
$limit.append($usage).append($text);
window.setTimeout(function () {
Cryptpad.isOverPinLimit(todo);
}, LIMIT_REFRESH_RATE);
};
Cryptpad.isOverPinLimit(todo);
}
Cryptpad.createUsageBar(function (err, $limitContainer) {
if (err) { return void logError(err); }
$leftside.html('');
$leftside.append($limitContainer);
});
/* add a history button */
var histConfig = {};
histConfig.onRender = function (val) {
if (typeof val === "undefined") { return; }
try {
var histConfig = {
onLocal: function () {
proxy.drive = history.currentObj.drive;
},
onRemote: function () {},
setHistory: setHistory,
applyVal: function (val) {
var obj = JSON.parse(val || '{}');
history.currentObj = obj;
history.onEnterHistory(obj);
} catch (e) {
// Probably a parse error
logError(e);
}
};
histConfig.onClose = function () {
// Close button clicked
setHistory(false, true);
};
histConfig.onRevert = function () {
// Revert button clicked
setHistory(false, false);
proxy.drive = history.currentObj.drive;
};
histConfig.onReady = function () {
// Called when the history is loaded and the UI displayed
setHistory(true);
},
$toolbar: APP.$bar,
href: window.location.origin + window.location.pathname + APP.hash
};
histConfig.$toolbar = APP.$bar;
histConfig.href = window.location.origin + window.location.pathname + APP.hash;
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist);
if (!APP.loggedIn) { $hist.hide(); }
if (!readOnly && !APP.loggedIn) {
var $backupButton = Cryptpad.createButton('', true).removeClass('fa').removeClass('fa-question');

@ -8,6 +8,9 @@ define([
], function ($, Config, Realtime, Crypto, TextPatcher, Cryptpad) {
var secret = Cryptpad.getSecrets();
if (!secret.keys) {
secret.keys = secret.key;
}
var module = window.APP = {
TextPatcher: TextPatcher
@ -19,8 +22,9 @@ define([
var config = module.config = {
initialState: '',
websocketURL: Config.websocketURL,
validateKey: secret.keys.validateKey || undefined,
channel: secret.channel,
crypto: Crypto.createEncryptor(secret.key),
crypto: Crypto.createEncryptor(secret.keys),
};
var setEditable = function (bool) { $textarea.attr('disabled', !bool); };
@ -29,7 +33,8 @@ define([
setEditable(false);
config.onInit = function (info) {
window.location.hash = info.channel + secret.key;
var editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
Cryptpad.replaceHash(editHash);
$(window).on('hashchange', function() {
window.location.reload();
});

@ -7,6 +7,24 @@ define([
var plainChunkLength = 128 * 1024;
var cypherChunkLength = 131088;
var computeEncryptedSize = function (bytes, meta) {
var metasize = Nacl.util.decodeUTF8(JSON.stringify(meta)).length;
var chunks = Math.ceil(bytes / plainChunkLength);
return metasize + 18 + (chunks * 16) + bytes;
};
var encodePrefix = function (p) {
return [
65280, // 255 << 8
255,
].map(function (n, i) {
return (p & n) >> ((1 - i) * 8);
});
};
var decodePrefix = function (A) {
return (A[0] << 8) | A[1];
};
var slice = function (A) {
return Array.prototype.slice.call(A);
};
@ -40,85 +58,146 @@ define([
};
var joinChunks = function (chunks) {
return new Uint8Array(chunks.reduce(function (A, B) {
return slice(A).concat(slice(B));
}, []));
return new Blob(chunks);
};
var concatBuffer = function (a, b) { // TODO make this not so ugly
return new Uint8Array(slice(a).concat(slice(b)));
};
var padChunk = function (A) {
var padding;
if (A.length === plainChunkLength) { return A; }
if (A.length < plainChunkLength) {
padding = new Array(plainChunkLength - A.length).fill(32);
return A.concat(padding);
var fetchMetadata = 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.setRequestHeader('Range', 'bytes=0-1');
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); }
var res = new Uint8Array(xhr.response);
var size = decodePrefix(res);
var xhr2 = new XMLHttpRequest();
xhr2.open("GET", src, true);
xhr2.setRequestHeader('Range', 'bytes=2-' + (size + 2));
xhr2.responseType = 'arraybuffer';
xhr2.onload = function () {
if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); }
var res2 = new Uint8Array(xhr2.response);
var all = concatBuffer(res, res2);
CB(void 0, all);
};
xhr2.send(null);
};
xhr.send(null);
};
var decryptMetadata = function (u8, key) {
var prefix = u8.subarray(0, 2);
var metadataLength = decodePrefix(prefix);
var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
var metaChunk = Nacl.secretbox.open(metaBox, createNonce(), key);
try {
return JSON.parse(Nacl.util.encodeUTF8(metaChunk));
}
if (A.length > plainChunkLength) {
// how many times larger is it?
var chunks = Math.ceil(A.length / plainChunkLength);
padding = new Array((plainChunkLength * chunks) - A.length).fill(32);
return A.concat(padding);
catch (e) { return null; }
};
var fetchDecryptedMetadata = function (src, key, cb) {
if (typeof(src) !== 'string') {
return window.setTimeout(function () {
cb('NO_SOURCE');
});
}
fetchMetadata(src, function (e, buffer) {
if (e) { return cb(e); }
cb(void 0, decryptMetadata(buffer, key));
});
};
var decrypt = function (u8, key, cb) {
var decrypt = function (u8, key, done, progress) {
var MAX = u8.length;
var _progress = function (offset) {
if (typeof(progress) !== 'function') { return; }
progress(Math.min(1, offset / MAX));
};
var nonce = createNonce();
var i = 0;
var takeChunk = function () {
var start = i * cypherChunkLength;
var end = start + cypherChunkLength;
i++;
var box = new Uint8Array(u8.subarray(start, end));
// decrypt the chunk
var plaintext = Nacl.secretbox.open(box, nonce, key);
increment(nonce);
return plaintext;
};
var buffer = '';
var prefix = u8.subarray(0, 2);
var metadataLength = decodePrefix(prefix);
var res = {
metadata: undefined,
};
// decrypt metadata
var chunk;
for (; !res.metadata && i * cypherChunkLength < u8.length;) {
chunk = takeChunk();
buffer += Nacl.util.encodeUTF8(chunk);
try {
res.metadata = JSON.parse(buffer);
//console.log(res.metadata);
} catch (e) {
console.log('buffering another chunk for metadata');
}
var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
var metaChunk = Nacl.secretbox.open(metaBox, nonce, key);
increment(nonce);
try {
res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk));
} catch (e) {
return window.setTimeout(function () {
done('E_METADATA_DECRYPTION');
});
}
if (!res.metadata) {
return void setTimeout(function () {
cb('NO_METADATA');
done('NO_METADATA');
});
}
var fail = function () {
cb("DECRYPTION_ERROR");
var takeChunk = function (cb) {
var start = i * cypherChunkLength + 2 + metadataLength;
var end = start + cypherChunkLength;
i++;
var box = new Uint8Array(u8.subarray(start, end));
// decrypt the chunk
var plaintext = Nacl.secretbox.open(box, nonce, key);
increment(nonce);
if (!plaintext) { return cb('DECRYPTION_ERROR'); }
_progress(end);
cb(void 0, plaintext);
};
var chunks = [];
// decrypt file contents
for (;i * cypherChunkLength < u8.length;) {
chunk = takeChunk();
if (!chunk) {
return window.setTimeout(fail);
}
chunks.push(chunk);
}
// send chunks
res.content = joinChunks(chunks);
var again = function () {
takeChunk(function (e, plaintext) {
if (e) {
return setTimeout(function () {
done(e);
});
}
if (plaintext) {
if (i * cypherChunkLength < u8.length) { // not done
chunks.push(plaintext);
return setTimeout(again);
}
chunks.push(plaintext);
res.content = joinChunks(chunks);
return done(void 0, res);
}
done('UNEXPECTED_ENDING');
});
};
cb(void 0, res);
again();
};
// metadata
@ -130,41 +209,32 @@ define([
var metaBuffer = Array.prototype.slice
.call(Nacl.util.decodeUTF8(JSON.stringify(metadata)));
var plaintext = new Uint8Array(padChunk(metaBuffer));
var plaintext = new Uint8Array(metaBuffer);
var j = 0;
var i = 0;
/*
0: metadata
1: u8
2: done
*/
var state = 0;
var next = function (cb) {
if (state === 2) { return void cb(); }
var start;
var end;
var part;
var box;
if (state === 0) { // metadata...
start = j * plainChunkLength;
end = start + plainChunkLength;
part = plaintext.subarray(start, end);
part = new Uint8Array(plaintext);
box = Nacl.secretbox(part, nonce, key);
increment(nonce);
j++;
// metadata is done
if (j * plainChunkLength >= plaintext.length) {
return void cb(state++, box);
if (box.length > 65535) {
return void cb('METADATA_TOO_LARGE');
}
var prefixed = new Uint8Array(encodePrefix(box.length)
.concat(slice(box)));
state++;
return void cb(state, box);
return void cb(void 0, prefixed);
}
// encrypt the rest of the file...
@ -179,7 +249,7 @@ define([
// regular data is done
if (i * plainChunkLength >= u8.length) { state = 2; }
return void cb(state, box);
return void cb(void 0, box);
};
return next;
@ -189,5 +259,9 @@ define([
decrypt: decrypt,
encrypt: encrypt,
joinChunks: joinChunks,
computeEncryptedSize: computeEncryptedSize,
decryptMetadata: decryptMetadata,
fetchMetadata: fetchMetadata,
fetchDecryptedMetadata: fetchDecryptedMetadata,
};
});

@ -0,0 +1,118 @@
html,
body {
margin: 0px;
height: 100%;
}
.cryptpad-toolbar {
margin-bottom: 1px;
padding: 0px;
display: inline-block;
}
#file,
#dl {
display: block;
height: 100%;
width: 100%;
border: 2px solid black;
}
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
#upload-form,
#download-form {
padding: 0px;
margin: 0px;
position: relative;
width: 50vh;
height: 50vh;
display: block;
margin: 50px auto;
max-width: 80vw;
}
#upload-form label,
#download-form label {
line-height: calc(50vh - 20px);
text-align: center;
position: relative;
padding: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 50vh;
box-sizing: border-box;
}
.hovering {
background-color: rgba(255, 0, 115, 0.5) !important;
}
.block {
display: block;
}
.hidden {
display: none;
}
.inputfile + label {
border: 2px solid black;
background-color: rgba(50, 50, 50, 0.1);
display: block;
}
.inputfile:focus + label,
.inputfile + label:hover {
background-color: rgba(50, 50, 50, 0.3);
}
#progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
transition: width 500ms;
width: 0%;
max-width: 100%;
max-height: 100%;
background-color: rgba(255, 0, 115, 0.75);
z-index: 10000;
display: block;
}
#status {
display: none;
width: 80vw;
margin-top: 50px;
margin-left: 10vw;
border: 1px solid black;
border-collapse: collapse;
}
#status tr:nth-child(1) {
background-color: #ccc;
border: 1px solid #999;
}
#status tr:nth-child(1) td {
text-align: center;
}
#status td {
border-left: 1px solid #BBB;
border-right: 1px solid #BBB;
padding: 0 10px;
}
#status .upProgress {
width: 200px;
position: relative;
text-align: center;
}
#status .progressContainer {
position: absolute;
width: 0px;
left: 5px;
top: 1px;
bottom: 1px;
background-color: rgba(0, 0, 255, 0.3);
}
#status .upCancel {
text-align: center;
}
#status .fa.cancel {
color: #ff0073;
}

@ -0,0 +1,124 @@
@import "../../customize.dist/src/less/variables.less";
@import "../../customize.dist/src/less/mixins.less";
@button-border: 2px;
html, body {
margin: 0px;
height: 100%;
}
.cryptpad-toolbar {
margin-bottom: 1px;
padding: 0px;
display: inline-block;
}
#file, #dl {
display: block;
height: 100%;
width: 100%;
border: @button-border solid black;
}
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
#upload-form, #download-form {
padding: 0px;
margin: 0px;
position: relative;
width: 50vh;
height: 50vh;
display: block;
margin: 50px auto;
max-width: 80vw;
label {
line-height: ~"calc(50vh - 20px)";
text-align: center;
position: relative;
padding: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 50vh;
box-sizing: border-box;
}
}
.hovering {
background-color: rgba(255, 0, 115, 0.5) !important;
}
.block {
display: block;
}
.hidden {
display: none;
}
.inputfile + label {
border: 2px solid black;
background-color: rgba(50, 50, 50, .10);
display: block;
}
.inputfile:focus + label,
.inputfile + label:hover {
background-color: rgba(50, 50, 50, 0.30);
}
#progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
transition: width 500ms;
width: 0%;
max-width: 100%;
max-height: 100%;
background-color: rgba(255, 0, 115, 0.75);
z-index: 10000;
display: block;
}
#status {
display: none;
width: 80vw;
margin-top: 50px;
margin-left: 10vw;
border: 1px solid black;
border-collapse: collapse;
tr:nth-child(1) {
background-color: #ccc;
border: 1px solid #999;
td { text-align: center; }
}
td {
border-left: 1px solid #BBB;
border-right: 1px solid #BBB;
padding: 0 10px;
}
.upProgress {
width: 200px;
position: relative;
text-align: center;
}
.progressContainer {
position: absolute;
width: 0px;
left: 5px;
top: 1px; bottom: 1px;
background-color: rgba(0,0,255,0.3);
}
.upCancel { text-align: center; }
.fa.cancel {
color: rgb(255, 0, 115);
}
}

@ -5,52 +5,31 @@
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
<style>
html, body {
margin: 0px;
}
.cryptpad-toolbar {
margin-bottom: 1px;
padding: 0px;
display: inline-block;
}
#file {
display: block;
height: 300px;
width: 300px;
border: 2px solid black;
margin: 50px;
}
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.inputfile + label {
border: 2px solid black;
display: block;
height: 500px;
width: 500px;
background-color: rgba(50, 50, 50, .10);
margin: 50px;
}
.inputfile:focus + label,
.inputfile + label:hover {
background-color: rgba(50, 50, 50, 0.30);
}
</style>
<link rel="stylesheet" href="/file/file.css">
<link rel="stylesheet" href="/customize/main.css">
</head>
<body>
<div id="toolbar" class="toolbar-container"></div>
<div id="upload-form" style="display: none;">
<input type="file" name="file" id="file" class="inputfile" />
<label for="file">Choose a file</label>
<label for="file" class="block unselectable" data-localization-title="upload_choose"
data-localization="upload_choose"></label>
</div>
<div id="download-form" style="display: none;">
<input type="button" name="dl" id="dl" class="inputfile" />
<label for="dl" class="block unselectable" data-localization-title="download_button"
data-localization="download_button"></label>
<span class="block" id="progress"></span>
</div>
<table id="status" style="display: none;">
<tr>
<td data-localization="upload_name">File name</td>
<td data-localization="upload_size">Size</td>
<td data-localization="upload_progress">Progress</td>
<td data-localization="cancel">Cancel</td>
</tr>
</table>
<div id="feedback" class="block hidden">
</div>
</body>
</html>

@ -2,7 +2,7 @@ define([
'jquery',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/common/toolbar.js',
'/common/toolbar2.js',
'/common/cryptpad-common.js',
'/common/visible.js',
'/common/notify.js',
@ -20,103 +20,130 @@ define([
var ifrw = $('#pad-iframe')[0].contentWindow;
var $iframe = $('#pad-iframe').contents();
var $form = $iframe.find('#upload-form');
var $dlform = $iframe.find('#download-form');
var $label = $form.find('label');
var $table = $iframe.find('#status');
var $progress = $iframe.find('#progress');
$iframe.find('body').on('dragover', function (e) { e.preventDefault(); });
$iframe.find('body').on('drop', function (e) { e.preventDefault(); });
Cryptpad.addLoadingScreen();
var fetch = function (src, cb) {
var xhr = new XMLHttpRequest();
xhr.open("GET", src, true);
xhr.responseType = "arraybuffer";
xhr.onload = function () {
return void cb(void 0, new Uint8Array(xhr.response));
};
xhr.send(null);
};
var Title;
var myFile;
var myDataType;
var upload = function (blob, metadata) {
var queue = {
queue: [],
inProgress: false
};
var uid = function () {
return 'file-' + String(Math.random()).substring(2);
};
var upload = function (blob, metadata, id) {
console.log(metadata);
if (queue.inProgress) { return; }
queue.inProgress = true;
var $row = $table.find('tr[id="'+id+'"]');
$row.find('.upCancel').html('-');
var $pv = $row.find('.progressValue');
var $pb = $row.find('.progressContainer');
var updateProgress = function (progressValue) {
$pv.text(Math.round(progressValue*100)/100 + '%');
$pb.css({
width: (progressValue/100)*188+'px'
});
};
var u8 = new Uint8Array(blob);
var key = Nacl.randomBytes(32);
var next = FileCrypto.encrypt(u8, metadata, key);
var chunks = [];
var estimate = FileCrypto.computeEncryptedSize(blob.byteLength, metadata);
var sendChunk = function (box, cb) {
var enc = Nacl.util.encodeBase64(box);
chunks.push(box);
Cryptpad.rpc.send('UPLOAD', enc, function (e, msg) {
Cryptpad.rpc.send.unauthenticated('UPLOAD', enc, function (e, msg) {
console.log(box);
cb(e, msg);
});
};
var again = function (state, box) {
switch (state) {
case 0:
sendChunk(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
break;
case 1:
sendChunk(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
break;
case 2:
sendChunk(box, function (e) {
if (e) { return console.error(e); }
Cryptpad.rpc.send('UPLOAD_COMPLETE', '', function (e, res) {
if (e) { return void console.error(e); }
var id = res[0];
var uri = ['', 'blob', id.slice(0,2), id].join('/');
console.log("encrypted blob is now available as %s", uri);
window.location.hash = [
'',
2,
Cryptpad.hexToBase64(id).replace(/\//g, '-'),
Nacl.util.encodeBase64(key).replace(/\//g, '-'),
''
].join('/');
APP.$form.hide();
var newU8 = FileCrypto.joinChunks(chunks);
FileCrypto.decrypt(newU8, key, function (e, res) {
if (e) { return console.error(e); }
var title = document.title = res.metadata.name;
myFile = res.content;
myDataType = res.metadata.type;
var defaultName = Cryptpad.getDefaultName(Cryptpad.parsePadUrl(window.location.href));
APP.updateTitle(title || defaultName);
});
});
});
break;
default:
throw new Error("E_INVAL_STATE");
var actual = 0;
var again = function (err, box) {
if (err) { throw new Error(err); }
if (box) {
actual += box.length;
var progressValue = (actual / estimate * 100);
updateProgress(progressValue);
return void sendChunk(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
}
if (actual !== estimate) {
console.error('Estimated size does not match actual size');
}
// if not box then done
Cryptpad.uploadComplete(function (e, id) {
if (e) { return void console.error(e); }
var uri = ['', 'blob', id.slice(0,2), id].join('/');
console.log("encrypted blob is now available as %s", uri);
var b64Key = Nacl.util.encodeBase64(key);
Cryptpad.replaceHash(Cryptpad.getFileHashFromKeys(id, b64Key));
APP.toolbar.addElement(['fileshare'], {});
var title = document.title = metadata.name;
myFile = blob;
myDataType = metadata.type;
var defaultName = Cryptpad.getDefaultName(Cryptpad.parsePadUrl(window.location.href));
Title.updateTitle(title || defaultName);
APP.toolbar.title.show();
console.log(title);
Cryptpad.alert(Messages._getKey('upload_success', [title]));
queue.inProgress = false;
queue.next();
});
};
Cryptpad.rpc.send('UPLOAD_STATUS', '', function (e, pending) {
Cryptpad.uploadStatus(estimate, function (e, pending) {
if (e) {
queue.inProgress = false;
queue.next();
if (e === 'TOO_LARGE') {
return void Cryptpad.alert(Messages.upload_tooLarge);
}
if (e === 'NOT_ENOUGH_SPACE') {
return void Cryptpad.alert(Messages.upload_notEnoughSpace);
}
console.error(e);
return void Cryptpad.alert("something went wrong");
return void Cryptpad.alert(Messages.upload_serverError);
}
if (pending[0]) {
return void Cryptpad.confirm('upload pending, abort?', function (yes) {
if (pending) {
// TODO keep this message in case of pending files in another window?
return void Cryptpad.confirm(Messages.upload_uploadPending, function (yes) {
if (!yes) { return; }
Cryptpad.rpc.send('UPLOAD_CANCEL', '', function (e, res) {
if (e) { return void console.error(e); }
Cryptpad.uploadCancel(function (e, res) {
if (e) {
return void console.error(e);
}
console.log(res);
next(again);
});
});
}
@ -124,13 +151,52 @@ define([
});
};
var prettySize = function (bytes) {
var kB = Cryptpad.bytesToKilobytes(bytes);
if (kB < 1024) { return kB + Messages.KB; }
var mB = Cryptpad.bytesToMegabytes(bytes);
return mB + Messages.MB;
};
queue.next = function () {
if (queue.queue.length === 0) { return; }
if (queue.inProgress) { return; }
var file = queue.queue.shift();
upload(file.blob, file.metadata, file.id);
};
queue.push = function (obj) {
var id = uid();
obj.id = id;
queue.queue.push(obj);
$table.show();
var estimate = FileCrypto.computeEncryptedSize(obj.blob.byteLength, obj.metadata);
var $progressBar = $('<div>', {'class':'progressContainer'});
var $progressValue = $('<span>', {'class':'progressValue'}).text(Messages.upload_pending);
var $tr = $('<tr>', {id: id}).appendTo($table);
var $cancel = $('<span>', {'class': 'cancel fa fa-times'}).click(function () {
queue.queue = queue.queue.filter(function (el) { return el.id !== id; });
$cancel.remove();
$tr.find('.upCancel').text('-');
$tr.find('.progressValue').text(Messages.upload_cancelled);
});
$('<td>').text(obj.metadata.name).appendTo($tr);
$('<td>').text(prettySize(estimate)).appendTo($tr);
$('<td>', {'class': 'upProgress'}).append($progressBar).append($progressValue).appendTo($tr);
$('<td>', {'class': 'upCancel'}).append($cancel).appendTo($tr);
queue.next();
};
var uploadMode = false;
var andThen = function () {
var $bar = $iframe.find('.toolbar-container');
// Test hash:
// #/2/K6xWU-LT9BJHCQcDCT-DcQ/TBo77200c0e-FdldQFcnQx4Y/
var secret;
var hexFileName;
if (window.location.hash) {
@ -141,9 +207,6 @@ define([
uploadMode = true;
}
var parsed = Cryptpad.parsePadUrl(window.location.href);
var defaultName = Cryptpad.getDefaultName(parsed);
var getTitle = function () {
var pad = Cryptpad.getRelativeHref(window.location.href);
var fo = Cryptpad.getStore().getProxy().fo;
@ -151,107 +214,154 @@ define([
return data ? data.title : undefined;
};
var updateTitle = APP.updateTitle = function (newTitle) {
Cryptpad.renamePad(newTitle, function (err, data) {
if (err) {
console.log("Couldn't set pad title");
console.error(err);
return;
}
document.title = newTitle;
$bar.find('.' + Toolbar.constants.title).find('span.title').text(data);
$bar.find('.' + Toolbar.constants.title).find('input').val(data);
});
};
var suggestName = function () {
return document.title || getTitle() || '';
};
var renameCb = function (err, title) {
document.title = title;
};
var exportFile = function () {
var suggestion = document.title;
Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion), function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; }
var blob = new Blob([myFile], {type: myDataType});
saveAs(blob, filename);
});
var filename = Cryptpad.fixFileName(document.title);
if (!(typeof(filename) === 'string' && filename)) { return; }
var blob = new Blob([myFile], {type: myDataType});
saveAs(blob, filename);
};
var displayed = ['useradmin', 'newpad', 'limit'];
Title = Cryptpad.createTitle({}, function(){}, Cryptpad);
var displayed = ['title', 'useradmin', 'newpad', 'limit', 'upgrade'];
if (secret && hexFileName) {
displayed.push('share');
displayed.push('fileshare');
}
var configTb = {
displayed: displayed,
ifrw: ifrw,
common: Cryptpad,
title: {
onRename: renameCb,
defaultName: defaultName,
suggestName: suggestName
},
share: {
secret: secret,
channel: hexFileName
},
hideDisplayName: true
title: Title.getTitleConfig(),
hideDisplayName: true,
$container: $bar
};
Toolbar.create($bar, null, null, null, null, configTb);
var $rightside = $bar.find('.' + Toolbar.constants.rightside);
var toolbar = APP.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
if (uploadMode) { toolbar.title.hide(); }
var $rightside = toolbar.$rightside;
var $export = Cryptpad.createButton('export', true, {}, exportFile);
$rightside.append($export);
updateTitle(Cryptpad.initialName || getTitle() || defaultName);
Title.updateTitle(Cryptpad.initialName || getTitle() || Title.defaultTitle);
if (!uploadMode) {
$dlform.show();
var src = Cryptpad.getBlobPathFromHex(hexFileName);
return fetch(src, function (e, u8) {
// now decrypt the u8
if (e) { return window.alert('error'); }
var cryptKey = secret.keys && secret.keys.fileKeyStr;
var key = Nacl.util.decodeBase64(cryptKey);
FileCrypto.decrypt(u8, key, function (e, data) {
if (e) {
Cryptpad.removeLoadingScreen();
return console.error(e);
}
console.log(data);
var title = document.title = data.metadata.name;
myFile = data.content;
myDataType = data.metadata.type;
updateTitle(title || defaultName);
Cryptpad.removeLoadingScreen();
var cryptKey = secret.keys && secret.keys.fileKeyStr;
var key = Nacl.util.decodeBase64(cryptKey);
FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) {
if (e) { return void console.error(e); }
var title = document.title = metadata.name;
Title.updateTitle(title || Title.defaultTitle);
Cryptpad.removeLoadingScreen();
var decrypting = false;
$dlform.find('#dl, #progress').click(function () {
if (decrypting) { return; }
if (myFile) { return void exportFile(); }
decrypting = true;
return Cryptpad.fetch(src, function (e, u8) {
if (e) {
decrypting = false;
return void Cryptpad.alert(e);
}
// now decrypt the u8
if (!u8 || !u8.length) {
return void Cryptpad.errorLoadingScreen(e);
}
FileCrypto.decrypt(u8, key, function (e, data) {
if (e) {
decrypting = false;
return console.error(e);
}
console.log(data);
var title = document.title = data.metadata.name;
myFile = data.content;
myDataType = data.metadata.type;
Title.updateTitle(title || Title.defaultTitle);
exportFile();
decrypting = false;
}, function (progress) {
var p = progress * 100 +'%';
$progress.width(p);
console.error(progress);
});
});
});
});
return;
}
if (!Cryptpad.isLoggedIn()) {
return Cryptpad.alert("You must be logged in to upload files");
return Cryptpad.alert(Messages.upload_mustLogin, function () {
if (sessionStorage) {
sessionStorage.redirectTo = window.location.href;
}
window.location.href = '/login/';
});
}
var $form = APP.$form = $iframe.find('#upload-form');
$form.css({
display: 'block',
});
$form.find("#file").on('change', function (e) {
var file = e.target.files[0];
var handleFile = function (file) {
console.log(file);
var reader = new FileReader();
reader.onloadend = function () {
upload(this.result, {
name: file.name,
type: file.type,
queue.push({
blob: this.result,
metadata: {
name: file.name,
type: file.type,
}
});
};
reader.readAsArrayBuffer(file);
};
$form.find("#file").on('change', function (e) {
var file = e.target.files[0];
handleFile(file);
});
var counter = 0;
$label
.on('dragenter', function (e) {
e.preventDefault();
e.stopPropagation();
counter++;
$label.addClass('hovering');
})
.on('dragleave', function (e) {
e.preventDefault();
e.stopPropagation();
counter--;
if (counter <= 0) {
$label.removeClass('hovering');
}
});
$form
.on('drag dragstart dragend dragover drop dragenter dragleave', function (e) {
e.preventDefault();
e.stopPropagation();
})
.on('drop', function (e) {
e.stopPropagation();
var dropped = e.originalEvent.dataTransfer.files;
counter = 0;
$label.removeClass('hovering');
handleFile(dropped[0]);
});
// we're in upload mode

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html class="cp pad">
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<link rel="icon" type="image/png"
href="/customize/main-favicon.png"
data-main-favicon="/customize/main-favicon.png"
data-alt-favicon="/customize/alt-favicon.png"
id="favicon" />
<link rel="stylesheet" href="/customize/main.css" />
</head>
<body>

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

@ -18,6 +18,7 @@
</a>
</span>
<span id="user-menu" class="right dropdown-bar"></span>
<span id="language-selector" class="right dropdown-bar"></span>
<span class="right">
<a href="/about.html" data-localization="about">About</a>
@ -61,6 +62,10 @@
<input type="text" id="name" name="name" class="form-control" data-localization-placeholder="login_username" autofocus>
<input type="password" id="password" name="password" class="form-control" data-localization-placeholder="login_password">
<button class="btn btn-primary login first" data-localization="login_login"></button>
<div class="extra">
<p data-localization="login_notRegistered"></p>
<button id="register" class="btn btn-success register first" data-localization="login_register"></button>
</div>
</div>
</div>
</div>

@ -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/';
});
});
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);
window.location.href = '/drive/';
});
});
break;
case 'INVAL_PASS':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalPass);
});
break;
default: // UNHANDLED ERROR
Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
}
return;
}
switch (err) {
case 'NO_SUCH_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser);
});
break;
case 'INVAL_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalUser);
});
break;
case 'INVAL_PASS':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalPass);
});
break;
default: // UNHANDLED ERROR
Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
}
});
});
});
}, 0);
}, 0);
}, 100);
});
$('#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/';
});
});
});

@ -16,6 +16,8 @@
}
media-tag * {
max-width: 100%;
margin: auto;
display: block;
}
</style>
</head>

@ -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.document).on('decryption', function (e) {
var decrypted = e.originalEvent;
var metadata = decrypted.metadata;
window.onMediaMetadata = function (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();

@ -4,6 +4,7 @@
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
<script data-bootload="main.js" data-main="/common/boot.js" src="/bower_components/requirejs/require.js"></script>
<link rel="icon" type="image/png"

@ -12,6 +12,7 @@
}
#cke_1_top {
overflow: visible;
padding: 0 6px;
}
#cke_1_toolbox {
display: inline-block;

@ -11,14 +11,11 @@ define([
'/bower_components/textpatcher/TextPatcher.js',
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/common/visible.js',
'/common/notify.js',
'/pad/links.js',
'/bower_components/file-saver/FileSaver.min.js',
'/bower_components/diff-dom/diffDOM.js'
], function ($, Crypto, realtimeInput, Hyperjson,
Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget,
Visible, Notify, Links) {
Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) {
var saveAs = window.saveAs;
var Messages = Cryptpad.Messages;
@ -105,8 +102,6 @@ define([
editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor));
editor.on('instanceReady', function () {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
var parsedHash = Cryptpad.parsePadUrl(window.location.href);
var defaultName = Cryptpad.getDefaultName(parsedHash);
var isHistoryMode = false;
@ -277,7 +272,10 @@ define([
};
var initializing = true;
var Title;
var UserList;
var Metadata;
var getHeadingText = function () {
var text;
@ -290,14 +288,6 @@ define([
})) { return text; }
};
var suggestName = function (fallback) {
if (document.title === defaultName) {
return getHeadingText() || fallback || "";
} else {
return document.title || getHeadingText() || defaultName;
}
};
var DD = new DiffDom(diffOptions);
// apply patches, and try not to lose the cursor in the process!
@ -316,11 +306,11 @@ define([
hjson[3] = {
metadata: {
users: UserList.userData,
defaultTitle: defaultName
defaultTitle: Title.defaultTitle
}
};
if (!initializing) {
hjson[3].metadata.title = document.title;
hjson[3].metadata.title = Title.title;
} else if (Cryptpad.initialName && !hjson[3].metadata.title) {
hjson[3].metadata.title = Cryptpad.initialName;
}
@ -369,68 +359,6 @@ define([
}
};
var updateTitle = function (newTitle) {
if (newTitle === document.title) { return; }
// Change the title now, and set it back to the old value if there is an error
var oldTitle = document.title;
document.title = newTitle;
Cryptpad.renamePad(newTitle, function (err, data) {
if (err) {
console.log("Couldn't set pad title");
console.error(err);
document.title = oldTitle;
return;
}
document.title = data;
$bar.find('.' + Toolbar.constants.title).find('span.title').text(data);
$bar.find('.' + Toolbar.constants.title).find('input').val(data);
});
};
var updateDefaultTitle = function (defaultTitle) {
defaultName = defaultTitle;
$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName);
};
var updateMetadata = function(shjson) {
// Extract the user list (metadata) from the hyperjson
if (!shjson || typeof (shjson) !== "string") { updateTitle(defaultName); return; }
var hjson = JSON.parse(shjson);
var peerMetadata = hjson[3];
var titleUpdated = false;
if (peerMetadata && peerMetadata.metadata) {
if (peerMetadata.metadata.users) {
var userData = peerMetadata.metadata.users;
// Update the local user data
UserList.addToUserData(userData);
}
if (peerMetadata.metadata.defaultTitle) {
updateDefaultTitle(peerMetadata.metadata.defaultTitle);
}
if (typeof peerMetadata.metadata.title !== "undefined") {
updateTitle(peerMetadata.metadata.title || defaultName);
titleUpdated = true;
}
}
if (!titleUpdated) {
updateTitle(defaultName);
}
};
var unnotify = function () {
if (module.tabNotification &&
typeof(module.tabNotification.cancel) === 'function') {
module.tabNotification.cancel();
}
};
var notify = function () {
if (Visible.isSupported() && !Visible.currently()) {
unnotify();
module.tabNotification = Notify.tab(1000, 10);
}
};
realtimeOptions.onRemote = function () {
if (initializing) { return; }
if (isHistoryMode) { return; }
@ -443,7 +371,7 @@ define([
cursor.update();
// Update the user list (metadata) from the hyperjson
updateMetadata(shjson);
Metadata.update(shjson);
var newInner = JSON.parse(shjson);
var newSInner;
@ -488,7 +416,7 @@ define([
// Notify only when the content has changed, not when someone has joined/left
var oldSInner = stringify(JSON.parse(oldShjson)[2]);
if (newSInner && newSInner !== oldSInner) {
notify();
Cryptpad.notify();
}
};
@ -502,7 +430,7 @@ define([
var exportFile = function () {
var html = getHTML();
var suggestion = suggestName('cryptpad-document');
var suggestion = Title.suggestTitle('cryptpad-document');
Cryptpad.prompt(Messages.exportPrompt,
Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
if (!(typeof(filename) === 'string' && filename)) { return; }
@ -516,27 +444,22 @@ define([
realtimeOptions.onLocal();
};
var renameCb = function (err, title) {
if (err) { return; }
document.title = title;
editor.fire('change');
};
realtimeOptions.onInit = function (info) {
UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad);
var titleCfg = { getHeadingText: getHeadingText };
Title = Cryptpad.createTitle(titleCfg, realtimeOptions.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,
@ -546,6 +469,8 @@ define([
};
toolbar = info.realtime.toolbar = Toolbar.create(configTb);
Title.setToolbar(toolbar);
var $rightside = toolbar.$rightside;
var editHash;
@ -574,31 +499,13 @@ define([
}
/* add a history button */
var histConfig = {};
histConfig.onRender = function (val) {
if (typeof val === "undefined") { return; }
try {
applyHjson(val || '["BODY",{},[]]');
} catch (e) {
// Probably a parse error
console.error(e);
}
var histConfig = {
onLocal: realtimeOptions.onLocal,
onRemote: realtimeOptions.onRemote,
setHistory: setHistory,
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
$toolbar: $bar
};
histConfig.onClose = function () {
// Close button clicked
setHistory(false, true);
};
histConfig.onRevert = function () {
// Revert button clicked
setHistory(false, false);
realtimeOptions.onLocal();
realtimeOptions.onRemote();
};
histConfig.onReady = function () {
// Called when the history is loaded and the UI displayed
setHistory(true);
};
histConfig.$toolbar = $bar;
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
$rightside.append($hist);
@ -666,13 +573,7 @@ define([
applyHjson(shjson);
// Update the user list (metadata) from the hyperjson
updateMetadata(shjson);
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
}
Metadata.update(shjson);
if (!readOnly) {
var shjson2 = stringifyDOM(inner);
@ -686,7 +587,7 @@ define([
}
}
} else {
updateTitle(Cryptpad.initialName || defaultName);
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
documentBody.innerHTML = Messages.initialState;
}

@ -8,17 +8,13 @@ define([
'/bower_components/hyperjson/hyperjson.js',
'render.js',
'/common/toolbar2.js',
'/common/visible.js',
'/common/notify.js',
'/bower_components/file-saver/FileSaver.min.js'
], function ($, TextPatcher, Listmap, Crypto, Cryptpad, Cryptget, Hyperjson, Renderer, Toolbar, Visible, Notify) {
], function ($, TextPatcher, Listmap, Crypto, Cryptpad, Cryptget, Hyperjson, Renderer, Toolbar) {
var Messages = Cryptpad.Messages;
$(function () {
var unlockHTML = '<i class="fa fa-unlock" aria-hidden="true"></i>';
var lockHTML = '<i class="fa fa-lock" aria-hidden="true"></i>';
var HIDE_INTRODUCTION_TEXT = "hide_poll_text";
var defaultName;
@ -34,7 +30,6 @@ define([
if (!DEBUG) {
debug = function() {};
}
var error = console.error;
Cryptpad.addLoadingScreen();
var onConnectError = function () {
@ -103,12 +98,10 @@ define([
// Enable the checkboxes for the user's column (committed or not)
$('input[disabled="disabled"][data-rt-id^="' + id + '"]').removeAttr('disabled');
$('input[type="checkbox"][data-rt-id^="' + id + '"]').addClass('enabled');
$('[data-rt-id="' + id + '"] ~ .edit').css('visibility', 'hidden');
$('.lock[data-rt-id="' + id + '"]').html(unlockHTML);
$('.lock[data-rt-id="' + id + '"]').addClass('fa-unlock').removeClass('fa-lock').attr('title', Messages.poll_unlocked);
if (isOwnColumnCommitted()) { return; }
$('[data-rt-id^="' + id + '"]').closest('td').addClass("uncommitted");
$('td.uncommitted .remove, td.uncommitted .edit').css('visibility', 'hidden');
$('td.uncommitted .cover').addClass("uncommitted");
$('.uncommitted input[type="text"]').attr("placeholder", Messages.poll_userPlaceholder);
};
@ -121,8 +114,7 @@ define([
APP.editable.col.forEach(function (id) {
$('input[disabled="disabled"][data-rt-id^="' + id + '"]').removeAttr('disabled');
$('input[type="checkbox"][data-rt-id^="' + id + '"]').addClass('enabled');
$('span.edit[data-rt-id="' + id + '"]').css('visibility', 'hidden');
$('.lock[data-rt-id="' + id + '"]').html(unlockHTML);
$('.lock[data-rt-id="' + id + '"]').addClass('fa-unlock').removeClass('fa-lock').attr('title', Messages.poll_unlocked);
});
};
@ -186,20 +178,6 @@ define([
}
};
var unnotify = function () {
if (APP.tabNotification &&
typeof(APP.tabNotification.cancel) === 'function') {
APP.tabNotification.cancel();
}
};
var notify = function () {
if (Visible.isSupported() && !Visible.currently()) {
unnotify();
APP.tabNotification = Notify.tab(1000, 10);
}
};
/* Any time the realtime object changes, call this function */
var change = function (o, n, path, throttle, cb) {
if (path && !Cryptpad.isArray(path)) {
@ -228,7 +206,7 @@ define([
https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
*/
notify();
Cryptpad.notify();
var getFocus = function () {
var active = document.activeElement;
@ -293,7 +271,6 @@ define([
switch (type) {
case 'text':
debug("text[rt-id='%s'] [%s]", id, input.value);
if (!input.value) { return void debug("Hit enter?"); }
Render.setValue(object, id, input.value);
change(null, null, null, 50);
break;
@ -312,12 +289,26 @@ define([
}
};
var hideInputs = function (target, isKeyup) {
if (!isKeyup && $(target).is('[type="text"]')) {
return;
}
$('.lock[data-rt-id!="' + APP.userid + '"]').addClass('fa-lock').removeClass('fa-unlock').attr('title', Messages.poll_locked);
var $cells = APP.$table.find('thead td:not(.uncommitted), tbody td');
$cells.find('[type="text"][data-rt-id!="' + APP.userid + '"]').attr('disabled', true);
$('.edit[data-rt-id!="' + APP.userid + '"]').css('visibility', 'visible');
APP.editable.col = [APP.userid];
APP.editable.row = [];
};
/* Called whenever an event is fired on a span */
var handleSpan = function (span) {
var id = span.getAttribute('data-rt-id');
var type = Render.typeofId(id);
var isRemove = span.className && span.className.split(' ').indexOf('remove') !== -1;
var isEdit = span.className && span.className.split(' ').indexOf('edit') !== -1;
var isLock = span.className && span.className.split(' ').indexOf('lock') !== -1;
var isLocked = span.className && span.className.split(' ').indexOf('fa-lock') !== -1;
if (type === 'row') {
if (isRemove) {
Cryptpad.confirm(Messages.poll_removeOption, function (res) {
@ -327,6 +318,7 @@ define([
});
});
} else if (isEdit) {
hideInputs(span);
unlockRow(id, function () {
change(null, null, null, null, function() {
$('input[data-rt-id="' + id + '"]').focus();
@ -341,7 +333,8 @@ define([
change();
});
});
} else if (isEdit) {
} else if (isLock && isLocked) {
hideInputs(span);
unlockColumn(id, function () {
change(null, null, null, null, function() {
$('input[data-rt-id="' + id + '"]').focus();
@ -355,48 +348,34 @@ define([
}
};
var hideInputs = function (e, isKeyup) {
if (!isKeyup && $(e.target).is('[type="text"]')) {
return;
}
$('.lock[data-rt-id!="' + APP.userid + '"]').html(lockHTML);
var $cells = APP.$table.find('thead td:not(.uncommitted), tbody td');
$cells.find('[type="text"][data-rt-id!="' + APP.userid + '"]').attr('disabled', true);
$('.edit[data-rt-id!="' + APP.userid + '"]').css('visibility', 'visible');
APP.editable.col = [APP.userid];
APP.editable.row = [];
};
$(window).click(hideInputs);
var handleClick = function (e, isKeyup) {
e.stopPropagation();
if (!APP.ready) { return; }
var target = e && e.target;
if (isKeyup) {
debug("Keyup!");
}
if (!target) { return void debug("NO TARGET"); }
var nodeName = target && target.nodeName;
var shouldLock = $(target).hasClass('fa-unlock');
if (!$(target).parents('#table tbody').length || $(target).hasClass('edit')) {
if ((!$(target).parents('#table tbody').length && $(target).hasClass('lock'))) {
hideInputs(e);
}
switch (nodeName) {
case 'INPUT':
if (isKeyup && (e.keyCode === 13 || e.keyCode === 27)) {
hideInputs(e, isKeyup);
return;
hideInputs(target, isKeyup);
break;
}
handleInput(target);
break;
case 'SPAN':
//case 'LABEL':
if (shouldLock) {
break;
}
handleSpan(target);
break;
case undefined:
@ -442,43 +421,9 @@ define([
});
};
var Title;
var UserList;
var updateTitle = function (newTitle) {
if (newTitle === document.title) { return; }
// Change the title now, and set it back to the old value if there is an error
var oldTitle = document.title;
document.title = newTitle;
Cryptpad.renamePad(newTitle, function (err, data) {
if (err) {
debug("Couldn't set pad title");
error(err);
document.title = oldTitle;
return;
}
document.title = data;
APP.$bar.find('.' + Toolbar.constants.title).find('span.title').text(data);
APP.$bar.find('.' + Toolbar.constants.title).find('input').val(data);
});
};
var updateDefaultTitle = function (defaultTitle) {
defaultName = defaultTitle;
APP.$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName);
};
var renameCb = function (err, title) {
if (err) { return; }
document.title = title;
APP.proxy.info.title = title === defaultName ? "" : title;
};
var suggestName = function (fallback) {
if (document.title === defaultName) {
return fallback || "";
}
return document.title || defaultName || "";
};
var copyObject = function (obj) {
return JSON.parse(JSON.stringify(obj));
};
@ -510,7 +455,6 @@ var ready = function (info, userid, readOnly) {
var $table = APP.$table = $(Render.asHTML(displayedObj, null, colsOrder, readOnly));
APP.$createRow = $('#create-option').click(function () {
//console.error("BUTTON CLICKED! LOL");
Render.createRow(proxy, function (empty, id) {
change(null, null, null, null, function() {
$('.edit[data-rt-id="' + id + '"]').click();
@ -521,7 +465,7 @@ var ready = function (info, userid, readOnly) {
APP.$createCol = $('#create-user').click(function () {
Render.createColumn(proxy, function (empty, id) {
change(null, null, null, null, function() {
$('.edit[data-rt-id="' + id + '"]').click();
$('.lock[data-rt-id="' + id + '"]').click();
});
});
});
@ -550,15 +494,15 @@ var ready = function (info, userid, readOnly) {
// Title
if (APP.proxy.info.defaultTitle) {
updateDefaultTitle(APP.proxy.info.defaultTitle);
Title.updateDefaultTitle(APP.proxy.info.defaultTitle);
} else {
APP.proxy.info.defaultTitle = defaultName;
APP.proxy.info.defaultTitle = Title.defaultTitle;
}
if (Cryptpad.initialName && !APP.proxy.info.title) {
APP.proxy.info.title = Cryptpad.initialName;
updateTitle(Cryptpad.initialName);
Title.updateTitle(Cryptpad.initialName);
} else {
updateTitle(APP.proxy.info.title || defaultName);
Title.updateTitle(APP.proxy.info.title || Title.defaultTitle);
}
// Description
@ -583,11 +527,13 @@ var ready = function (info, userid, readOnly) {
.click(handleClick)
.on('keyup', function (e) { handleClick(e, true); });
$(window).click(hideInputs);
proxy
.on('change', ['info'], function (o, n, p) {
if (p[1] === 'title') {
updateTitle(n);
notify();
Title.updateTitle(n);
Cryptpad.notify();
} else if (p[1] === "userData") {
UserList.addToUserData(APP.proxy.info.userData);
} else if (p[1] === 'description') {
@ -602,7 +548,7 @@ var ready = function (info, userid, readOnly) {
el.selectionStart = selects[0];
el.selectionEnd = selects[1];
}
notify();
Cryptpad.notify();
}
debug("change: (%s, %s, [%s])", o, n, p.join(', '));
@ -612,13 +558,6 @@ var ready = function (info, userid, readOnly) {
UserList.addToUserData(APP.proxy.info.userData);
if (Visible.isSupported()) {
Visible.onChange(function (yes) {
if (yes) { unnotify(); }
});
}
APP.ready = true;
if (!proxy.published) {
publish(false);
@ -664,18 +603,19 @@ var create = function (info) {
};
UserList = Cryptpad.createUserList(info, onLocal, Cryptget, Cryptpad);
var onLocalTitle = function () {
APP.proxy.info.title = Title.isDefaultTitle() ? "" : Title.title;
};
Title = Cryptpad.createTitle({}, onLocalTitle, Cryptpad);
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,
@ -685,6 +625,8 @@ var create = function (info) {
};
APP.toolbar = Toolbar.create(configTb);
Title.setToolbar(APP.toolbar);
var $rightside = APP.toolbar.$rightside;
/* add a forget button */

@ -32,6 +32,9 @@ textarea[disabled] {
font: white;
border: 0px;
}
input[type="text"]::placeholder {
color: #666;
}
table#table {
margin: 0px;
}
@ -66,7 +69,7 @@ table#table {
#tableScroll {
overflow-y: hidden;
overflow-x: auto;
margin-left: calc(30% - 50px + 29px);
margin-left: calc(30% - 50px + 31px);
max-width: 70%;
width: auto;
display: inline-block;
@ -104,6 +107,9 @@ table {
tbody {
border: 1px solid #555;
}
tbody * {
box-sizing: border-box;
}
tbody tr {
text-align: center;
}
@ -260,23 +266,37 @@ div.realtime table input[type="text"] {
border: 1px solid #fff;
width: 80%;
}
form.realtime table span,
div.realtime table span {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
form.realtime table thead td,
div.realtime table thead td {
padding: 0px 5px;
background: #aaa;
border-radius: 20px 20px 0 0;
text-align: center;
}
form.realtime table thead td:nth-of-type(2),
div.realtime table thead td:nth-of-type(2) {
background: #999;
}
form.realtime table thead td:nth-of-type(2) .lock,
div.realtime table thead td:nth-of-type(2) .lock {
cursor: default;
}
form.realtime table thead td input[type="text"],
div.realtime table thead td input[type="text"] {
width: 100%;
box-sizing: border-box;
padding: 1px 5px;
}
form.realtime table thead td input[type="text"][disabled],
div.realtime table thead td input[type="text"][disabled] {
color: #000;
padding: 1px 5px;
border: none;
border: 1px solid transparent;
}
form.realtime table tbody .text-cell,
div.realtime table tbody .text-cell {
@ -296,9 +316,9 @@ div.realtime table tbody .text-cell .remove {
float: left;
margin: 0 0 0 10px;
}
form.realtime table tbody td label,
div.realtime table tbody td label {
border: 0.5px solid #555;
form.realtime table tbody tr:not(:first-child) td:not(:first-child) label,
div.realtime table tbody tr:not(:first-child) td:not(:first-child) label {
border-top: 1px solid #555;
}
form.realtime table .edit,
div.realtime table .edit {
@ -307,6 +327,13 @@ div.realtime table .edit {
float: left;
margin-left: 10px;
}
form.realtime table .lock,
div.realtime table .lock {
margin-left: calc(50% - 0.5em);
cursor: pointer;
width: 1em;
text-align: center;
}
form.realtime table .remove,
div.realtime table .remove {
float: right;

@ -2,7 +2,9 @@
@import "../../customize.dist/src/less/mixins.less";
@poll-th-bg: #aaa;
@poll-th-user-bg: #999;
@poll-td-bg: #aaa;
@poll-placeholder: #666;
@poll-border-color: #555;
@poll-cover-color: #000;
@poll-fg: #000;
@ -42,6 +44,13 @@ input[type="text"][disabled], textarea[disabled] {
font: white;
border: 0px;
}
// The placeholder color only seems to effect Safari when not set
input[type="text"]::placeholder {
color: @poll-placeholder;
}
table#table {
margin: 0px;
}
@ -75,7 +84,7 @@ table#table {
#tableScroll {
overflow-y: hidden;
overflow-x: auto;
margin-left: calc(~"30% - 50px + 29px");
margin-left: calc(~"30% - 50px + 31px");
max-width: 70%;
width: auto;
display: inline-block;
@ -118,6 +127,9 @@ table {
}
tbody {
border: 1px solid @poll-border-color;
* {
box-sizing: border-box;
}
tr {
text-align: center;
&:first-of-type th{
@ -282,20 +294,32 @@ form.realtime, div.realtime {
width: 80%;
}
}
span {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
thead {
td {
padding: 0px 5px;
background: @poll-th-bg;
border-radius: 20px 20px 0 0;
text-align: center;
//text-align: center;
&:nth-of-type(2) {
background: @poll-th-user-bg;
.lock {
cursor: default;
}
}
input {
&[type="text"] {
width: 100%;
box-sizing: border-box;
padding: 1px 5px;
&[disabled] {
color: @poll-fg;
padding: 1px 5px;
border: none;
border: 1px solid transparent;
}
}
}
@ -318,9 +342,11 @@ form.realtime, div.realtime {
margin: 0 0 0 10px;
}
}
td {
label {
border: .5px solid @poll-border-color;
tr:not(:first-child) {
td:not(:first-child) {
label {
border-top: 1px solid @poll-border-color;
}
}
}
}
@ -331,6 +357,12 @@ form.realtime, div.realtime {
margin-left: 10px;
}
.lock {
margin-left: ~"calc(50% - 0.5em)";
cursor: pointer;
width: 1em;
text-align: center;
}
.remove {
float: right;
margin-right: 10px;

@ -252,6 +252,7 @@ var Renderer = function (Cryptpad) {
var makeRemoveElement = Render.makeRemoveElement = function (id) {
return ['SPAN', {
'data-rt-id': id,
'title': Cryptpad.Messages.poll_remove,
class: 'remove',
}, ['✖']];
};
@ -259,6 +260,7 @@ var Renderer = function (Cryptpad) {
var makeEditElement = Render.makeEditElement = function (id) {
return ['SPAN', {
'data-rt-id': id,
'title': Cryptpad.Messages.poll_edit,
class: 'edit',
}, ['✐']];
};
@ -266,25 +268,18 @@ var Renderer = function (Cryptpad) {
var makeLockElement = Render.makeLockElement = function (id) {
return ['SPAN', {
'data-rt-id': id,
class: 'lock',
}, [['i', {
class: 'fa fa-lock',
'aria-hidden': true,
}, []]
]];
'title': Cryptpad.Messages.poll_locked,
class: 'lock fa fa-lock',
}, []];
};
var makeHeadingCell = Render.makeHeadingCell = function (cell, readOnly) {
if (!cell) { return ['TD', {}, []]; }
if (cell.type === 'text') {
var removeElement = makeRemoveElement(cell['data-rt-id']);
var editElement = makeEditElement(cell['data-rt-id']);
var lockElement = makeLockElement(cell['data-rt-id']);
var elements = [['INPUT', cell, []]];
if (!readOnly) {
elements.unshift(removeElement);
elements.unshift(lockElement);
elements.unshift(editElement);
elements.unshift(makeRemoveElement(cell['data-rt-id']));
elements.unshift(makeLockElement(cell['data-rt-id']));
}
return ['TD', {}, elements];
}
@ -321,12 +316,10 @@ var Renderer = function (Cryptpad) {
var makeBodyCell = Render.makeBodyCell = function (cell, readOnly) {
if (cell && cell.type === 'text') {
var removeElement = makeRemoveElement(cell['data-rt-id']);
var editElement = makeEditElement(cell['data-rt-id']);
var elements = [['INPUT', cell, []]];
if (!readOnly) {
elements.push(removeElement);
elements.push(editElement);
elements.push(makeRemoveElement(cell['data-rt-id']));
elements.push(makeEditElement(cell['data-rt-id']));
}
return ['TD', {}, [
['DIV', {class: 'text-cell'}, elements]

@ -17,6 +17,7 @@
</a>
</span>
<span id="user-menu" class="right dropdown-bar"></span>
<span id="language-selector" class="right dropdown-bar"></span>
<span class="right">
<a href="/about.html" data-localization="about">About</a>

@ -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;
if (err) {
switch (err) {
case 'NO_SUCH_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser);
});
break;
case 'INVAL_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalUser);
});
break;
case 'INVAL_PASS':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalPass);
});
break;
case 'ALREADY_REGISTERED':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.confirm(Messages.register_alreadyRegistered, function (yes) {
if (!yes) { return; }
proxy.login_name = uname;
if (!proxy[Cryptpad.displayNameKey]) {
proxy[Cryptpad.displayNameKey] = uname;
}
Cryptpad.eraseTempSessionValues();
logMeIn(result);
});
});
break;
default: // UNHANDLED ERROR
Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
}
return;
}
Cryptpad.eraseTempSessionValues();
if (shouldImport) {
sessionStorage.migrateAnonDrive = 1;
}
proxy.login_name = uname;
proxy[Cryptpad.displayNameKey] = uname;
sessionStorage.createReadme = 1;
logMeIn(result);
});
// setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up
window.setTimeout(function () {
Cryptpad.addLoadingScreen(Messages.login_hashing);
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password
window.setTimeout(function () {
Login.loginOrRegister(uname, passwd, true, function (err, result) {
var proxy = result.proxy;
if (err) {
switch (err) {
case 'NO_SUCH_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_noSuchUser);
});
break;
case 'INVAL_USER':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalUser);
});
break;
case 'INVAL_PASS':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.alert(Messages.login_invalPass);
});
break;
case 'ALREADY_REGISTERED':
Cryptpad.removeLoadingScreen(function () {
Cryptpad.confirm(Messages.register_alreadyRegistered, function (yes) {
if (!yes) { return; }
proxy.login_name = uname;
if (!proxy[Cryptpad.displayNameKey]) {
proxy[Cryptpad.displayNameKey] = uname;
}
Cryptpad.eraseTempSessionValues();
logMeIn(result);
});
});
break;
default: // UNHANDLED ERROR
Cryptpad.errorLoadingScreen(Messages.login_unhandledError);
}
return;
}
if (Test.testing) { return void 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);
});
});
});

@ -40,6 +40,9 @@
<span class="link right">
<a href="https://blog.cryptpad.fr/" data-localization="blog">Blog</a>
</span>
<span class="link right">
<button id="upgrade" class="upgrade btn buttonSuccess" style="display: none;"></button>
</span>
</div>
@ -97,7 +100,7 @@
<div class="col">
<ul class="list-unstyled">
<li class="title" data-localization="footer_contact"><li>
<li><a href="https://riot.im/app/#/room/!cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://riot.im/app/#/room/#cryptpad:matrix.org" target="_blank" rel="noopener noreferrer">Chat</a></li>
<li><a href="https://twitter.com/cryptpad" target="_blank" rel="noopener noreferrer">Twitter</a></li>
<li><a href="https://github.com/xwiki-labs/cryptpad" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a href="/contact.html">Email</a></li>
@ -105,7 +108,7 @@
</div>
</div>
</div>
<div class="version-footer">CryptPad v1.6.0 (Grootslang)</div>
<div class="version-footer">CryptPad v1.8.0 (Igopogo)</div>
</footer>
</body>

@ -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 = $('<span>', {'class': 'label'})
.text(Messages.settings_publicSigningKey + ':');
var $pubKey = $('<input>', {type: 'text', readonly: true})
.css({
width: '28em'
})
.val(publicKey);
.val(userHref);
$div.append('<br>').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 = $('<div>', { 'class': 'pinned-usage' })
.text(Messages.settings_usageTitle)
.append('<br>');
$('<button>', {
'class': 'btn btn-primary', // fa fa-hdd-o ?
})
.text(Messages.settings_usage)
.click(function () {
if (!(proxy.edPublic && proxy.edPrivate)) {
// suggest that they login/register
Cryptpad.alert(Messages.settings_pinningNotAvailable);
return;
}
Cryptpad.getPinnedUsage(function (e, bytes) {
if (e) {
Cryptpad.alert(Messages.settings_pinningError);
return;
}
Cryptpad.alert(Messages._getKey('settings_usageAmount', [Cryptpad.bytesToMegabytes(bytes)]));
});
})
.appendTo($div);
Cryptpad.createUsageBar(function (err, $bar) {
$div.find('.limit-container').remove();
$bar.find('.upgrade').addClass('btn btn-success');
$div.append($bar);
}, true);
return $div;
};
@ -334,10 +318,10 @@ define([
APP.$container.append(createLogoutEverywhere(obj));
}
APP.$container.append(createResetTips());
APP.$container.append(createUsageButton(obj));
APP.$container.append(createBackupDrive(obj));
APP.$container.append(createImportLocalPads(obj));
APP.$container.append(createResetDrive(obj));
APP.$container.append(createUsageButton(obj));
APP.$container.append(createUserFeedbackToggle(obj));
obj.proxy.on('change', [], refresh);
obj.proxy.on('remove', [], refresh);
@ -345,19 +329,6 @@ 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();

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

Loading…
Cancel
Save