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

pull/1/head
yflory 5 years ago
commit 97e45d91ee

@ -0,0 +1,45 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Where did it happen?**
Did the issue occur on CryptPad.fr or an instance hosted by a third-party?
If on another instance, please provide its full URL.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Browser (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. firefox, tor browser, chrome, safari, brave, edge, ???]
- variations [e.g. Firefox nightly, Firefox ESR, Chromium, Ungoogled chrome]
- Version [e.g. 22]
- Extensions installed (UBlock Origin, Passbolt, LibreJS]
- Browser tweaks [e.g. firefox "Enhanced Tracking Protection" strict/custom mode, tor browser "safer" security level, chrome incognito mode]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

2
.gitignore vendored

@ -20,4 +20,4 @@ block/
logs/ logs/
privileged.conf privileged.conf
config/config.js config/config.js
*yolo.sh

@ -1,67 +1,110 @@
/* /* globals module */
globals module
*/ /* DISCLAIMER:
var _domain = 'http://localhost:3000/';
There are two recommended methods of running a CryptPad instance:
// You can `kill -USR2` the node process and it will write out a heap dump. 1. Using a standalone nodejs server without HTTPS (suitable for local development)
// If your system doesn't support dumping, comment this out and install with 2. Using NGINX to serve static assets and to handle HTTPS for API server's websocket traffic
// `npm install --production`
// See: https://strongloop.github.io/strongloop.com/strongblog/how-to-heap-snapshots/
// to enable this feature, uncomment the line below: We do not officially recommend or support Apache, Docker, Kubernetes, Traefik, or any other configuration.
// require('heapdump'); Support requests for such setups should be directed to their authors.
// we prepend a space because every usage expects it If you're having difficulty difficulty configuring your instance
// requiring admins to preserve it is unnecessarily confusing we suggest that you join the project's IRC/Matrix channel.
var domain = ' ' + _domain;
// Content-Security-Policy If you don't have any difficulty configuring your instance and you'd like to
var baseCSP = [ support us for the work that went into making it pain-free we are quite happy
"default-src 'none'", to accept donations via our opencollective page: https://opencollective.com/cryptpad
"style-src 'unsafe-inline' 'self' " + domain,
"font-src 'self' data:" + domain,
/* child-src is used to restrict iframes to a set of allowed domains. */
* connect-src is used to restrict what domains can connect to the websocket. module.exports = {
/* CryptPad is designed to serve its content over two domains.
* Account passwords and cryptographic content is handled on the 'main' domain,
* while the user interface is loaded on a 'sandbox' domain
* which can only access information which the main domain willingly shares.
*
* In the event of an XSS vulnerability in the UI (that's bad)
* this system prevents attackers from gaining access to your account (that's good).
*
* Most problems with new instances are related to this system blocking access
* because of incorrectly configured sandboxes. If you only see a white screen
* when you try to load CryptPad, this is probably the cause.
*
* PLEASE READ THE FOLLOWING COMMENTS CAREFULLY.
*
*/
/* httpUnsafeOrigin is the URL that clients will enter to load your instance.
* Any other URL that somehow points to your instance is supposed to be blocked.
* The default provided below assumes you are loading CryptPad from a server
* which is running on the same machine, using port 3000.
*
* In a production instance this should be available ONLY over HTTPS
* using the default port for HTTPS (443) ie. https://cryptpad.fr
* In such a case this should be handled by NGINX, as documented in
* cryptpad/docs/example.nginx.conf (see the $main_domain variable)
* *
* it is recommended that you configure these fields to match the
* domain which will serve your CryptPad instance.
*/ */
"child-src blob: *", httpUnsafeOrigin: 'http://localhost:3000/',
// IE/Edge
"frame-src blob: *",
/* this allows connections over secure or insecure websockets /* httpSafeOrigin is the URL that is used for the 'sandbox' described above.
if you are deploying to production, you'll probably want to remove * If you're testing or developing with CryptPad on your local machine then
the ws://* directive, and change '*' to your domain * it is appropriate to leave this blank. The default behaviour is to serve
* the main domain over port 3000 and to serve the content over port 3001.
*
* This is not appropriate in a production environment where invasive networks
* may filter traffic going over abnormal ports.
* To correctly configure your production instance you must provide a URL
* with a different domain (a subdomain is sufficient).
* It will be used to load the UI in our 'sandbox' system.
*
* This value corresponds to the $sandbox_domain variable
* in the example nginx file.
*
* CUSTOMIZE AND UNCOMMENT THIS FOR PRODUCTION INSTALLATIONS.
*/ */
"connect-src 'self' ws: wss: blob:" + domain, // httpSafeOrigin: "https://some-other-domain.xyz",
// data: is used by codemirror /* httpAddress specifies the address on which the nodejs server
"img-src 'self' data: blob:" + domain, * should be accessible. By default it will listen on 127.0.0.1
"media-src * blob:", * (IPv4 localhost on most systems). If you want it to listen on
* all addresses, including IPv6, set this to '::'.
*
*/
//httpAddress: '::',
// for accounts.cryptpad.fr authentication and cross-domain iframe sandbox /* httpPort specifies on which port the nodejs server should listen.
"frame-ancestors *", * By default it will serve content over port 3000, which is suitable
"" * for both local development and for use with the provided nginx example,
]; * which will proxy websocket traffic to your node server.
*
*/
//httpPort: 3000,
/* httpSafePort allows you to specify an alternative port from which
* the node process should serve sandboxed assets. The default value is
* that of your httpPort + 1. You probably don't need to change this.
*
*/
//httpSafePort: 3001,
module.exports = {
/* ===================== /* =====================
* Admin * Admin
* ===================== */ * ===================== */
/* /*
* CryptPad now contains an administration panel. Its access is restricted to specific * CryptPad contains an administration panel. Its access is restricted to specific
* users using the following list. * users using the following list.
* To give access to the admin panel to a user account, just add their user id, * To give access to the admin panel to a user account, just add their user id,
* which can be found on the settings page for registered users. * which can be found on the settings page for registered users.
* Entries should be strings separated by a comma. * Entries should be strings separated by a comma.
*/ */
/*
adminKeys: [ adminKeys: [
//"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=", //"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=",
], ],
*/
/* CryptPad's administration panel includes a "support" tab /* CryptPad's administration panel includes a "support" tab
* wherein administrators with a secret key can view messages * wherein administrators with a secret key can view messages
@ -76,159 +119,55 @@ module.exports = {
*/ */
// supportMailboxPublicKey: "", // supportMailboxPublicKey: "",
/* ===================== /* We're very proud that CryptPad is available to the public as free software!
* Infra setup * We do, however, still need to pay our bills as we develop the platform.
* ===================== */
// the address you want to bind to, :: means all ipv4 and ipv6 addresses
// this may not work on all operating systems
httpAddress: '::',
// the port on which your httpd will listen
httpPort: 3000,
// This is for allowing the cross-domain iframe to function when developing
httpSafePort: 3001,
// This is for deployment in production, CryptPad uses a separate origin (domain) to host the
// cross-domain iframe. It can simply host the same content as CryptPad.
// httpSafeOrigin: "https://some-other-domain.xyz",
httpUnsafeOrigin: domain,
/* Your CryptPad server will share this value with clients
* via its /api/config endpoint.
* *
* If you want to host your API and asset servers on different hosts * By default CryptPad will prompt users to consider donating to
* specify a URL for your API server websocket endpoint, like so: * our OpenCollective campaign. We publish the state of our finances periodically
* wss://api.yourdomain.com/cryptpad_websocket * so you can decide for yourself whether our expenses are reasonable.
* *
* Otherwise, leave this commented and your clients will use the default * You can disable any solicitations for donations by setting 'removeDonateButton' to true,
* websocket (wss://yourdomain.com/cryptpad_websocket) * but we'd appreciate it if you didn't!
*/ */
//externalWebsocketURL: 'wss://api.yourdomain.com/cryptpad_websocket //removeDonateButton: false,
/* CryptPad can be configured to send customized HTTP Headers /* CryptPad will display a point of contact for your instance on its contact page
* These settings may vary widely depending on your needs * (/contact.html) if you provide it below.
* Examples are provided below
*/
httpHeaders: {
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*"
},
contentSecurity: baseCSP.join('; ') +
"script-src 'self'" + domain,
// CKEditor and OnlyOffice require significantly more lax content security policy in order to function.
padContentSecurity: baseCSP.join('; ') +
"script-src 'self' 'unsafe-eval' 'unsafe-inline'" + domain,
/* Main pages
* add exceptions to the router so that we can access /privacy.html
* and other odd pages
*/
mainPages: [
'index',
'privacy',
'terms',
'about',
'contact',
'what-is-cryptpad',
'features',
'faq',
'maintenance'
],
/* =====================
* Subscriptions
* ===================== */
/* Limits, Donations, Subscriptions and Contact
*
* By default, CryptPad limits every registered user to 50MB of storage. It also shows a
* subscribe button which allows them to upgrade to a paid account. We handle payment,
* and keep 50% of the proceeds to fund ongoing development.
*
* You can:
* A: leave things as they are
* B: disable accounts but display a donate button
* C: hide any reference to paid accounts or donation
*
* If you chose A then there's nothing to do.
* If you chose B, set 'allowSubscriptions' to false.
* If you chose C, set 'removeDonateButton' to true
*/
allowSubscriptions: true,
removeDonateButton: false,
/*
* 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', adminEmail: 'i.did.not.read.my.config@cryptpad.fr',
/* Sales coming from your server will be identified by your domain
*
* If you are using CryptPad in a business context, please consider taking a support contract
* by contacting sales@cryptpad.fr
*/
myDomain: _domain,
/* /*
* If you are using CryptPad internally and you want to increase the per-user storage limit, * By default, CryptPad contacts one of our servers once a day.
* change the following value. * This check-in will also send some very basic information about your instance including its
* version and the adminEmail so we can reach you if we are aware of a serious problem.
* We will never sell it or send you marketing mail.
* *
* Please note: This limit is what makes people subscribe and what pays for CryptPad * If you want to block this check-in and remain set 'blockDailyCheck' to true.
* development. Running a public instance that provides a "better deal" than cryptpad.fr
* is effectively using the project against itself.
*/ */
defaultStorageLimit: 50 * 1024 * 1024, //blockDailyCheck: false,
/* /*
* CryptPad allows administrators to give custom limits to their friends. * By default users get 50MB of storage by registering on an instance.
* add an entry for each friend, identified by their user id, * You can set this value to whatever you want.
* which can be found on the settings page. Include a 'limit' (number of bytes),
* a 'plan' (string), and a 'note' (string).
* *
* hint: 1GB is 1024 * 1024 * 1024 bytes * hint: 50MB is 50 * 1024 * 1024
*/ */
customLimits: { //defaultStorageLimit: 50 * 1024 * 1024,
/*
"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": {
limit: 20 * 1024 * 1024 * 1024,
plan: 'insider',
note: 'storage space donated by my.awesome.website'
},
"https://my.awesome.website/user/#/1/cryptpad-user2/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=": {
limit: 10 * 1024 * 1024 * 1024,
plan: 'insider',
note: 'storage space donated by my.awesome.website'
}
*/
},
/* ===================== /* =====================
* STORAGE * STORAGE
* ===================== */ * ===================== */
/* By default the CryptPad server will run scheduled tasks every five minutes
* If you want to run scheduled tasks in a separate process (like a crontab)
* you can disable this behaviour by setting the following value to true
*/
disableIntegratedTasks: false,
/* Pads that are not 'pinned' by any registered user can be set to expire /* Pads that are not 'pinned' by any registered user can be set to expire
* after a configurable number of days of inactivity (default 90 days). * after a configurable number of days of inactivity (default 90 days).
* The value can be changed or set to false to remove expiration. * The value can be changed or set to false to remove expiration.
* Expired pads can then be removed using a cron job calling the * Expired pads can then be removed using a cron job calling the
* `delete-inactive.js` script with node * `evict-inactive.js` script with node
*
* defaults to 90 days if nothing is provided
*/ */
inactiveTime: 90, // days //inactiveTime: 90, // days
/* CryptPad archives some data instead of deleting it outright. /* CryptPad archives some data instead of deleting it outright.
* This archived data still takes up space and so you'll probably still want to * This archived data still takes up space and so you'll probably still want to
@ -241,31 +180,46 @@ module.exports = {
* deletion. Set this value to the number of days you'd like to retain * deletion. Set this value to the number of days you'd like to retain
* archived data before it's removed permanently. * archived data before it's removed permanently.
* *
* defaults to 15 days if nothing is provided
*/ */
archiveRetentionTime: 15, //archiveRetentionTime: 15,
/* Max Upload Size (bytes) /* Max Upload Size (bytes)
* this sets the maximum size of any one file uploaded to the server. * this sets the maximum size of any one file uploaded to the server.
* anything larger than this size will be rejected * anything larger than this size will be rejected
* defaults to 20MB if no value is provided
*/ */
maxUploadSize: 20 * 1024 * 1024, //maxUploadSize: 20 * 1024 * 1024,
// XXX
premiumUploadSize: 100 * 1024 * 1024,
/* ===================== /*
* HARDWARE RELATED * CryptPad allows administrators to give custom limits to their friends.
* ===================== */ * add an entry for each friend, identified by their user id,
* which can be found on the settings page. Include a 'limit' (number of bytes),
/* CryptPad's file storage adaptor closes unused files after a configurable * a 'plan' (string), and a 'note' (string).
* number of milliseconds (default 30000 (30 seconds)) *
* hint: 1GB is 1024 * 1024 * 1024 bytes
*/
/*
customLimits: {
"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": {
limit: 20 * 1024 * 1024 * 1024,
plan: 'insider',
note: 'storage space donated by my.awesome.website'
},
"https://my.awesome.website/user/#/1/cryptpad-user2/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=": {
limit: 10 * 1024 * 1024 * 1024,
plan: 'insider',
note: 'storage space donated by my.awesome.website'
}
},
*/ */
channelExpirationMs: 30000,
/* CryptPad's file storage adaptor is limited by the number of open files. /* Users with premium accounts (those with a plan included in their customLimit)
* When the adaptor reaches openFileLimit, it will clean up older files * can benefit from an increased upload size limit. By default they are restricted to the same
* upload size as any other registered user.
*
*/ */
openFileLimit: 2048, //premiumUploadSize: 100 * 1024 * 1024,
/* ===================== /* =====================
* DATABASE VOLUMES * DATABASE VOLUMES

@ -107,7 +107,7 @@ define([
])*/ ])*/
]) ])
]), ]),
h('div.cp-version-footer', "CryptPad v3.12.0 (Megaloceros)") h('div.cp-version-footer', "CryptPad v3.13.0 (NorthernWhiteRhino)")
]); ]);
}; };

@ -431,7 +431,7 @@
width: 50px; width: 50px;
margin: 0; margin: 0;
min-width: 0; min-width: 0;
font-size: 18px; font-size: 18px !important;
} }
} }
} }

@ -5,6 +5,7 @@ const Util = require("../common-util");
const nThen = require("nthen"); const nThen = require("nthen");
const Core = require("./core"); const Core = require("./core");
const Metadata = require("./metadata"); const Metadata = require("./metadata");
const HK = require("../hk-util");
Channel.clearOwnedChannel = function (Env, safeKey, channelId, cb, Server) { Channel.clearOwnedChannel = function (Env, safeKey, channelId, cb, Server) {
if (typeof(channelId) !== 'string' || channelId.length !== 32) { if (typeof(channelId) !== 'string' || channelId.length !== 32) {
@ -228,7 +229,9 @@ Channel.isNewChannel = function (Env, channel, cb) {
Otherwise behaves the same as sending to a channel Otherwise behaves the same as sending to a channel
*/ */
Channel.writePrivateMessage = function (Env, args, cb, Server) { Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
var cb = Util.once(Util.mkAsync(_cb));
var channelId = args[0]; var channelId = args[0];
var msg = args[1]; var msg = args[1];
@ -246,6 +249,27 @@ Channel.writePrivateMessage = function (Env, args, cb, Server) {
return void cb("NOT_IMPLEMENTED"); return void cb("NOT_IMPLEMENTED");
} }
nThen(function (w) {
Metadata.getMetadataRaw(Env, channelId, w(function (err, metadata) {
if (err) {
w.abort();
Env.Log.error('HK_WRITE_PRIVATE_MESSAGE', err);
return void cb('METADATA_ERR');
}
if (!metadata || !metadata.restricted) {
return;
}
var session = HK.getNetfluxSession(Env, netfluxId);
var allowed = HK.listAllowedUsers(metadata);
if (HK.isUserSessionAllowed(allowed, session)) { return; }
w.abort();
cb('INSUFFICIENT_PERMISSIONS');
}));
}).nThen(function () {
// historyKeeper expects something with an 'id' attribute // historyKeeper expects something with an 'id' attribute
// it will fail unless you provide it, but it doesn't need anything else // it will fail unless you provide it, but it doesn't need anything else
var channelStruct = { var channelStruct = {
@ -261,7 +285,6 @@ Channel.writePrivateMessage = function (Env, args, cb, Server) {
msg // the actual message content. Generally a string msg // the actual message content. Generally a string
]; ];
// XXX RESTRICT respect allow lists
// historyKeeper already knows how to handle metadata and message validation, so we just pass it off here // historyKeeper already knows how to handle metadata and message validation, so we just pass it off here
// if the message isn't valid it won't be stored. // if the message isn't valid it won't be stored.
@ -272,5 +295,6 @@ Channel.writePrivateMessage = function (Env, args, cb, Server) {
}); });
cb(); cb();
});
}; };

@ -38,6 +38,7 @@ Quota.updateCachedLimits = function (Env, cb) {
if (Env.adminEmail === false) { if (Env.adminEmail === false) {
Quota.applyCustomLimits(Env); Quota.applyCustomLimits(Env);
if (Env.allowSubscriptions === false) { return; } if (Env.allowSubscriptions === false) { return; }
if (Env.blockDailyCheck === true) { return; }
throw new Error("allowSubscriptions must be false if adminEmail is false"); throw new Error("allowSubscriptions must be false if adminEmail is false");
} }

@ -0,0 +1,86 @@
var Default = module.exports;
Default.commonCSP = function (domain) {
domain = ' ' + domain;
// Content-Security-Policy
return [
"default-src 'none'",
"style-src 'unsafe-inline' 'self' " + domain,
"font-src 'self' data:" + domain,
/* child-src is used to restrict iframes to a set of allowed domains.
* 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.
*/
"child-src blob: *",
// IE/Edge
"frame-src blob: *",
/* 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: blob:" + domain,
// data: is used by codemirror
"img-src 'self' data: blob:" + domain,
"media-src * blob:",
// for accounts.cryptpad.fr authentication and cross-domain iframe sandbox
"frame-ancestors *",
""
];
};
Default.contentSecurity = function (domain) {
return (Default.commonCSP(domain).join('; ') + "script-src 'self' " + domain).replace(/\s+/g, ' ');
};
Default.padContentSecurity = function (domain) {
return (Default.commonCSP(domain).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' " + domain).replace(/\s+/g, ' ');
};
Default.httpHeaders = function () {
return {
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*"
};
};
Default.mainPages = function () {
return [
'index',
'privacy',
'terms',
'about',
'contact',
'what-is-cryptpad',
'features',
'faq',
'maintenance'
];
};
/* By default the CryptPad server will run scheduled tasks every five minutes
* If you want to run scheduled tasks in a separate process (like a crontab)
* you can disable this behaviour by setting the following value to true
*/
//disableIntegratedTasks: false,
/* CryptPad's file storage adaptor closes unused files after a configurable
* number of milliseconds (default 30000 (30 seconds))
*/
// channelExpirationMs: 30000,
/* 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,

@ -65,10 +65,12 @@ module.exports.create = function (config, cb) {
WARN: WARN, WARN: WARN,
flushCache: config.flushCache, flushCache: config.flushCache,
adminEmail: config.adminEmail, adminEmail: config.adminEmail,
allowSubscriptions: config.allowSubscriptions, allowSubscriptions: config.allowSubscriptions === true,
myDomain: config.myDomain, blockDailyCheck: config.blockDailyCheck === true,
mySubdomain: config.mySubdomain,
customLimits: config.customLimits, myDomain: config.httpUnsafeOrigin,
mySubdomain: config.mySubdomain, // only exists for the accounts integration
customLimits: config.customLimits || {},
// FIXME this attribute isn't in the default conf // FIXME this attribute isn't in the default conf
// but it is referenced in Quota // but it is referenced in Quota
domain: config.domain domain: config.domain

@ -834,6 +834,7 @@ const directMessageCommands = {
*/ */
HK.onDirectMessage = function (Env, Server, seq, userId, json) { HK.onDirectMessage = function (Env, Server, seq, userId, json) {
const Log = Env.Log; const Log = Env.Log;
const HISTORY_KEEPER_ID = Env.id;
Log.silly('HK_MESSAGE', json); Log.silly('HK_MESSAGE', json);
let parsed; let parsed;
@ -891,10 +892,27 @@ HK.onDirectMessage = function (Env, Server, seq, userId, json) {
return; return;
} }
// XXX NOT ALLOWED /* Anyone in the userlist that isn't in the allow list should have already
// respond to txid with error as in handleGetHistory been kicked out of the channel. Likewise, disallowed users should not
// send the allow list anyway, it might not get used currently be able to add themselves to the userlist because JOIN commands respect
// but will in the future access control settings. The error that is sent below protects against
the remaining case, in which users try to get history without having
joined the channel. Normally we'd send the allow list to tell them the
key with which they should authenticate, but since we don't use this
behaviour, I'm doing the easy thing and just telling them to GO AWAY.
We can implement the more advanced behaviour later if it turns out that
we need it. This command validates guards against all kinds of history
access: GET_HISTORY, GET_HISTORY_RANGE, GET_FULL_HISTORY.
*/
w.abort();
return void Server.send(userId, [
seq,
'ERROR',
'ERESTRICTED',
HISTORY_KEEPER_ID
]);
})); }));
}).nThen(function () { }).nThen(function () {
// run the appropriate command from the map // run the appropriate command from the map

@ -1,7 +1,7 @@
/* jslint node: true */ /* jslint node: true */
"use strict"; "use strict";
var config; var config;
var configPath = process.env.CRYPTPAD_CONFIG || "../config/config"; var configPath = process.env.CRYPTPAD_CONFIG || "../config/config.js";
try { try {
config = require(configPath); config = require(configPath);
if (config.adminEmail === 'i.did.not.read.my.config@cryptpad.fr') { if (config.adminEmail === 'i.did.not.read.my.config@cryptpad.fr') {
@ -18,5 +18,29 @@ try {
} }
config = require("../config/config.example"); config = require("../config/config.example");
} }
var isPositiveNumber = function (n) {
return (!isNaN(n) && n >= 0);
};
if (!isPositiveNumber(config.inactiveTime)) {
config.inactiveTime = 90;
}
if (!isPositiveNumber(config.archiveRetentionTime)) {
config.archiveRetentionTime = 90;
}
if (!isPositiveNumber(config.maxUploadSize)) {
config.maxUploadSize = 20 * 1024 * 1024;
}
if (!isPositiveNumber(config.defaultStorageLimit)) {
config.defaultStorageLimit = 50 * 1024 * 1024;
}
// premiumUploadSize is worthless if it isn't a valid positive number
// or if it's less than the default upload size
if (!isPositiveNumber(config.premiumUploadSize) || config.premiumUploadSize < config.defaultStorageLimit) {
delete config.premiumUploadSize;
}
module.exports = config; module.exports = config;

4
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"version": "3.12.0", "version": "3.13.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -114,6 +114,8 @@
}, },
"chainpad-server": { "chainpad-server": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chainpad-server/-/chainpad-server-4.0.3.tgz",
"integrity": "sha512-lTYd5Nk8iCm/2nrA6lQIbxEhw4OraBMxa6+7YQXZZlMI290Cpzna41hs63aXbwT+UR9llS5C9U6yLqVrlm7MCQ==",
"requires": { "requires": {
"nthen": "0.1.8", "nthen": "0.1.8",
"pull-stream": "^3.6.9", "pull-stream": "^3.6.9",

@ -1,7 +1,7 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server", "description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.12.0", "version": "3.13.0",
"license": "AGPL-3.0+", "license": "AGPL-3.0+",
"repository": { "repository": {
"type": "git", "type": "git",

@ -4,7 +4,7 @@ var Config = require("../lib/load-config");
var Package = require("../package.json"); var Package = require("../package.json");
var body = JSON.stringify({ var body = JSON.stringify({
domain: Config.myDomain, domain: Config.myDomain || Config.httpUnsafeOrigin,
subdomain: Config.mySubdomain || null, subdomain: Config.mySubdomain || null,
adminEmail: Config.adminEmail, adminEmail: Config.adminEmail,
version: Package.version, version: Package.version,

@ -373,11 +373,24 @@ nThen(function (w) {
} }
})); }));
}).nThen(function (w) { }).nThen(function (w) {
// XXX RESTRICT GET_METADATA should fail because alice is not on the allow list alice.anonRpc.send('GET_METADATA', oscar.mailboxChannel, w(function (err, response) {
// expect INSUFFICIENT_PERMISSIONS if (!response) { throw new Error("EXPECTED RESPONSE"); }
alice.anonRpc.send('GET_METADATA', oscar.mailboxChannel, w(function (err) { var metadata = response[0];
if (!err) { var expected_fields = ['restricted', 'allowed'];
// XXX RESTRICT alice should not be permitted to read oscar's mailbox's metadata for (var key in metadata) {
if (expected_fields.indexOf(key) === -1) {
console.log(metadata);
throw new Error("EXPECTED METADATA TO BE RESTRICTED");
}
}
}));
}).nThen(function (w) {
alice.anonRpc.send('WRITE_PRIVATE_MESSAGE', [
oscar.mailboxChannel,
'["VANDALISM"]',
], w(function (err) {
if (err !== 'INSUFFICIENT_PERMISSIONS') {
throw new Error("EXPECTED INSUFFICIENT PERMISSIONS ERROR");
} }
})); }));
}).nThen(function (w) { }).nThen(function (w) {
@ -388,11 +401,17 @@ nThen(function (w) {
value: [ value: [
alice.edKeys.edPublic alice.edKeys.edPublic
] ]
}, w(function (err /*, metadata */) { }, w(function (err, response) {
if (err) { if (err) {
throw new Error("FAIL");
return void console.error(err); return void console.error(err);
} }
//console.log('XXX', metadata);
var metadata = response && response[0];
if (!metadata || !Array.isArray(metadata.allowed) ||
metadata.allowed.indexOf(alice.edKeys.edPublic) === -1) {
throw new Error("EXPECTED ALICE TO BE IN THE ALLOW LIST");
}
})); }));
}).nThen(function (w) { }).nThen(function (w) {
oscar.anonRpc.send('GET_METADATA', oscar.mailboxChannel, w(function (err, response) { oscar.anonRpc.send('GET_METADATA', oscar.mailboxChannel, w(function (err, response) {
@ -410,14 +429,12 @@ nThen(function (w) {
} }
})); }));
}).nThen(function () { }).nThen(function () {
// XXX RESTRICT alice should now be able to read oscar's mailbox metadata
/*
alice.anonRpc.send('GET_METADATA', oscar.mailboxChannel, function (err, response) { alice.anonRpc.send('GET_METADATA', oscar.mailboxChannel, function (err, response) {
if (err) { var metadata = response && response[0];
PROBLEM if (!metadata || !metadata.restricted || !metadata.channel) {
throw new Error("EXPECTED FULL ACCESS TO CHANNEL METADATA");
} }
}); });
*/
}).nThen(function (w) { }).nThen(function (w) {
//throw new Error("boop"); //throw new Error("boop");
// add alice as an owner of oscar's mailbox for some reason // add alice as an owner of oscar's mailbox for some reason

@ -8,6 +8,7 @@ var Package = require('./package.json');
var Path = require("path"); var Path = require("path");
var nThen = require("nthen"); var nThen = require("nthen");
var Util = require("./lib/common-util"); var Util = require("./lib/common-util");
var Default = require("./lib/defaults");
var config = require("./lib/load-config"); var config = require("./lib/load-config");
@ -35,6 +36,47 @@ if (process.env.PACKAGE) {
FRESH_KEY = +new Date(); FRESH_KEY = +new Date();
} }
(function () {
// you absolutely must provide an 'httpUnsafeOrigin'
if (typeof(config.httpUnsafeOrigin) !== 'string') {
throw new Error("No 'httpUnsafeOrigin' provided");
}
config.httpUnsafeOrigin = config.httpUnsafeOrigin.trim();
// fall back to listening on a local address
// if httpAddress is not a string
if (typeof(config.httpAddress) !== 'string') {
config.httpAddress = '127.0.0.1';
}
// listen on port 3000 if a valid port number was not provided
if (typeof(config.httpPort) !== 'number' || config.httpPort > 65535) {
config.httpPort = 3000;
}
if (typeof(httpSafeOrigin) !== 'string') {
if (typeof(config.httpSafePort) !== 'number') {
config.httpSafePort = config.httpPort + 1;
}
if (DEV_MODE) { return; }
console.log(`
m m mm mmmmm mm m mmmmm mm m mmm m
# # # ## # "# #"m # # #"m # m" " #
" #"# # # # #mmmm" # #m # # # #m # # mm #
## ##" #mm# # "m # # # # # # # # #
# # # # # " # ## mm#mm # ## "mmm" #
`);
console.log("\nNo 'httpSafeOrigin' provided.");
console.log("Your configuration probably isn't taking advantage of all of CryptPad's security features!");
console.log("This is acceptable for development, otherwise your users may be at risk.\n");
console.log("Serving sandboxed content via port %s.\nThis is probably not what you want for a production instance!\n", config.httpSafePort);
}
}());
var configCache = {}; var configCache = {};
config.flushCache = function () { config.flushCache = function () {
configCache = {}; configCache = {};
@ -47,11 +89,21 @@ config.flushCache = function () {
const clone = (x) => (JSON.parse(JSON.stringify(x))); const clone = (x) => (JSON.parse(JSON.stringify(x)));
var setHeaders = (function () { var setHeaders = (function () {
if (typeof(config.httpHeaders) !== 'object') { return function () {}; } // load the default http headers unless the admin has provided their own via the config file
var headers;
var custom = config.httpHeaders;
// if the admin provided valid http headers then use them
if (custom && typeof(custom) === 'object' && !Array.isArray(custom)) {
headers = clone(custom);
} else {
// otherwise use the default
headers = Default.httpHeaders();
}
const headers = clone(config.httpHeaders); // next define the base Content Security Policy (CSP) headers
if (config.contentSecurity) { if (typeof(config.contentSecurity) === 'string') {
headers['Content-Security-Policy'] = clone(config.contentSecurity); headers['Content-Security-Policy'] = config.contentSecurity;
if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' } if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' }
if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) { if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) {
// backward compat for those who do not merge the new version of the config // backward compat for those who do not merge the new version of the config
@ -59,10 +111,16 @@ var setHeaders = (function () {
// It also fixes the cross-domain iframe. // It also fixes the cross-domain iframe.
headers['Content-Security-Policy'] += "frame-ancestors *;"; headers['Content-Security-Policy'] += "frame-ancestors *;";
} }
} else {
// use the default CSP headers constructed with your domain
headers['Content-Security-Policy'] = Default.contentSecurity(config.httpUnsafeOrigin);
} }
const padHeaders = clone(headers); const padHeaders = clone(headers);
if (config.padContentSecurity) { if (typeof(config.padContentSecurity) === 'string') {
padHeaders['Content-Security-Policy'] = clone(config.padContentSecurity); padHeaders['Content-Security-Policy'] = config.padContentSecurity;
} else {
padHeaders['Content-Security-Policy'] = Default.padContentSecurity(config.httpUnsafeOrigin);
} }
if (Object.keys(headers).length) { if (Object.keys(headers).length) {
return function (req, res) { return function (req, res) {
@ -116,7 +174,7 @@ app.use(Express.static(__dirname + '/www'));
// FIXME I think this is a regression caused by a recent PR // FIXME I think this is a regression caused by a recent PR
// correct this hack without breaking the contributor's intended behaviour. // correct this hack without breaking the contributor's intended behaviour.
var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'contact']; var mainPages = config.mainPages || Default.mainPages();
var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
app.get(mainPagePattern, Express.static(__dirname + '/customize')); app.get(mainPagePattern, Express.static(__dirname + '/customize'));
app.get(mainPagePattern, Express.static(__dirname + '/customize.dist')); app.get(mainPagePattern, Express.static(__dirname + '/customize.dist'));
@ -163,11 +221,13 @@ var serveConfig = (function () {
removeDonateButton: (config.removeDonateButton === true), removeDonateButton: (config.removeDonateButton === true),
allowSubscriptions: (config.allowSubscriptions === true), allowSubscriptions: (config.allowSubscriptions === true),
websocketPath: config.externalWebsocketURL, websocketPath: config.externalWebsocketURL,
httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''), httpUnsafeOrigin: config.httpUnsafeOrigin,
adminEmail: config.adminEmail, adminEmail: config.adminEmail,
adminKeys: admins, adminKeys: admins,
inactiveTime: config.inactiveTime, inactiveTime: config.inactiveTime,
supportMailbox: config.supportMailboxPublicKey supportMailbox: config.supportMailboxPublicKey,
maxUploadSize: config.maxUploadSize,
premiumUploadSize: config.premiumUploadSize,
}, null, '\t'), }, null, '\t'),
'obj.httpSafeOrigin = ' + (function () { 'obj.httpSafeOrigin = ' + (function () {
if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; } if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; }

@ -953,7 +953,6 @@ define([
'data-curve': data.curvePublic || '', 'data-curve': data.curvePublic || '',
'data-name': name.toLowerCase(), 'data-name': name.toLowerCase(),
'data-order': i, 'data-order': i,
title: name,
style: 'order:'+i+';' style: 'order:'+i+';'
},[ },[
avatar, avatar,
@ -2426,9 +2425,9 @@ define([
case 'access': case 'access':
button = $('<button>', { button = $('<button>', {
'class': 'fa fa-unlock-alt cp-toolbar-icon-access', 'class': 'fa fa-unlock-alt cp-toolbar-icon-access',
title: "ACCESS", // XXX title: Messages.accessButton,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}) }).append($('<span>', {'class': 'cp-toolbar-drawer-element'})
.text("ACCESS")) // XXX .text(Messages.accessButton))
.click(common.prepareFeedback(type)) .click(common.prepareFeedback(type))
.click(function () { .click(function () {
common.isPadStored(function (err, data) { common.isPadStored(function (err, data) {
@ -3391,7 +3390,7 @@ define([
attributes: { attributes: {
'class': 'cp-toolbar-menu-logout-everywhere fa fa-plug', 'class': 'cp-toolbar-menu-logout-everywhere fa fa-plug',
}, },
content: h('span', "CLOSE REMOTE SESSIONS") // XXX Messages.settings_logoutEverywhereTitle) content: h('span', Messages.settings_logoutEverywhereTitle)
}); });
options.push({ options.push({
tag: 'a', tag: 'a',
@ -4235,7 +4234,7 @@ define([
msg += Messages.errorCopy; msg += Messages.errorCopy;
} }
} else if (err.type === 'ERESTRICTED') { } else if (err.type === 'ERESTRICTED') {
msg = Messages.restrictedError || "RESTRICTED"; // XXX msg = Messages.restrictedError;
} }
var sframeChan = common.getSframeChannel(); var sframeChan = common.getSframeChannel();
sframeChan.event('EV_SHARE_OPEN', {hidden: true}); sframeChan.event('EV_SHARE_OPEN', {hidden: true});

@ -325,6 +325,10 @@ define([
'tabindex': '-1', 'tabindex': '-1',
'data-icon': faOpenInCode, 'data-icon': faOpenInCode,
}, Messages.fc_openInCode)), }, Messages.fc_openInCode)),
h('li', h('a.cp-app-drive-context-savelocal.dropdown-item', {
'tabindex': '-1',
'data-icon': 'fa-cloud-upload',
}, Messages.pad_mediatagImport)), // Save in your CryptDrive
$separator.clone()[0], $separator.clone()[0],
h('li', h('a.cp-app-drive-context-expandall.dropdown-item', { h('li', h('a.cp-app-drive-context-expandall.dropdown-item', {
'tabindex': '-1', 'tabindex': '-1',
@ -344,14 +348,14 @@ define([
'tabindex': '-1', 'tabindex': '-1',
'data-icon': 'fa-shhare-alt', 'data-icon': 'fa-shhare-alt',
}, Messages.shareButton)), }, Messages.shareButton)),
h('li', h('a.cp-app-drive-context-savelocal.dropdown-item', { h('li', h('a.cp-app-drive-context-access.dropdown-item', {
'tabindex': '-1', 'tabindex': '-1',
'data-icon': 'fa-cloud-upload', 'data-icon': faAccess,
}, Messages.pad_mediatagImport)), // Save in your CryptDrive }, Messages.accessButton)),
h('li', h('a.cp-app-drive-context-download.dropdown-item', { h('li', h('a.cp-app-drive-context-properties.dropdown-item', {
'tabindex': '-1', 'tabindex': '-1',
'data-icon': faDownload, 'data-icon': faProperties,
}, Messages.download_mt_button)), }, Messages.fc_prop)),
$separator.clone()[0], $separator.clone()[0],
h('li', h('a.cp-app-drive-context-newfolder.dropdown-item.cp-app-drive-context-editable', { h('li', h('a.cp-app-drive-context-newfolder.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1', 'tabindex': '-1',
@ -458,14 +462,10 @@ define([
'data-icon': faDelete, 'data-icon': faDelete,
}, Messages.fc_remove_sharedfolder)), }, Messages.fc_remove_sharedfolder)),
$separator.clone()[0], $separator.clone()[0],
h('li', h('a.cp-app-drive-context-access.dropdown-item', { h('li', h('a.cp-app-drive-context-download.dropdown-item', {
'tabindex': '-1',
'data-icon': faAccess,
}, "ACCESS")), // XXX
h('li', h('a.cp-app-drive-context-properties.dropdown-item', {
'tabindex': '-1', 'tabindex': '-1',
'data-icon': faProperties, 'data-icon': faDownload,
}, Messages.fc_prop)), }, Messages.download_mt_button)),
]) ])
]); ]);
// add icons to the contextmenu options // add icons to the contextmenu options
@ -1279,7 +1279,7 @@ define([
case 'tree': case 'tree':
show = ['open', 'openro', 'openincode', 'expandall', 'collapseall', show = ['open', 'openro', 'openincode', 'expandall', 'collapseall',
'color', 'download', 'share', 'savelocal', 'rename', 'delete', 'makeacopy', 'color', 'download', 'share', 'savelocal', 'rename', 'delete', 'makeacopy',
'deleteowned', 'removesf', 'properties', 'hashtag']; 'deleteowned', 'removesf', 'access', 'properties', 'hashtag'];
break; break;
case 'default': case 'default':
show = ['open', 'openro', 'share', 'openparent', 'delete', 'deleteowned', 'properties', 'access', 'hashtag', 'makeacopy']; show = ['open', 'openro', 'share', 'openparent', 'delete', 'deleteowned', 'properties', 'access', 'hashtag', 'makeacopy'];

@ -11,10 +11,7 @@ define([
Messages, nThen) { Messages, nThen) {
var Access = {}; var Access = {};
// XXX contacts, teams, access_noContact
Messages.contacts = "Contacts"; // XXX
Messages.teams = "Teams"; // XXX
Messages.access_noContact = "No other contact to add"; // XXX
var evRedrawAll = Util.mkEvent(); var evRedrawAll = Util.mkEvent();
@ -120,7 +117,7 @@ define([
if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; } if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; }
if (ed === edPublic && !teamOwner) { me = true; } if (ed === edPublic && !teamOwner) { me = true; }
nThen(function (waitFor) { nThen(function (waitFor) {
var msg = me ? Messages.owner_removeMeConfirm : Messages.owner_removeConfirm; // XXX check existing keys var msg = me ? Messages.owner_removeMeConfirm : Messages.owner_removeConfirm;
UI.confirm(msg, waitFor(function (yes) { UI.confirm(msg, waitFor(function (yes) {
if (!yes) { if (!yes) {
waitFor.abort(); waitFor.abort();
@ -338,6 +335,7 @@ define([
pending_owners = data.pending_owners || []; pending_owners = data.pending_owners || [];
$div1.empty(); $div1.empty();
$div2.empty(); $div2.empty();
$div1.append(h('p', Messages.owner_text));
$div1.append(drawRemove(false)).append(drawRemove(true)); $div1.append(drawRemove(false)).append(drawRemove(true));
$div2.append(drawAdd()); $div2.append(drawAdd());
}); });
@ -438,12 +436,6 @@ define([
} }
}); });
// XXX allow_removeConfirm, allow_checkbox, allow_text, allow_addConfirm
Messages.allow_addConfirm = "Are you sure?"; // XXX
Messages.allow_removeConfirm = "Are you sure?"; // XXX
Messages.allow_checkbox = "Enable allow list"; // XXX
Messages.allow_text = 'Pewpewpew'; // XXX
var remove = function (el) { var remove = function (el) {
// Check selection // Check selection
var $el = $(el); var $el = $(el);
@ -701,7 +693,7 @@ define([
} }
// Otherwise it's a stranger // Otherwise it's a stranger
_owners[ed] = { _owners[ed] = {
name: '???', // XXX unkwown? name: Messages.owner_unknownUser,
}; };
strangers++; strangers++;
}); });
@ -884,11 +876,6 @@ define([
return $d; return $d;
}; };
var drawRight = function () { var drawRight = function () {
// XXX allow_enabled, allow_disabled, allow_label
Messages.allow_enabled = 'ENABLED'; // XXX
Messages.allow_disabled = 'DISABLED'; // XXX
Messages.allow_label = 'Allow list: {0}'; // XXX
// Owners // Owners
var content = []; var content = [];
var _ownersGrid = getUserList(common, data.owners); var _ownersGrid = getUserList(common, data.owners);
@ -939,9 +926,6 @@ define([
}); });
} }
// XXX access_muteRequests
Messages.access_muteRequests = "Mute access requests for this pad"; // XXX
// Mute access requests // Mute access requests
var priv = common.getMetadataMgr().getPrivateData(); var priv = common.getMetadataMgr().getPrivateData();
var edPublic = priv.edPublic; var edPublic = priv.edPublic;
@ -1105,10 +1089,6 @@ define([
}); });
})); }));
}).nThen(function () { }).nThen(function () {
// XXX access_main, access_allow
Messages.access_main = 'ACCESS'; // XXX
Messages.access_allow = 'ALLOW'; // XXX
var tabs = UI.dialog.tabs([{ var tabs = UI.dialog.tabs([{
title: Messages.access_main, title: Messages.access_main,
icon: "fa fa-unlock-alt", icon: "fa fa-unlock-alt",

@ -164,9 +164,6 @@ define([
queue.next(); queue.next();
if (e === 'TOO_LARGE') { if (e === 'TOO_LARGE') {
$pv.text(Messages.upload_tooLargeBrief); $pv.text(Messages.upload_tooLargeBrief);
// XXX translate
// instead of "This file exceeds the maximum upload size"
// use "the maximum upload size allowed for your account"
return void UI.alert(Messages.upload_tooLarge); return void UI.alert(Messages.upload_tooLarge);
} }
if (e === 'NOT_ENOUGH_SPACE') { if (e === 'NOT_ENOUGH_SPACE') {

@ -536,8 +536,8 @@
"settings_creationSkipFalse": "Mostra", "settings_creationSkipFalse": "Mostra",
"settings_templateSkip": "Salta la finestra de selecció de plantilla", "settings_templateSkip": "Salta la finestra de selecció de plantilla",
"settings_templateSkipHint": "Quan genereu un document nou buit, si teniu desades plantilles per aquest tipus de document, apareix una finestra preguntant-vos si voleu utilitzar una plantilla. Aquí podeu triar si no voleu veure mai més la finestra i no utilitzar una plantilla.", "settings_templateSkipHint": "Quan genereu un document nou buit, si teniu desades plantilles per aquest tipus de document, apareix una finestra preguntant-vos si voleu utilitzar una plantilla. Aquí podeu triar si no voleu veure mai més la finestra i no utilitzar una plantilla.",
"settings_ownDriveTitle": "Habilita les darreres funcionalitats del compte", "settings_ownDriveTitle": "Milloreu el compte",
"settings_ownDriveHint": "Per raons tècniques, els comptes antics no tenen accés a totes les funcionalitats noves. Si feu una actualització a un compte nou, preparareu el vostre CryptDrive per les properes funcionalitats sense interrompre la vostra activitat habitual.", "settings_ownDriveHint": "Degut a raons tècniques els comptes antics no tenen accés a les darreres funcionalitats . Una actualització gratuïta habilitarà les funcionalitats actuals i prepararà el vostre CryptDrive per futures actualitzacions.",
"settings_ownDriveButton": "Milloreu el vostre compte", "settings_ownDriveButton": "Milloreu el vostre compte",
"settings_ownDriveConfirm": "Millorar el vostre compte porta una estona. Necessitareu tornar-vos a connectar en tots els vostres dispositius. Segur que ho voleu fer?", "settings_ownDriveConfirm": "Millorar el vostre compte porta una estona. Necessitareu tornar-vos a connectar en tots els vostres dispositius. Segur que ho voleu fer?",
"settings_ownDrivePending": "El vostre compte s'està posant al dia. No tanqueu ni torneu a carregar aquesta pàgina fins que el procés hagi acabat.", "settings_ownDrivePending": "El vostre compte s'està posant al dia. No tanqueu ni torneu a carregar aquesta pàgina fins que el procés hagi acabat.",
@ -616,5 +616,8 @@
"download_step2": "Desxifrant", "download_step2": "Desxifrant",
"download_step1": "Descarregant", "download_step1": "Descarregant",
"download_dl": "Descarrega", "download_dl": "Descarrega",
"download_resourceNotAvailable": "El recurs sol·licitat no estava disponible... Premeu Esc per continuar." "download_resourceNotAvailable": "El recurs sol·licitat no estava disponible... Premeu Esc per continuar.",
"about_contributors": "Col·laboracions clau",
"about_core": "Desenvolupament principal",
"about_intro": "CryptPad s'ha creat dins l'Equip de Recerca de <a href=\"http://xwiki.com\">XWiki SAS</a>, una petita empresa de París, França i Iasi, Romania. Hi ha 3 membres de l'equip central treballant amb CryptPad més una quantitat de persones col·laboradores, dins i fora d'XWiki SAS."
} }

@ -554,7 +554,7 @@
"upload_success": "Votre fichier ({0}) a été importé avec succès et ajouté à votre CryptDrive.", "upload_success": "Votre fichier ({0}) a été importé avec succès et ajouté à votre CryptDrive.",
"upload_notEnoughSpace": "Il n'y a pas assez d'espace libre dans votre CryptDrive pour ce fichier.", "upload_notEnoughSpace": "Il n'y a pas assez d'espace libre dans votre CryptDrive pour ce fichier.",
"upload_notEnoughSpaceBrief": "Pas assez d'espace", "upload_notEnoughSpaceBrief": "Pas assez d'espace",
"upload_tooLarge": "Ce fichier dépasse la taille maximale autorisée.", "upload_tooLarge": "Ce fichier dépasse la taille maximale autorisée pour votre compte.",
"upload_tooLargeBrief": "Fichier trop volumineux", "upload_tooLargeBrief": "Fichier trop volumineux",
"upload_choose": "Choisir un fichier", "upload_choose": "Choisir un fichier",
"upload_pending": "En attente", "upload_pending": "En attente",
@ -1142,10 +1142,10 @@
"register_emailWarning1": "Vous pouvez continuer, mais ces données ne sont pas nécessaires et ne seront pas envoyées à notre serveur.", "register_emailWarning1": "Vous pouvez continuer, mais ces données ne sont pas nécessaires et ne seront pas envoyées à notre serveur.",
"register_emailWarning2": "Vous ne pourrez pas réinitialiser votre mot de passe en utilisant votre adresse email comme sur beaucoup d'autres services.", "register_emailWarning2": "Vous ne pourrez pas réinitialiser votre mot de passe en utilisant votre adresse email comme sur beaucoup d'autres services.",
"register_emailWarning3": "Si vous souhaitez tout de même utiliser votre adresse email comme nom d'utilisateur, appuyez sur OK.", "register_emailWarning3": "Si vous souhaitez tout de même utiliser votre adresse email comme nom d'utilisateur, appuyez sur OK.",
"owner_removeText": "Supprimer un propriétaire existant", "owner_removeText": "Propriétaires",
"owner_removePendingText": "Annuler une offre en attente", "owner_removePendingText": "En attente",
"owner_addText": "Proposer à un contact d'être co-propriétaire de ce document", "owner_addText": "Proposer à un contact d'être co-propriétaire de ce document",
"owner_unknownUser": "Utilisateur inconnu", "owner_unknownUser": "?????",
"owner_removeButton": "Supprimer les propriétaires sélectionnés", "owner_removeButton": "Supprimer les propriétaires sélectionnés",
"owner_removePendingButton": "Annuler les offres sélectionnées", "owner_removePendingButton": "Annuler les offres sélectionnées",
"owner_addButton": "Proposer d'être propriétaire", "owner_addButton": "Proposer d'être propriétaire",
@ -1311,5 +1311,17 @@
"historyTrim_historySize": "Historique : {0}", "historyTrim_historySize": "Historique : {0}",
"areYouSure": "Êtes-vous sûr ?", "areYouSure": "Êtes-vous sûr ?",
"copy_title": "{0} (copie)", "copy_title": "{0} (copie)",
"makeACopy": "Créer une copie" "makeACopy": "Créer une copie",
"owner_text": "Le(s) propriétaire(s) d'un pad sont les seuls utilisateurs autorisés à : ajouter/supprimer des propriétaires, restreindre l'accès au bloc-notes avec une liste d'accès, ou à supprimer le pad.",
"access_muteRequests": "Masquer les requêtes d'accès pour ce pad",
"allow_label": "Liste d'accès : {0}",
"allow_disabled": "désactivée",
"allow_enabled": "activée",
"allow_checkbox": "L'utilisation d'une liste d'accès signifie que seuls les utilisateurs et propriétaires sélectionnés pourront accéder à ce document.",
"access_noContact": "Il n'y a plus de contacts à ajouter",
"contacts": "Contacts",
"restrictedError": "Vous n'êtes pas autorisé à accéder à ce document",
"accessButton": "Accès",
"access_allow": "Liste",
"access_main": "Accès"
} }

@ -571,7 +571,7 @@
"upload_success": "Your file ({0}) has been successfully uploaded and added to your drive.", "upload_success": "Your file ({0}) has been successfully uploaded and added to your drive.",
"upload_notEnoughSpace": "There is not enough space for this file in your CryptDrive.", "upload_notEnoughSpace": "There is not enough space for this file in your CryptDrive.",
"upload_notEnoughSpaceBrief": "Not enough space", "upload_notEnoughSpaceBrief": "Not enough space",
"upload_tooLarge": "This file exceeds the maximum upload size.", "upload_tooLarge": "This file exceeds the maximum upload size allowed for your account.",
"upload_tooLargeBrief": "File too large", "upload_tooLargeBrief": "File too large",
"upload_choose": "Choose a file", "upload_choose": "Choose a file",
"upload_pending": "Pending", "upload_pending": "Pending",
@ -1147,10 +1147,10 @@
"features_noData": "No personal information required", "features_noData": "No personal information required",
"features_pricing": "Between {0} and {2}€ per month", "features_pricing": "Between {0} and {2}€ per month",
"features_emailRequired": "Email address required", "features_emailRequired": "Email address required",
"owner_removeText": "Remove an existing owner", "owner_removeText": "Owners",
"owner_removePendingText": "Cancel a pending offer", "owner_removePendingText": "Pending",
"owner_addText": "Offer co-ownership to a contact", "owner_addText": "Offer co-ownership to a contact",
"owner_unknownUser": "Unknown user", "owner_unknownUser": "?????",
"owner_removeButton": "Remove selected owners", "owner_removeButton": "Remove selected owners",
"owner_removePendingButton": "Cancel selected offers", "owner_removePendingButton": "Cancel selected offers",
"owner_addButton": "Offer ownership", "owner_addButton": "Offer ownership",
@ -1311,5 +1311,17 @@
"settings_trimHistoryTitle": "Delete History", "settings_trimHistoryTitle": "Delete History",
"settings_trimHistoryHint": "Save storage space by deleting the history of your drive and notifications. This will not affect the history of your pads. You can delete the history of pads in their properties dialog.", "settings_trimHistoryHint": "Save storage space by deleting the history of your drive and notifications. This will not affect the history of your pads. You can delete the history of pads in their properties dialog.",
"makeACopy": "Make a copy", "makeACopy": "Make a copy",
"copy_title": "{0} (copy)" "copy_title": "{0} (copy)",
"access_main": "Access",
"access_allow": "List",
"accessButton": "Access",
"restrictedError": "You are not authorised to access this document",
"contacts": "Contacts",
"access_noContact": "No other contact to add",
"allow_checkbox": "Using an access list means that only selected users and owners will be able to access this document.",
"allow_enabled": "enabled",
"allow_disabled": "disabled",
"allow_label": "Access list: {0}",
"access_muteRequests": "Mute access requests for this pad",
"owner_text": "The owner(s) of a pad are the only users authorised to: add/remove owners, restrict access to the pad with an access list, or delete the pad."
} }

Loading…
Cancel
Save