Merge branch 'staging' into documents-toolbar

This commit is contained in:
yflory 2024-03-07 14:02:41 +01:00
commit a1e2a03d03
681 changed files with 137250 additions and 2718 deletions

View File

@ -1,16 +0,0 @@
# SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
charset = utf-8

57
.eslintrc.js Normal file
View File

@ -0,0 +1,57 @@
module.exports = {
'env': {
'browser': true,
'es2021': true,
'node': true
},
'plugins': ['compat'],
'extends': ['eslint:recommended', 'plugin:compat/recommended'],
"globals": {
"define": "readonly",
},
'overrides': [
{
'env': {
'node': true
},
'files': [
'.eslintrc.{js,cjs}'
],
'parserOptions': {
'sourceType': 'script'
}
}
],
'parserOptions': {
'ecmaVersion': 'latest'
},
'rules': {
'indent': [
'off', // TODO enable this check
4
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'off', // TODO enable this check
'single'
],
'semi': [
'error',
'always'
],
// TODO remove these exceptions from the eslint defaults
'no-irregular-whitespace': ['off'],
'no-unused-vars': ['warn'],
'no-self-assign': ['off'],
'no-empty': ['off'],
'no-useless-escape': ['off'],
'no-redeclare': ['off'],
'no-extra-boolean-cast': ['off'],
'no-global-assign': ['off'],
'no-prototype-builtins': ['off'],
}
};

View File

@ -1,12 +0,0 @@
# SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
#
# SPDX-License-Identifier: AGPL-3.0-or-later
[ignore]
.*/components/.*
.*/node_modules/lesshint/*
[include]
[libs]
[options]

View File

@ -55,7 +55,7 @@ body:
- type: dropdown
id: operating-system
attributes:
label: What opreating system are you using?
label: Which operating system are you using?
multiple: true
options:
- Linux/BSD/UNIX
@ -89,6 +89,8 @@ body:
label: Version
description: What version of CryptPad are you running?
options:
- 5.7.0
- 5.6.0
- 5.5.0
- 5.4.1
- 5.4.0
@ -99,11 +101,6 @@ body:
- 5.0.0
- 4.14.1
- 4.14.0
- 4.13.0
- 4.12.1
- 4.12.0
- 4.11.0
- 4.10.0
- Other
validations:
required: true

3
.gitignore vendored
View File

@ -27,3 +27,6 @@ block/
logs/
privileged.conf
config/config.js
config/sso.js
lib/plugins/*
!lib/plugins/README.md

View File

@ -1,26 +0,0 @@
{
"laxcomma": true,
"laxbreak": true,
"sub": true,
"curly": true,
"eqeqeq": true,
"iterator": true,
"latedef": true,
"nocomma": true,
"shadow": false,
"undef": true,
"unused": true,
"futurehostile":true,
"browser": true,
"esversion": 6,
"predef": [
"console",
"define",
"require",
"module",
"__dirname"
],
"globals": {
"self": true
}
}

View File

@ -39,6 +39,10 @@ Files: www/lib/mermaid/*
Copyright: (c) 2014 - 2021 Knut Sveidqvist
License: MIT
Files: www/lib/calendar/moment.min.js
Copyright: (c) JS Foundation and other contributors
License: MIT
Files: www/lib/pdfjs/*
Copyright: 2017 Mozilla Foundation
License: Apache-2.0

View File

@ -4,6 +4,235 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and cont
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# 5.7.0
## Goals
This release includes some features that could not be included into 5.6.0, namely instance invitations and support for images in diagrams. It also includes bug fixes in the drive, calendar and many other places.
## Features
- Instance administrators can now issue invitation links that can be used to create one account each, even if registration is closed on the instance. An optional User Directory can help keep track of the known accounts on the instance. This feature is designed for the needs of enterprise customers who use their own instance, hence allowing administrators access to more information than on a public-facing service [#1395](https://github.com/cryptpad/cryptpad/pull/1395)
- Diagram documents now support images [#1295](https://github.com/cryptpad/cryptpad/pull/1295)
## Fixes
- Fix access modal issues after password change [#1394](https://github.com/cryptpad/cryptpad/pull/1394)
- Drive
- Shared folder access list [#1388](https://github.com/cryptpad/cryptpad/pull/1388)
- File icons in drive [#1386](https://github.com/cryptpad/cryptpad/pull/1386)
- Emptying trash with multiple folders and files fails [#1344](https://github.com/cryptpad/cryptpad/issues/1344)
- Shared folder and drive, read-only link issue [#1238](https://github.com/cryptpad/cryptpad/issues/1238)
- Loss of access to a shared folder after a double password change [#1365](https://github.com/cryptpad/cryptpad/issues/1365)
- Files
- PDFjs rendering issue with Firefox 121 [#1393](https://github.com/cryptpad/cryptpad/pull/1393)
- Rich Text
- Fix richtext issues [#1392](https://github.com/cryptpad/cryptpad/pull/1392)
- Duplicated element in table of content (TOC) [#1336](https://github.com/cryptpad/cryptpad/issues/1336)
- Anchors don't work anymore [#1226](https://github.com/cryptpad/cryptpad/issues/1226)
- Rows and columns numbers in tables can't be modified anymore [#1358](https://github.com/cryptpad/cryptpad/issues/1358)
- Forms
- Fix issue with duplicating choice/checkbox grid questions [#1359](https://github.com/cryptpad/cryptpad/pull/1359)
- Date question datepicker/input field now displays correctly [#1357](https://github.com/cryptpad/cryptpad/pull/1357)
- Duplicated “Enter” event sent when navigating with keyboard [#1396](https://github.com/cryptpad/cryptpad/issues/1396)
- Kanban
- Kanban item export [#1360](https://github.com/cryptpad/cryptpad/pull/1360)
- Calendar
- Calendar datepicker on mobile now easily toggled [#1368](https://github.com/cryptpad/cryptpad/pull/1368)
- Behaviour change: keep the offset between start and end date constant when updating the start date (otherwise it was possible to create events that end before even starting that thus dont appear in the calendar)
- Calendar yearly recurring event - wrong month name [#1398](https://github.com/cryptpad/cryptpad/issues/1398)
- Admin
- Encoding issues in broadcast messages [#1379](https://github.com/cryptpad/cryptpad/issues/1379)
- Deployment
- Fix Cryptpad is unhealthy on Docker [#1350](https://github.com/cryptpad/cryptpad/pull/1350) thanks to @llaumgui
## Dependencies
- Bump follow-redirects from 1.15.3 to 1.15.4 [#1378](https://github.com/cryptpad/cryptpad/pull/1378)
## Upgrade notes
If you are upgrading from a version older than `5.6.0` please read the upgrade notes of all versions between yours and `5.6.0` to avoid configuration issues.
⚠️ Before proceeding note that this upgrade requires changes to the Nginx configuration, please see full diff below.
To upgrade:
1. Stop your server
2. Get the latest code with git
```bash
git fetch origin --tags
git checkout 5.7.0
```
3. Update dependencies
```bash
npm ci
npm run install:components
```
4. Restart your server
5. Review your instance's checkup page to ensure that you are passing all tests
### Nginx config changes
```diff
diff --git a/docs/example-advanced.nginx.conf b/docs/example-advanced.nginx.conf
index cb827b4b0..f2b32e959 100644
--- a/docs/example-advanced.nginx.conf
+++ b/docs/example-advanced.nginx.conf
@@ -14,6 +14,8 @@ server {
# Let's Encrypt webroot
include letsencrypt-webroot;
+ # Include mime.types to be able to support .mjs files (see "types" below)
+ include mime.types;
# CryptPad serves static assets over these two domains.
# `main_domain` is what users will enter in their address bar.
@@ -166,11 +168,6 @@ server {
# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly in this privileged scope
if ($uri ~ ^\/unsafeiframe\/inner\.html.*$) { set $unsafe 1; }
- # draw.io uses inline script tags in it's index.html. The hashes are added here.
- if ($uri ~ ^\/components\/drawio\/src\/main\/webapp\/index.html.*$) {
- set $scriptSrc "'self' 'sha256-dLMFD7ijAw6AVaqecS7kbPcFFzkxQ+yeZSsKpOdLxps=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: https://${main_domain}";
- }
-
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
if ($unsafe) {
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: https://${main_domain}";
@@ -179,6 +176,11 @@ server {
# Finally, set all the rules you composed above.
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc; frame-ancestors $frameAncestors";
+ # Add support for .mjs files used by pdfjs
+ types {
+ application/javascript mjs;
+ }
+
# The nodejs process can handle all traffic whether accessed over websocket or as static assets
# We prefer to serve static content from nginx directly and to leave the API server to handle
# the dynamic content that only it can manage. This is primarily an optimization
```
# 5.6.0
## Goals
This release introduces support for integrating CryptPad instances with Single-Sign On authentication. It brings a lot of improvements and fixes to Form, Calendar, and other parts of CryptPad. This release begins to improve the accessibility of the toolbar towards full WCAG compliance which we hope to achieve in the near future.
## Features
- Authentication
- This version paves the way for SSO authentication for a CryptPad instance via a plugin (est. release Jan. 2024) [#1320](https://github.com/cryptpad/cryptpad/pull/1320)
- New setting to make Two-Factor Authentication mandatory for all user accounts on an instance [#1341](https://github.com/cryptpad/cryptpad/pull/1341)
- Form
- New button to duplicate a question [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Calendar
- New description field for calendar events [#1299](https://github.com/cryptpad/cryptpad/pull/1299)
## Improvements
- Accessibility of toolbars and some drop-down menus [#1290](https://github.com/cryptpad/cryptpad/pull/1290)
- "+ New" drop-down menu in Drive and Team Drive #1191
- New `Ctrl + e` modal #1192
- Code contact request notifications as headings #1197
- DOM order of toolbar #1198
- Notifications menu not accessible via Keyboard #1201
- Sidebar "tabs" not accessible via keyboard #1203
- Implement keyboard navigation of toolbar menus #1209
- CryptDrive page needs a logical tab order #1151
- Elements not accessible using the keyboard #1162
- Calendar event modal date-picker is cut-off at some screen resolutions #1280
- Visible focus #1206
- Rich Text
- Improvements to the Rich Text toolbar and layout for mobile usage [#1296](https://github.com/cryptpad/cryptpad/pull/1296)
- Calendar
- Handling the move of repeating events from a calendar to another [#1308](https://github.com/cryptpad/cryptpad/pull/1308)
- Kanban
- Changed positioning of kanban tag container on smaller screens [#1307](https://github.com/cryptpad/cryptpad/pull/1307)
- New option to increase the number of teams slots for premium users only [#1315](https://github.com/cryptpad/cryptpad/pull/1315)
- Improve licensing information, CryptPad code now complies with the [REUSE](https://reuse.software/) specifications [#1300](https://github.com/cryptpad/cryptpad/pull/1300)
- Deployment
- Basic configuration for Apache HTTPd [#1332](https://github.com/cryptpad/cryptpad/pull/1332)
- Add Docker health check [#1287](https://github.com/cryptpad/cryptpad/pull/1287)
- Cleanup
- Old // XXX comments [#1334](https://github.com/cryptpad/cryptpad/pull/1334)
- Outdated/misplaced files [#1327](https://github.com/cryptpad/cryptpad/pull/1327)
## Fixes
- Fix browser autocomplete issues (password, numbers, etc.) [#1342](https://github.com/cryptpad/cryptpad/pull/1342)
- Drive
- Container height fills screen [#1304](https://github.com/cryptpad/cryptpad/pull/1304)
- Context menu on mobile [#1301](https://github.com/cryptpad/cryptpad/pull/1301)
- OnlyOffice applications
- Use correct mime type for .wasm files (export functionality) [#1288](https://github.com/cryptpad/cryptpad/pull/1288)
- Fix filter functionality in Sheets [#1319](https://github.com/cryptpad/cryptpad/issues/1319)
- Form
- Fix an error upon importing a template in forms [#1316](https://github.com/cryptpad/cryptpad/pull/1316)
- Can now set form closing date/time on mobile [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Can now edit time options for poll questions on mobile [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Dates in CSV exports of forms are now in ISO (not timestamp) format [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Page breaks are no longer visible in conditional sections when condition is not met [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Final submission page now has margins [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Question blocks on mobile are now only draggable at the top of the block to make scrolling possible [#1305](https://github.com/cryptpad/cryptpad/pull/1305)
- Whiteboard
- Fix a few export-related issues [#1328](https://github.com/cryptpad/cryptpad/pull/1328)
- Calendar
- Reformat `www/calendar/export.js` [#1314](https://github.com/cryptpad/cryptpad/pull/1314)
- Fix a bug with stopping the recurrence of a calendar event [#1312](https://github.com/cryptpad/cryptpad/pull/1312)
- Calendar creates itself twice when navigating with the keyboard [#1250](https://github.com/cryptpad/cryptpad/issues/1250)
- Fix timezone in Daylight Saving Time issues [#1317](https://github.com/cryptpad/cryptpad/pull/1317)
- Translations
- Revise the translation of `zh` [#1329](https://github.com/cryptpad/cryptpad/pull/1329)
## Dependencies
- Added [Moment.js](http://momentjs.com/) for improved handling of dates in Calendar (added as part of [#1317](https://github.com/cryptpad/cryptpad/pull/1317))
## Deployment
We [fixed an issue with the Systemd service file and logging](https://github.com/cryptpad/cryptpad/commit/078095c3e25d39707bdaab7ec066ceed6cb7158b), you'll need to add the following lines to your `cryptpad.service` before continuing by following the upgrade notes below.
```diff
# Restart service after 10 seconds if node service crashes
RestartSec=2
+ # Proper logging to journald
+ StandardOutput=journal
+ StandardError=journal+console
User=cryptpad
Group=cryptpad
```
## Upgrade notes
If you are upgrading from a version older than `5.5.0` please read the upgrade notes of all versions between yours and `5.5.0` to avoid configuration issues.
To upgrade:
1. Reload the Systemd daemon, required due to the changes in the **Deployment** section
```bash
sudo systemctl daemon-reload
```
2. Stop your server
3. Get the latest code with git
```bash
git fetch origin --tags
git checkout 5.6.0
```
4. Restart your server
5. Review your instance's checkup page to ensure that you are passing all tests
# 5.5.0
## Features

View File

@ -61,7 +61,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at contact@cryptpad.fr. All
reported by contacting the project team at contact@cryptpad.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.

View File

@ -12,7 +12,7 @@ WORKDIR /cryptpad
# Copy CryptPad source code to the container
COPY . /cryptpad
RUN sed -i "s@//httpAddress: '::'@httpAddress: '0.0.0.0'@" /cryptpad/config/config.example.js
RUN sed -i "s@//httpAddress: 'localhost'@httpAddress: '0.0.0.0'@" /cryptpad/config/config.example.js
RUN sed -i "s@installMethod: 'unspecified'@installMethod: 'docker'@" /cryptpad/config/config.example.js
# Install dependencies
@ -26,6 +26,11 @@ FROM node:lts-slim
RUN groupadd cryptpad -g 4001
RUN useradd cryptpad -u 4001 -g 4001 -d /cryptpad
# Install wget for healthcheck
RUN apt-get update && apt-get install --no-install-recommends -y wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Copy cryptpad with installed modules
COPY --from=build --chown=cryptpad /cryptpad /cryptpad
USER cryptpad
@ -48,6 +53,9 @@ VOLUME /cryptpad/datastore
ENTRYPOINT ["/bin/bash", "/cryptpad/docker-entrypoint.sh"]
# Healthcheck
HEALTHCHECK --interval=1m CMD wget --no-verbose --tries=1 http://localhost:3000/ -q -O /dev/null || exit 1
# Ports
EXPOSE 3000 3001 3003

25
SECURITY.md Normal file
View File

@ -0,0 +1,25 @@
<!--
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# Security Policy
## Supported Versions
Considering the amount of resources necessary to backport security or bug fixes to previous, unsupported CryptPad versions, it's not something we do.
However, we quickly release new minor versions in case of need.
Please keep up with the latest release published here: https://github.com/cryptpad/cryptpad/releases
Note that every GitHub release page has an RSS compatible feed that you can subscribe on to be informed of every new release.
We do also communicate about this topic on:
- [Our blog](https://blog.cryptpad.org)
- [Our Matrix public space](https://matrix.to/#/#cryptpad:matrix.xwiki.com)
- [Our Mastodon account](https://fosstodon.org/@cryptpad)
## Reporting a Vulnerability
Vulnerabilities can be reported using the GitHub Security interface. You can also send us an email at security@cryptpad.org

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@ -74,12 +74,12 @@ module.exports = {
// httpSafeOrigin: "https://some-other-domain.xyz",
/* httpAddress specifies the address on which the nodejs server
* should be accessible. By default it will listen on 127.0.0.1
* (IPv4 localhost on most systems). If you want it to listen on
* all addresses, including IPv6, set this to '::'.
* should be accessible. By default it will listen on localhost
* (IPv4 & IPv6 if enabled). If you want it to listen on
* a specific address, specify it here. e.g '192.168.0.1'
*
*/
//httpAddress: '::',
//httpAddress: 'localhost',
/* httpPort specifies on which port the nodejs server should listen.
* By default it will serve content over port 3000, which is suitable
@ -117,6 +117,28 @@ module.exports = {
*/
// maxWorkers: 4,
/* =====================
* Sessions
* ===================== */
/* Accounts can be protected with an OTP (One Time Password) system
* to add a second authentication layer. Such accounts use a session
* with a given lifetime after which they are logged out and need
* to be re-authenticated. You can configure the lifetime of these
* sessions here.
*
* defaults to 7 days
*/
//otpSessionExpiration: 7*24, // hours
/* Registered users can be forced to protect their account
* with a Multi-factor Authentication (MFA) tool like a TOTP
* authenticator application.
*
* defaults to false
*/
//enforceMFA: false,
/* =====================
* Admin
* ===================== */

37
config/sso.example.js Normal file
View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
//const fs = require('node:fs');
module.exports = {
// Enable SSO login on this instance
enabled: false,
// Block registration for non-SSO users on this instance
enforced: false,
// Allow users to add an additional CryptPad password to their SSO account
cpPassword: false,
// You can also force your SSO users to add a CryptPad password
forceCpPassword: false,
// List of SSO providers
list: [
/*
{
name: 'google',
type: 'oidc',
url: 'https://accounts.google.com',
client_id: "{your_client_id}",
client_secret: "{your_client_secret}",
jwt_alg: 'RS256' (optional)
}, {
name: 'samltest',
type: 'saml',
url: 'https://samltest.id/idp/profile/SAML2/Redirect/SSO',
issuer: 'your-cryptpad-issuer-id',
cert: String or fs.readFileSync("./your/cert/location", "utf-8"),
privateKey: fs.readFileSync("./your/private/key/location", "utf-8"),
signingCert: fs.readFileSync("./your/signing/cert/location", "utf-8"),
}
*/
]
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

View File

@ -8,13 +8,12 @@ CKEDITOR.editorConfig = function( config ) {
// https://dev.ckeditor.com/ticket/10907
config.needsBrFiller= fixThings;
config.needsNbspFiller= fixThings;
config.disableObjectResizing = true;
config.removeButtons= 'Source,Maximize';
// magicline plugin inserts html crap into the document which is not part of the
// document itself and causes problems when it's sent across the wire and reflected back
config.removePlugins= 'resize,elementspath';
config.removePlugins= 'resize,elementspath,liststyle';
config.resize_enabled= false; //bottom-bar
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount,comments';
config.toolbarGroups= [
@ -35,7 +34,7 @@ CKEDITOR.editorConfig = function( config ) {
config.mathJaxLib = '/pad/mathjax/MathJax.js?config=TeX-AMS_HTML';
config.font_defaultLabel = 'Arial';
config.fontSize_defaultLabel = '16';
config.accessibility = 'true';
config.keystrokes = [
[ CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' ],
[ CKEDITOR.ALT + 122 /*F11*/, 'elementsPathFocus' ],
@ -53,7 +52,13 @@ CKEDITOR.editorConfig = function( config ) {
[ CKEDITOR.CTRL + 73 /*I*/, 'italic' ],
[ CKEDITOR.CTRL + 85 /*U*/, 'underline' ],
[ CKEDITOR.ALT + 109 /*-*/, 'toolbarCollapse' ]
[CKEDITOR.ALT + 109 /*-*/, 'toolbarCollapse' ],
[37 /* Left Arrow */, 'focusPreviousButton'],
[39 /* Right Arrow */, 'focusNextButton'],
//enter
[13, 'clickFocusedButton'],
//space bar
[32, 'clickFocusedButton']
];
//skin: 'moono-cryptpad,/pad/themes/moono-cryptpad/'

View File

@ -8,6 +8,7 @@ define([
'/components/chainpad-crypto/crypto.js',
'/common/common-util.js',
'/common/outer/network-config.js',
'/common/common-login.js',
'/common/common-credential.js',
'/components/chainpad/chainpad.dist.js',
'/common/common-realtime.js',
@ -24,14 +25,14 @@ define([
'/components/tweetnacl/nacl-fast.min.js',
'/components/scrypt-async/scrypt-async.min.js', // better load speed
], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
], function ($, Listmap, Crypto, Util, NetConfig, Login, Cred, ChainPad, Realtime, Constants, UI,
Feedback, h, LocalStore, Messages, nThen, Block, Hash, ServerCommand) {
var Exports = {
Cred: Cred,
Block: Block,
// this is depended on by non-customizable files
// be careful when modifying login.js
requiredBytes: 192,
requiredBytes: Login.requiredBytes,
};
var Nacl = window.nacl;
@ -44,511 +45,33 @@ define([
redirectTo = newPad.href;
}
};
if (window.location.hash) {
setRedirectTo();
}
if (window.location.hash) { setRedirectTo(); }
var allocateBytes = Exports.allocateBytes = function (bytes) {
var dispense = Cred.dispenser(bytes);
var opt = {};
// dispense 18 bytes of entropy for your encryption key
var encryptionSeed = dispense(18);
// 16 bytes for a deterministic channel key
var channelSeed = dispense(16);
// 32 bytes for a curve key
var curveSeed = dispense(32);
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
opt.curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
opt.curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
// 32 more for a signing key
var edSeed = opt.edSeed = dispense(32);
// 64 more bytes to seed an additional signing key
var blockKeys = opt.blockKeys = Block.genkeys(new Uint8Array(dispense(64)));
opt.blockHash = Block.getBlockHash(blockKeys);
// derive a private key from the ed seed
var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));
opt.edPrivate = Nacl.util.encodeBase64(signingKeypair.secretKey);
opt.edPublic = Nacl.util.encodeBase64(signingKeypair.publicKey);
var keys = opt.keys = Crypto.createEditCryptor(null, encryptionSeed);
// 24 bytes of base64
keys.editKeyStr = keys.editKeyStr.replace(/\//g, '-');
// 32 bytes of hex
var channelHex = opt.channelHex = Util.uint8ArrayToHex(channelSeed);
// should never happen
if (channelHex.length !== 32) { throw new Error('invalid channel id'); }
var channel64 = Util.hexToBase64(channelHex);
// we still generate a v1 hash because this function needs to deterministically
// derive the same values as it always has. New accounts will generate their own
// userHash values
opt.userHash = '/1/edit/' + [channel64, opt.keys.editKeyStr].join('/') + '/';
return opt;
};
var loginOptionsFromBlock = Exports.loginOptionsFromBlock = function (blockInfo) {
var opt = {};
var parsed = Hash.getSecrets('pad', blockInfo.User_hash);
opt.channelHex = parsed.channel;
opt.keys = parsed.keys;
opt.edPublic = blockInfo.edPublic;
return opt;
};
var loadUserObject = Exports.loadUserObject = function (opt, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var config = {
websocketURL: NetConfig.getWebsocketURL(),
channel: opt.channelHex,
data: {},
validateKey: opt.keys.validateKey, // derived validation key
crypto: Crypto.createEncryptor(opt.keys),
logLevel: 1,
classic: true,
ChainPad: ChainPad,
owners: [opt.edPublic]
};
var rt = opt.rt = Listmap.create(config);
rt.proxy
.on('ready', function () {
setTimeout(function () { cb(void 0, rt); });
})
.on('error', function (info) {
cb(info.type, {reason: info.message});
})
.on('disconnect', function (info) {
cb('E_DISCONNECT', info);
Exports.ssoAuth = function (provider, cb) {
var keys = Nacl.sign.keyPair();
var inviteToken = window.location.hash.slice(1);
localStorage.CP_sso_auth = JSON.stringify({
s: Nacl.util.encodeBase64(keys.secretKey),
p: Nacl.util.encodeBase64(keys.publicKey),
token: inviteToken
});
};
var isProxyEmpty = function (proxy) {
var l = Object.keys(proxy).length;
return l === 0 || (l === 2 && proxy._events && proxy.on);
};
var setMergeAnonDrive = function () {
Exports.mergeAnonDrive = 1;
};
Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, onOTP, cb) {
if (typeof(cb) !== 'function') { return; }
// Usernames are all lowercase. No going back on this one
uname = uname.toLowerCase();
// validate inputs
if (!Cred.isValidUsername(uname)) { return void cb('INVAL_USER'); }
if (!Cred.isValidPassword(passwd)) { return void cb('INVAL_PASS'); }
if (isRegister && !Cred.isLongEnoughPassword(passwd)) {
return void cb('PASS_TOO_SHORT');
}
// results...
var res = {
register: isRegister,
};
var RT, blockKeys, blockHash, blockUrl, Pinpad, rpc, userHash;
nThen(function (waitFor) {
// derive a predefined number of bytes from the user's inputs,
// and allocate them in a deterministic fashion
Cred.deriveFromPassphrase(uname, passwd, Exports.requiredBytes, waitFor(function (bytes) {
res.opt = allocateBytes(bytes);
blockHash = res.opt.blockHash;
blockKeys = res.opt.blockKeys;
}));
}).nThen(function (waitFor) {
// the allocated bytes can be used either in a legacy fashion,
// or in such a way that a previously unused byte range determines
// the location of a layer of indirection which points users to
// an encrypted block, from which they can recover the location of
// the rest of their data
// determine where a block for your set of keys would be stored
blockUrl = Block.getBlockUrl(res.opt.blockKeys);
var TOTP_prompt = function (err, cb) {
onOTP(function (code) {
ServerCommand(res.opt.blockKeys.sign, {
command: 'TOTP_VALIDATE',
code: code,
// TODO optionally allow the user to specify a lifetime for this session?
// this will require a little bit of server work
// and more UI/UX:
// ie. just a simple "remember me" checkbox?
// allow them to specify a lifetime for the session?
// "log me out after one day"?
ServerCommand(keys, {
command: 'SSO_AUTH',
provider: provider,
register: true
}, cb);
}, false, err);
};
Exports.ssoLogin = function () {
};
var done = waitFor();
var responseToDecryptedBlock = function (response, cb) {
response.arrayBuffer().then(arraybuffer => {
arraybuffer = new Uint8Array(arraybuffer);
var decryptedBlock = Block.decrypt(arraybuffer, blockKeys);
if (!decryptedBlock) {
console.error("BLOCK DECRYPTION ERROR");
return void cb("BLOCK_DECRYPTION_ERROR");
}
cb(void 0, decryptedBlock);
});
Exports.allocateBytes = Login.allocateBytes;
Exports.loadUserObject = Login.loadUserObject;
var setMergeAnonDrive = function (value) {
Exports.mergeAnonDrive = Boolean(value);
};
var TOTP_response;
nThen(function (w) {
Util.getBlock(blockUrl, {
// request the block without credentials
}, w(function (err, response) {
if (err === 401) {
return void console.log("Block requires 2FA");
}
if (err === 404 && response && response.reason) {
waitFor.abort();
w.abort();
/*
// the following block prevent users from re-using an old password
if (isRegister) { return void cb('HAS_PLACEHOLDER'); }
*/
return void cb('DELETED_USER', response);
}
// Some other error?
if (err) {
console.error(err);
w.abort();
return void done();
}
// If the block was returned without requiring authentication
// then we can abort the subsequent steps of this nested nThen
w.abort();
// decrypt the response and continue the normal procedure with its payload
responseToDecryptedBlock(response, function (err, decryptedBlock) {
if (err) {
// if a block was present but you were not able to decrypt it...
console.error(err);
waitFor.abort();
return void cb(err);
}
res.blockInfo = decryptedBlock;
done();
});
}));
}).nThen(function (w) {
// if you're here then you need to request a JWT
var done = w();
var tries = 3;
var ask = function () {
if (!tries) {
w.abort();
waitFor.abort();
return void cb('TOTP_ATTEMPTS_EXHAUSTED');
}
tries--;
TOTP_prompt(tries !== 2, function (err, response) {
// ask again until your number of tries are exhausted
if (err) {
console.error(err);
console.log("Normal failure. Asking again...");
return void ask();
}
if (!response || !response.bearer) {
console.log(response);
console.log("Unexpected failure. No bearer token. Asking again");
return void ask();
}
console.log("Successfully retrieved a bearer token");
res.TOTP_token = TOTP_response = response;
done();
});
};
ask();
}).nThen(function (w) {
Util.getBlock(blockUrl, TOTP_response, function (err, response) {
if (err) {
w.abort();
console.error(err);
return void cb('BLOCK_ERROR_3');
}
responseToDecryptedBlock(response, function (err, decryptedBlock) {
if (err) {
waitFor.abort();
return void cb(err);
}
res.blockInfo = decryptedBlock;
done();
});
});
});
}).nThen(function (waitFor) {
// we assume that if there is a block, it was created in a valid manner
// so, just proceed to the next block which handles that stuff
if (res.blockInfo) { return; }
var opt = res.opt;
// load the user's object using the legacy credentials
loadUserObject(opt, waitFor(function (err, rt) {
if (err) {
waitFor.abort();
if (err === 'EDELETED') { return void cb('DELETED_USER', rt); }
return void cb(err);
}
// if a proxy is marked as deprecated, it is because someone had a non-owned drive
// but changed their password, and couldn't delete their old data.
// if they are here, they have entered their old credentials, so we should not
// allow them to proceed. In time, their old drive should get deleted, since
// it will should be pinned by anyone's drive.
if (rt.proxy[Constants.deprecatedKey]) {
waitFor.abort();
return void cb('NO_SUCH_USER', res);
}
if (isRegister && isProxyEmpty(rt.proxy)) {
// If they are trying to register,
// and the proxy is empty, then there is no 'legacy user' either
// so we should just shut down this session and disconnect.
//rt.network.disconnect();
return; // proceed to the next async block
}
// they tried to just log in but there's no such user
// and since we're here at all there is no modern-block
if (!isRegister && isProxyEmpty(rt.proxy)) {
//rt.network.disconnect(); // clean up after yourself
waitFor.abort();
return void cb('NO_SUCH_USER', res);
}
// they tried to register, but those exact credentials exist
if (isRegister && !isProxyEmpty(rt.proxy)) {
//rt.network.disconnect();
waitFor.abort();
Feedback.send('LOGIN', true);
return void cb('ALREADY_REGISTERED', res);
}
// if you are here, then there is no block, the user is trying
// to log in. The proxy is **not** empty. All values assigned here
// should have been deterministically created using their credentials
// so setting them is just a precaution to keep things in good shape
res.proxy = rt.proxy;
res.realtime = rt.realtime;
res.network = rt.network;
// they're registering...
res.userHash = opt.userHash;
res.userName = uname;
// export their signing key
res.edPrivate = opt.edPrivate;
res.edPublic = opt.edPublic;
// export their encryption key
res.curvePrivate = opt.curvePrivate;
res.curvePublic = opt.curvePublic;
if (shouldImport) { setMergeAnonDrive(); }
// don't proceed past this async block.
waitFor.abort();
// We have to call whenRealtimeSyncs asynchronously here because in the current
// version of listmap, onLocal calls `chainpad.contentUpdate(newValue)`
// asynchronously.
// The following setTimeout is here to make sure whenRealtimeSyncs is called after
// `contentUpdate` so that we have an update userDoc in chainpad.
setTimeout(function () {
Realtime.whenRealtimeSyncs(rt.realtime, function () {
// the following stages are there to initialize a new drive
// if you are registering
LocalStore.login(res.userHash, undefined, res.userName, function () {
setTimeout(function () { cb(void 0, res); });
});
});
});
}));
}).nThen(function (waitFor) { // MODERN REGISTRATION / LOGIN
var opt;
if (res.blockInfo) {
opt = loginOptionsFromBlock(res.blockInfo);
userHash = res.blockInfo.User_hash;
//console.error(opt, userHash);
} else {
console.log("allocating random bytes for a new user object");
opt = allocateBytes(Nacl.randomBytes(Exports.requiredBytes));
// create a random v2 hash, since we don't need backwards compatibility
userHash = opt.userHash = Hash.createRandomHash('drive');
var secret = Hash.getSecrets('drive', userHash);
opt.keys = secret.keys;
opt.channelHex = secret.channel;
}
// according to the location derived from the credentials which you entered
loadUserObject(opt, waitFor(function (err, rt) {
if (err) {
waitFor.abort();
if (err === 'EDELETED') { return void cb('DELETED_USER', rt); }
return void cb('MODERN_REGISTRATION_INIT');
}
//console.error(JSON.stringify(rt.proxy));
// export the realtime object you checked
RT = rt;
var proxy = rt.proxy;
if (isRegister && !isProxyEmpty(proxy) && (!proxy.edPublic || !proxy.edPrivate)) {
console.error("INVALID KEYS");
console.log(JSON.stringify(proxy));
return;
}
res.proxy = rt.proxy;
res.realtime = rt.realtime;
res.network = rt.network;
// they're registering...
res.userHash = userHash;
res.userName = uname;
// somehow they have a block present, but nothing in the user object it specifies
// this shouldn't happen, but let's send feedback if it does
if (!isRegister && isProxyEmpty(rt.proxy)) {
// this really shouldn't happen, but let's handle it anyway
Feedback.send('EMPTY_LOGIN_WITH_BLOCK');
//rt.network.disconnect(); // clean up after yourself
waitFor.abort();
return void cb('NO_SUCH_USER', res);
}
// they tried to register, but those exact credentials exist
if (isRegister && !isProxyEmpty(rt.proxy)) {
//rt.network.disconnect();
waitFor.abort();
res.blockHash = blockHash;
if (shouldImport) {
setMergeAnonDrive();
}
return void cb('ALREADY_REGISTERED', res);
}
if (!isRegister && !isProxyEmpty(rt.proxy)) {
waitFor.abort();
if (shouldImport) {
setMergeAnonDrive();
}
var l = Util.find(rt.proxy, ['settings', 'general', 'language']);
var LS_LANG = "CRYPTPAD_LANG";
if (l) {
localStorage.setItem(LS_LANG, l);
}
if (res.TOTP_token && res.TOTP_token.bearer) {
LocalStore.setSessionToken(res.TOTP_token.bearer);
}
return void LocalStore.login(undefined, blockHash, uname, function () {
cb(void 0, res);
});
}
if (isRegister && isProxyEmpty(rt.proxy)) {
proxy.edPublic = opt.edPublic;
proxy.edPrivate = opt.edPrivate;
proxy.curvePublic = opt.curvePublic;
proxy.curvePrivate = opt.curvePrivate;
proxy.login_name = uname;
proxy[Constants.displayNameKey] = uname;
if (shouldImport) {
setMergeAnonDrive();
} else {
proxy.version = 11;
}
Feedback.send('REGISTRATION', true);
} else {
Feedback.send('LOGIN', true);
}
setTimeout(waitFor(function () {
Realtime.whenRealtimeSyncs(rt.realtime, waitFor());
}));
}));
}).nThen(function (waitFor) {
require(['/common/pinpad.js'], waitFor(function (_Pinpad) {
console.log("loaded rpc module");
Pinpad = _Pinpad;
}));
}).nThen(function (waitFor) {
// send an RPC to store the block which you created.
console.log("initializing rpc interface");
Pinpad.create(RT.network, Block.keysToRPCFormat(res.opt.blockKeys), waitFor(function (e, _rpc) {
if (e) {
waitFor.abort();
console.error(e); // INVALID_KEYS
return void cb('RPC_CREATION_ERROR');
}
rpc = _rpc;
console.log("rpc initialized");
}));
}).nThen(function (waitFor) {
console.log("creating request to publish a login block");
// Finally, create the login block for the object you just created.
var toPublish = {};
toPublish[Constants.userHashKey] = userHash;
toPublish.edPublic = RT.proxy.edPublic;
Block.writeLoginBlock({
blockKeys: blockKeys,
content: toPublish
}, waitFor(function (e) {
if (e) {
console.error(e);
waitFor.abort();
return void cb(e);
}
}));
}).nThen(function (waitFor) {
// confirm that the block was actually written before considering registration successful
Util.fetch(blockUrl, waitFor(function (err /*, block */) {
if (err) {
console.error(err);
waitFor.abort();
return void cb(err);
}
console.log("blockInfo available at:", blockHash);
LocalStore.login(undefined, blockHash, uname, function () {
cb(void 0, res);
});
}));
});
};
Exports.redirect = function () {
if (redirectTo) {
var h = redirectTo;
@ -569,14 +92,17 @@ define([
};
var hashing;
Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, onOTP, testing, test) {
Exports.loginOrRegisterUI = function (config) {
let { uname, token, shouldImport, cb } = config;
if (hashing) { return void console.log("hashing is already in progress"); }
hashing = true;
setMergeAnonDrive(shouldImport);
var proceed = function (result) {
hashing = false;
// NOTE: test is also use as a cb for the install page
if (test && typeof test === "function" && test(result)) { return; }
if (cb && typeof cb === "function" && cb(result)) { return; }
LocalStore.clearLoginToken();
Realtime.whenRealtimeSyncs(result.realtime, function () {
Exports.redirect();
@ -594,11 +120,12 @@ define([
// We need a setTimeout(cb, 0) otherwise the loading screen is only displayed
// after hashing the password
window.setTimeout(function () {
Exports.loginOrRegister(uname, passwd, isRegister, shouldImport, onOTP, function (err, result) {
var proxy;
Login.loginOrRegister(config, function (err, result) {
var proxy = {};
if (result) { proxy = result.proxy; }
if (err) {
console.warn(err);
switch (err) {
case 'NO_SUCH_USER':
UI.removeLoadingScreen(function () {
@ -668,6 +195,9 @@ define([
});
break;
case 'E_RESTRICTED':
if (token) {
return UI.errorLoadingScreen(Messages.register_invalidToken);
}
UI.errorLoadingScreen(Messages.register_registrationIsClosed);
break;
default: // UNHANDLED ERROR
@ -677,8 +207,6 @@ define([
return;
}
//if (testing) { return void proceed(result); }
if (!(proxy.curvePrivate && proxy.curvePublic &&
proxy.edPrivate && proxy.edPublic)) {

View File

@ -98,7 +98,7 @@ define([
return h('a', attrs, [icon, text]);
};
Pages.versionString = "5.5.0";
Pages.versionString = "5.7.0";
var customURLs = Pages.customURLs = {};
(function () {

View File

@ -11,13 +11,17 @@ define([
], function (h, UI, Msg, Pages, Config) {
return function () {
document.title = Msg.login_login;
var ssoEnabled = (Config.sso && Config.sso.list && Config.sso.list.length) ?'': '.cp-hidden';
var ssoEnforced = (Config.sso && Config.sso.force) ? '.cp-hidden' : '';
return [h('div#cp-main', [
Pages.infopageTopbar(),
h('div.container.cp-container', [
h('div.row.cp-page-title', h('h1', Msg.login_login)),
h('div.row', [
h('div.col-md-3'),
h('div#userForm.form-group.hidden.col-md-6', [
h('div.col-md-3'+ssoEnforced),
h('div#userForm.form-group.col-md-6'+ssoEnforced, [
h('div.cp-login-instance', Msg._getKey('login_instance', [ Pages.Instance.name ])),
h('div.big-container', [
h('div.input-container', [
@ -48,17 +52,22 @@ define([
]),
h('div.extra', [
(Config.restrictRegistration?
undefined:
h('div'):
h('a#register', {
href: "/register/",
}, Msg.login_register)
),
h('button.login', Msg.login_login)
])
h('button.login', Msg.login_login),
]),
h('div.col-md-3')
]),
h('div.row', [
h('div.col-md-3'+ssoEnforced),
h('div.col-md-3'+ssoEnabled),
h('div#ssoForm.form-group.col-md-6'+ssoEnabled, [
h('div.cp-login-sso', Msg.sso_login_description)
]),
h('div.col-md-3'+ssoEnabled),
]),
h('div.row.cp-login-encryption', [
h('div.col-md-3'),
h('div.col-md-6', Msg.register_warning_note),
h('div.col-md-3'),

View File

@ -14,6 +14,9 @@ define([
document.title = Msg.register_header;
var tos = $(UI.createCheckbox('accept-terms')).find('.cp-checkmark-label').append(Msg.register_acceptTerms).parent()[0];
var ssoEnabled = (Config.sso && Config.sso.list && Config.sso.list.length) ?'': '.cp-hidden';
var ssoEnforced = (Config.sso && Config.sso.force) ? '.cp-hidden' : '';
var termsLink = Pages.customURLs.terms;
$(tos).find('a').attr({
href: termsLink,
@ -33,27 +36,29 @@ define([
];
};
if (Config.restrictRegistration) {
return frame([
h('div.cp-restricted-registration', [
h('p', Msg.register_registrationIsClosed),
])
]);
}
var termsCheck;
if (termsLink) {
termsCheck = h('div.checkbox-container', tos);
}
var closed = Config.restrictRegistration;
if (closed) {
$('body').addClass('cp-register-closed');
}
return frame([
h('div.cp-restricted-registration', [
h('p', Msg.register_registrationIsClosed),
]),
h('div.row.cp-register-det', [
h('div#data.hidden.col-md-6', [
h('h2', Msg.register_notes_title),
Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes)
]),
h('div.col-md-3.cp-closed-filler'+ssoEnabled, h('div')),
h('div.cp-reg-form.col-md-6', [
h('div#userForm.form-group.hidden', [
h('div#userForm.form-group'+ssoEnforced, [
h('div.cp-register-instance', [
Msg._getKey('register_instance', [Pages.Instance.name]),
h('br'),
@ -95,9 +100,13 @@ define([
UI.createCheckbox('import-recent', Msg.register_importRecent, true)
]),
termsCheck,
h('button#register', Msg.login_register)
])
h('button#register', Msg.login_register),
]),
h('div#ssoForm.form-group.col-md-6'+ssoEnabled, [
h('div.cp-register-sso', Msg.sso_register_description)
]),
]),
h('div.col-md-3.cp-closed-filler'+ssoEnabled),
])
]);
};

View File

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/api/config',
'jquery',
'/common/hyperscript.js',
'/common/common-interface.js',
'/customize/messages.js',
'/customize/pages.js'
], function (Config, $, h, UI, Msg, Pages) {
return function () {
document.title = Msg.ssoauth_header;
var frame = function (content) {
return [
h('div#cp-main', [
Pages.infopageTopbar(),
h('div.container.cp-container', [
h('div.row.cp-page-title', h('h1', Msg.ssoauth_header)),
].concat(content)),
Pages.infopageFooter(),
]),
];
};
return frame([
h('div.row', [
h('div.hidden.col-md-3'),
h('div#userForm.form-group.col-md-6.cp-ssoauth-pw', [
h('p.cp-isregister.cp-login-instance', Msg.ssoauth_form_hint_register),
h('p.cp-islogin.cp-login-instance', Msg.ssoauth_form_hint_login),
h('input.form-control#password', {
type: 'password',
placeholder: Msg.login_password,
}),
h('input.form-control.cp-isregister#passwordconfirm', {
type: 'password',
placeholder: Msg.login_confirm,
}),
h('div.cp-ssoauth-button.extra',
h('div'),
h('button.login#cp-ssoauth-button', Msg.continue)
)
]),
h('div.hidden.col-md-3'),
])
]);
};
});

View File

@ -6,26 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-or-later
# Customizing CryptPad
In order allow a variety of features to be changed and to allow site-specific changes
to CryptPad apps while still keeping the git repository pristine, this directory exists
to allow a set of hooks to be run.
The server is configured to load files from the `/customize/` path preferentially from
`cryptpad/customize/`, and to fall back to `cryptpad/customize.dist/` if they are not found
If you wish to customize cryptpad, please **copy**
`/customize.dist/` to `/customize` and then edit it there, this way you will still be able
to pull from (and make pull requests to (!) the git repository.
## Files you may be interested in
* index.html is the main page
* main.js contains javascript for the home page
* application_config.js allows you to modify settings used by the various applications
* messages.js contains functions for applying translations to various pages
* look inside `/translations/` for the rest of the files which contain translated strings
* `/share/` implements an iframe RPC which allows multiple domains to access the same localStorage
* `/src/` contains source files for html and css (in the form of html templates and .less stylesheets)
All other content which is placed in this directory will be referencable at the `/customize/`
URL location.
This tutorial is part of our documentation, you can find it in our administrator guide: https://docs.cryptpad.org/en/admin_guide/customization.html

View File

@ -1,3 +1,9 @@
/*
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@import (reference) "./browser.less";
@import (reference) './leftside-menu.less';
@import (reference) "./tools.less";
@ -48,7 +54,9 @@
width: 100%;
height: 24px;
margin: 0;
display: inline-block;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
//align-items: center;
//justify-content: center;
@ -661,6 +669,17 @@
font-size: 18px;
}
}
.cp-app-drive-element-icon {
font-size: 0.9rem;
margin: 0;
margin-right: 0.3rem;
}
.cp-app-drive-element-name-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.cp-app-drive-element-state {
left: 3px;
}
@ -672,7 +691,7 @@
max-height: 100px;
background: @cp_drive-thumb-bg;
& ~ .fa, & ~ .cptools {
display: inline;
display: none;
font-size: 17px;
position: absolute;
top: 3px;
@ -695,6 +714,9 @@
}
div.cp-app-drive-content-list {
.cp-app-drive-element-icon {
display: none !important;
}
.cp-app-drive-element-grid {
display: none;
}

View File

@ -62,6 +62,7 @@
}
.cp-dropdown-content {
list-style-type: none;
display: none;
position: absolute;
background-color: @cp_dropdown-bg;

View File

@ -63,7 +63,7 @@
.cp-loading-container {
width: 700px;
max-width: 90vw;
height: 236px;
min-height: 236px;
max-height: calc(100vh - 20px);
margin: 50px;
flex-shrink: 0;
@ -106,6 +106,7 @@
color: @cp_loading-fg;
text-align: left;
display: none;
overflow-y: auto;
a {
color: @cp_loading-link;
}

View File

@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@import (reference) "../include/infopages.less";
@import (reference) "../include/colortheme-all.less";
@import (reference) "../include/alertify.less";
@import (reference) "../include/forms.less";
.login_main() {
#userForm, #ssoForm {
.cp-shadow();
background-color: @cp_static-card-bg;
padding: 10px;
margin: 0;
border-radius: @infopages-radius-L;
.cp-login-instance, .cp-login-sso-description {
margin-bottom: 5px;
}
.form-control {
border-radius: @infopages-radius;
color: @cryptpad_text_col;
background-color: @cp_forms-bg;
margin-bottom: 10px;
&:focus {
border-color: @cryptpad_color_brand;
}
.tools_placeholder-color();
}
.checkbox-container {
color: @cryptpad_text_col;
}
}
#ssoForm {
margin-top: 10px;
button {
margin: 0 5px;
}
.cp-login-sso {
display: flex;
align-items: center;
}
}
.cp-default-label {
display: none;
}
.extra {
margin-top: 1em;
button.login {
margin-right: 0px;
}
}
}

View File

@ -41,6 +41,44 @@
}
}
.cp-loading-missing-mfa {
.cp-settings-qr-container {
display: flex;
align-items: center;
justify-content: space-evenly;
.cp-settings-qr-code {
input {
max-width: 250px;
}
button {
margin-top: 10px;
}
}
}
.cp-settings-qr {
img {
border: 10px solid white;
border-radius: 10px;
}
margin: 10px 10px 10px 0;
}
.cp-password-container {
flex-wrap: wrap;
gap: 0.5rem;
justify-content: flex-start;
input {
flex-shrink: 1;
max-width: 400px;
}
label {
width: 100%;
font-weight: unset;
margin-bottom: 5px;
}
}
}
// Properties modal
.cp-app-prop {
margin-bottom: 10px;

View File

@ -9,6 +9,7 @@
@import (reference) "../include/alertify.less";
@import (reference) "../include/checkmark.less";
@import (reference) "../include/forms.less";
@import (reference) "../include/login.less";
&.cp-page-login {
.infopages_main();
@ -25,42 +26,18 @@
}
}
.cp-container {
#userForm {
.cp-shadow();
background-color: @cp_static-card-bg;
padding: 10px;
margin: 0 10px 10px 10px;
border-radius: @infopages-radius-L;
.cp-login-instance {
margin-bottom: 5px;
}
.form-control {
border-radius: @infopages-radius;
color: @cryptpad_text_col;
background-color: @cp_forms-bg;
margin-bottom: 10px;
&:focus {
border-color: @cryptpad_color_brand;
}
.tools_placeholder-color();
}
.checkbox-container {
color: @cryptpad_text_col;
}
.cp-hidden {
display: none !important;
}
.login_main();
.align-items-center {
box-shadow: 0 5px 15px @cp_shadow-color;
background: @cryptpad_color_white;
}
.extra {
margin-top: 1em;
button.login {
margin-right: 0px;
}
}
.cp-default-label {
display: none;
}
.cp-login-encryption {
margin-top: 10px;
}
.cp-password-form {

View File

@ -9,6 +9,7 @@
@import (reference) "../include/alertify.less";
@import (reference) "../include/checkmark.less";
@import (reference) "../include/forms.less";
@import (reference) "../include/login.less";
&.cp-page-register {
.infopages_main();
@ -17,6 +18,20 @@
.alertify_main();
.checkmark_main(20px);
&:not(.cp-register-closed) {
.cp-restricted-registration {
display: none;
}
.cp-closed-filler {
display: none;
}
}
&.cp-register-closed {
div.cp-register-det {
display: none;
}
}
.cp-container {
.form-group {
.cp-register-instance {
@ -91,32 +106,25 @@
z-index: 0;
}
}
#userForm {
padding: 15px;
background-color: @cp_static-card-bg;
.cp-hidden {
display: none !important;
}
.login_main();
#userForm, #ssoForm {
position: relative;
z-index: 2;
margin-bottom: 100px;
border-radius: @infopages-radius-L;
.cp-shadow();
.form-control {
border-radius: @infopages-radius;
color: @cryptpad_text_col;
background-color: @cp_forms-bg;
margin-bottom: 10px;
&:focus {
border-color: @cryptpad_color_brand;
}
.tools_placeholder-color();
}
max-width: 100%;
padding: 15px;
.checkbox-container {
margin-top: 0.5rem;
color: @cryptpad_text_col;
}
button#register {
margin-top: 10px;
}
}
#ssoForm {
margin-top: 15px;
}
.cp-register-notes {
ul.cp-notes-list {

View File

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@import (reference) "../include/infopages.less";
@import (reference) "../include/colortheme-all.less";
@import (reference) "../include/alertify.less";
@import (reference) "../include/forms.less";
@import (reference) "../include/login.less";
&.cp-page-ssoauth {
.infopages_main();
.forms_main();
.alertify_main();
.form-group {
.extra {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
}
div.cp-ssoauth-pw {
display: none;
width: 100%;
}
.cp-container {
.login_main();
}
&.cp-register {
.cp-islogin {
display: none;
}
#passwordconfirm {
margin-bottom: 0px !important;
}
}
&.cp-login {
.cp-isregister {
display: none;
}
#password {
margin-bottom: 0px !important;
}
}
}

View File

@ -69,6 +69,8 @@ $(function () {
require([ '/install/main.js' ], function () {});
} else if (/^\/recovery\//.test(pathname)) {
require([ '/recovery/main.js' ], function () {});
} else if (/^\/ssoauth\//.test(pathname)) {
require([ '/ssoauth/main.js' ], function () {});
} else if (/^\/login\//.test(pathname)) {
require([ '/login/main.js' ], function () {});
} else if (/^\/($|^\/index\.html$)/.test(pathname)) {

View File

@ -6,32 +6,4 @@ SPDX-License-Identifier: AGPL-3.0-or-later
# Translations
Translations can now be made using [Weblate](https://weblate.cryptpad.org). We may still accept PRs for the internal translation files, but we won't provide support for this. See the state of the translated languages:
![](https://weblate.cryptpad.org/widgets/cryptpad/-/app/multi-auto.svg)
## Request a new language
* New languages require some minor changes in the code which we have to apply manually. If you want to translate CryptPad into a new language, please send us an email at weblate@cryptpad.fr
* Please note that members of the current development team are only fluent in English and French, so additional languages will require ongoing contributions in order to stay up to date.
* If a language becomes too outdated, we will consider dropping it from the available language until it is fixed.
* Please refrain from asking for a new language if you are not able to provide a sufficiently translated version and maintain it or to find other users that can do so.
## Become a reviewer
* We need reviewers for all the languages except English and French. Their job will be to approve/reject/discuss the translations made by other contributors to this language, using the Weblate UI.
* If you want to become a reviewer for a language, please write us an email at weblate@cryptpad.fr
* Please tell us if you want to stop being a reviewer (even momentarily) so that we can find a replacement as soon as posible
## Translate an existing language
* All translations can be done using the Weblate UI. For better help about how to use the tool, please check the [Weblate documentation](https://docs.weblate.org/en/latest/).
* Our Weblate instance is configured to always require approval for changes.
### Update an existing translation
* Existing translations that have already been approved can't be changed directly. Users can only make suggestions, and the reviewers will have to approve the changes if they agree.
### Translate a new key
* Untranslated strings can be changed by any user and the translation can be edited by others as long as it has not been approved by a reviewer.
This tutorial is part of our documentation, you can find it in our how to contribute guide: https://docs.cryptpad.org/en/how_to_contribute.html#translate-cryptpad

View File

@ -14,6 +14,10 @@ Restart=always
# Restart service after 10 seconds if node service crashes
RestartSec=2
# Proper logging to journald
StandardOutput=journal
StandardError=journal+console
User=cryptpad
Group=cryptpad
# modify to match your working directory

View File

@ -14,6 +14,8 @@ server {
# Let's Encrypt webroot
include letsencrypt-webroot;
# Include mime.types to be able to support .mjs files (see "types" below)
include mime.types;
# CryptPad serves static assets over these two domains.
# `main_domain` is what users will enter in their address bar.
@ -166,11 +168,6 @@ server {
# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly in this privileged scope
if ($uri ~ ^\/unsafeiframe\/inner\.html.*$) { set $unsafe 1; }
# draw.io uses inline script tags in it's index.html. The hashes are added here.
if ($uri ~ ^\/components\/drawio\/src\/main\/webapp\/index.html.*$) {
set $scriptSrc "'self' 'sha256-dLMFD7ijAw6AVaqecS7kbPcFFzkxQ+yeZSsKpOdLxps=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: https://${main_domain}";
}
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
if ($unsafe) {
set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: https://${main_domain}";
@ -179,6 +176,11 @@ server {
# Finally, set all the rules you composed above.
add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc; frame-ancestors $frameAncestors";
# Add support for .mjs files used by pdfjs
types {
application/javascript mjs;
}
# The nodejs process can handle all traffic whether accessed over websocket or as static assets
# We prefer to serve static content from nginx directly and to leave the API server to handle
# the dynamic content that only it can manage. This is primarily an optimization

39
docs/example.httpd.conf Normal file
View File

@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# This file is included strictly as an example of how Apache httpd can be
# configured to work with CryptPad. If you are using CryptPad in production
# and require professional support please contact sales@cryptpad.fr
# This configuration requires mod_ssl, mod_socache_shmcb, mod_proxy,
# mod_proxy_http and mod_headers
Listen 443
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLProxyCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLProxyProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLSessionCache "shmcb:logs/ssl_scache(512000)"
SSLSessionCacheTimeout 86400
SSLSessionTickets off
SSLUseStapling on
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
<VirtualHost *:443>
ServerName cryptpad.your-domain.com
ServerAlias sandbox.your-domain.com
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
BrowserMatch "MSIE [2-5]" \
nokeepalive ssl-unclean-shutdown \
downgrade-1.0 force-response-1.0
Protocols h2 http/1.1
LimitRequestBody 157286400
ProxyPass / http://localhost:3000/ upgrade=websocket
ProxyPassReverse / http://localhost:3000/
</VirtualHost>

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6 */
const WebSocketServer = require('ws').Server;
const NetfluxSrv = require('chainpad-server');
const Decrees = require("./decrees");

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6, node: true */
const nThen = require('nthen');
const Pins = require('./pins');
const Util = require("./common-util");
@ -13,6 +12,9 @@ const Core = require("./commands/core");
const Metadata = require("./commands/metadata");
const Meta = require("./metadata");
const Logger = require("./log");
const plugins = require("./plugin-manager");
let SSOUtils = plugins.SSO && plugins.SSO.utils;
const Path = require("path");
const Fse = require("fs-extra");
@ -200,6 +202,12 @@ COMMANDS.start = (edPublic, blockId, reason) => {
}
Log.info('MODERATION_ACCOUNT_BLOCK', safeKey, waitFor());
}));
if (!SSOUtils) { return; }
SSOUtils.deleteAccount(Env, blockId, waitFor((err) => {
if (err) {
return Log.error('MODERATION_ACCOUNT_BLOCK_SSO', err, waitFor());
}
}));
}));
}).nThen((waitFor) => {
var report = {
@ -289,6 +297,12 @@ COMMANDS.restore = (edPublic) => {
}
Log.info('MODERATION_ACCOUNT_BLOCK_RESTORE', safeKey, waitFor());
}));
if (!SSOUtils) { return; }
SSOUtils.restoreAccount(Env, blockId, waitFor(function (err) {
if (err) {
return Log.error('MODERATION_ACCOUNT_BLOCK_RESTORE_SSO', err, waitFor());
}
}));
}));
}).nThen((waitFor) => {
deleteReport(Env, safeKey, waitFor((err) => {

View File

@ -5,6 +5,7 @@
const Block = require("../commands/block");
const MFA = require("../storage/mfa");
const Util = require("../common-util");
const Sessions = require("../storage/sessions");
const Commands = module.exports;
@ -55,8 +56,16 @@ const writeBlock = Commands.WRITE_BLOCK = function (Env, body, cb) {
};
writeBlock.complete = function (Env, body, cb) {
const { content } = body;
Block.writeLoginBlock(Env, content, cb);
const { publicKey, content, session } = body;
Block.writeLoginBlock(Env, content, (err) => {
if (err) { return void cb(err); }
if (!session) { return void cb(); }
const proof = Util.tryParse(content.registrationProof);
const oldKey = proof && proof[0];
Sessions.update(Env, publicKey, oldKey, session, "", cb);
});
};
// Remove a login block IFF
@ -73,8 +82,8 @@ const removeBlock = Commands.REMOVE_BLOCK = function (Env, body, cb) {
};
removeBlock.complete = function (Env, body, cb) {
const { publicKey, reason } = body;
Block.removeLoginBlock(Env, publicKey, reason, cb);
const { publicKey, edPublic, reason } = body;
Block.removeLoginBlock(Env, publicKey, reason, edPublic, cb);
};

View File

@ -12,6 +12,7 @@ const MFA = require("../storage/mfa");
const Sessions = require("../storage/sessions");
const BlockStore = require("../storage/block");
const Block = require("../commands/block");
const config = require("../load-config");
const Commands = module.exports;
@ -61,21 +62,40 @@ var decode32 = S => {
};
// Decide expire time
// Allow user settings?
var EXPIRATION = 7 * 24 * 3600 * 1000; // Sessions are valid 7 days
var EXPIRATION = (config.otpSessionExpiration || 7 * 24) * 3600 * 1000;
// Create a session with a token for the given public key
const makeSession = (Env, publicKey, cb) => {
const sessionId = Sessions.randomId();
const makeSession = (Env, publicKey, oldKey, ssoSession, cb) => {
const sessionId = ssoSession || Sessions.randomId();
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
// For password change, we need to get the sso session associated to the old block key
// In other cases (login and totp_setup), the sso session is associated to the current block
oldKey = oldKey || publicKey; // use the current block if no old key
let isUpdate = false;
nThen(function (w) {
if (!ssoSession || !SSOUtils) { return; }
// If we have an session token, confirm this is an sso account
SSOUtils.readBlock(Env, oldKey, w((err) => {
if (err === 'ENOENT') { return; } // No sso block, no need to update the session
if (err) {
w.abort();
return void cb('TOTP_VALIDATE_READ_SSO');
}
// We have an existing session for an SSO account: update the existing session
isUpdate = true;
}));
}).nThen(function (w) {
// store the token
Sessions.write(Env, publicKey, sessionId, JSON.stringify({
let sessionData = {
mfa: {
type: 'otp',
exp: (+new Date()) + EXPIRATION
}
}), w(function (err) {
};
var then = w(function (err) {
if (err) {
Env.Log.error("TOTP_VALIDATE_SESSION_WRITE", {
error: Util.serializeError(err),
@ -86,7 +106,12 @@ const makeSession = (Env, publicKey, cb) => {
return void cb("SESSION_WRITE_ERROR");
}
// else continue
}));
});
if (isUpdate) {
Sessions.update(Env, publicKey, oldKey, sessionId, JSON.stringify(sessionData), then);
} else {
Sessions.write(Env, publicKey, sessionId, JSON.stringify(sessionData), then);
}
}).nThen(function () {
cb(void 0, {
bearer: sessionId,
@ -207,7 +232,7 @@ const TOTP_SETUP = Commands.TOTP_SETUP = function (Env, body, cb) {
// There's still a little bit more to do and it could still fail.
TOTP_SETUP.complete = function (Env, body, cb) {
// the OTP code should have already been validated
var { publicKey, secret, contact } = body;
var { publicKey, secret, contact, session } = body;
// the device from which they configure MFA settings
// is assumed to be safe, so we'll respond with a JWT token
@ -257,7 +282,7 @@ TOTP_SETUP.complete = function (Env, body, cb) {
// we have already stored the MFA data, which will cause access to the resource to be restricted to the provided TOTP secret.
// we attempt to create a session as a matter of convenience - so if it fails
// that just means they'll be forced to authenticate
makeSession(Env, publicKey, cb);
makeSession(Env, publicKey, null, session, cb);
});
};
@ -305,12 +330,12 @@ So, we should:
2. send them the token
*/
var { publicKey } = body;
makeSession(Env, publicKey, cb);
var { publicKey, session } = body;
makeSession(Env, publicKey, null, session, cb);
};
// Same as TOTP_VALIDATE but without making a session at the end
const check = Commands.TOTP_CHECK = function (Env, body, cb) {
const check = Commands.TOTP_MFA_CHECK = function (Env, body, cb) {
var { publicKey, auth } = body;
const code = auth;
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
@ -436,7 +461,8 @@ const writeBlock = Commands.TOTP_WRITE_BLOCK = function (Env, body, cb) {
writeBlock.complete = function (Env, body, cb) {
const { publicKey, content } = body;
const { publicKey, content, session } = body;
let oldKey;
nThen(function (w) {
// Write new block
Block.writeLoginBlock(Env, content, w((err) => {
@ -448,7 +474,7 @@ writeBlock.complete = function (Env, body, cb) {
}).nThen(function (w) {
// Copy MFA settings
const proof = Util.tryParse(content.registrationProof);
const oldKey = proof && proof[0];
oldKey = proof && proof[0];
if (!oldKey) {
w.abort();
return void cb('INVALID_ANCESTOR');
@ -456,7 +482,7 @@ writeBlock.complete = function (Env, body, cb) {
MFA.copy(Env, oldKey, publicKey, w());
}).nThen(function () {
// Create a session for the current user
makeSession(Env, publicKey, cb);
makeSession(Env, publicKey, oldKey, session, cb);
});
};
@ -487,10 +513,10 @@ const removeBlock = Commands.TOTP_REMOVE_BLOCK = function (Env, body, cb) {
};
removeBlock.complete = function (Env, body, cb) {
const { publicKey, reason } = body;
const { publicKey, edPublic, reason } = body;
nThen(function (w) {
// Remove the block
Block.removeLoginBlock(Env, publicKey, reason, w((err) => {
Block.removeLoginBlock(Env, publicKey, reason, edPublic, w((err) => {
if (err) {
w.abort();
return void cb(err);

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
var Netflux = require("netflux-websocket");
var WebSocket = require("ws"); // jshint ignore:line
var WebSocket = require("ws");
var nThen = require("nthen");
var Util = require("../../www/common/common-util");

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
/* globals process */
const nThen = require("nthen");
const getFolderSize = require("get-folder-size");
@ -12,12 +11,12 @@ const Decrees = require("../decrees");
const Pinning = require("./pin-rpc");
const Core = require("./core");
const Channel = require("./channel");
const Invitation = require("./invitation");
const Users = require("./users");
const BlockStore = require("../storage/block");
const MFA = require("../storage/mfa");
const ArchiveAccount = require('../archive-account');
/* jshint ignore:start */
const { Worker } = require('node:worker_threads');
/* jshint ignore:end */
var Fs = require("fs");
@ -83,7 +82,7 @@ var getActiveSessions = function (Env, Server, cb) {
};
var shutdown = function (Env, Server, cb) {
if (true) {
if (true) { // eslint-disable-line no-constant-condition
return void cb('E_NOT_IMPLEMENTED');
}
@ -437,7 +436,15 @@ the server adds two pieces of information to the supplied decree:
if (!changed) { return void cb(); }
Env.Log.info('ADMIN_DECREE', decree);
Decrees.write(Env, decree, cb);
let _err;
nThen((waitFor) => {
Decrees.write(Env, decree, waitFor((err) => {
_err = err;
}));
setTimeout(waitFor(), 300); // NOTE: 300 because cache update may take up to 250ms
}).nThen(function () {
cb(_err);
});
};
// CryptPad_AsyncStore.rpc.send('ADMIN', ['SET_LAST_EVICTION', 0], console.log)
@ -459,6 +466,10 @@ var setLastEviction = function (Env, Server, cb, data, unsafeKey) {
var instanceStatus = function (Env, Server, cb) {
cb(void 0, {
restrictRegistration: Env.restrictRegistration,
restrictSsoRegistration: Env.restrictSsoRegistration,
dontStoreSSOUsers: Env.dontStoreSSOUsers,
dontStoreInvitedUsers: Env.dontStoreInvitedUsers,
enableEmbedding: Env.enableEmbedding,
launchTime: Env.launchTime,
currentTime: +new Date(),
@ -495,6 +506,7 @@ var instanceStatus = function (Env, Server, cb) {
instanceJurisdiction: Env.instanceJurisdiction,
instanceName: Env.instanceName,
instanceNotice: Env.instanceNotice,
enforceMFA: Env.enforceMFA,
});
};
@ -726,6 +738,8 @@ var archiveBlock = function (Env, Server, cb, data) {
});
cb(err);
});
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
if (SSOUtils) { SSOUtils.deleteAccount(Env, key, () => {}); }
};
var restoreArchivedBlock = function (Env, Server, cb, data) {
@ -740,6 +754,11 @@ var restoreArchivedBlock = function (Env, Server, cb, data) {
key: key,
reason: reason || '',
});
// Also restore SSO data
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
if (SSOUtils) { SSOUtils.restoreAccount(Env, key, () => {}); }
cb(err);
});
};
@ -854,6 +873,48 @@ var getMetadataHistory = function (Env, Server, cb, data) {
});
};
var getKnownUsers = (Env, Server, cb) => {
Users.getAll(Env, cb);
};
var addKnownUser = (Env, Server, cb, data, unsafeKey) => {
var obj = Array.isArray(data) && data[1];
var edPublic = obj.edPublic;
var block = obj.block;
var alias = obj.alias;
var userData = {
edPublic,
block,
alias,
email: obj.email,
name: obj.name,
type: 'manual'
};
Users.add(Env, edPublic, userData, unsafeKey, cb);
};
var deleteKnownUser = (Env, Server, cb, data) => {
var id = Array.isArray(data) && data[1];
Users.delete(Env, id, cb);
};
var updateKnownUser = (Env, Server, cb, data) => {
var args = Array.isArray(data) && data[1];
var edPublic = args.edPublic;
var changes = args.changes;
Users.update(Env, edPublic, changes, cb);
};
var getInvitations = (Env, Server, cb) => {
Invitation.getAll(Env, cb);
};
var createInvitation = (Env, Server, cb, data, unsafeKey) => {
const args = Array.isArray(data) && data[1];
if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); }
Invitation.create(Env, args.alias, args.email, cb, unsafeKey);
};
var deleteInvitation = (Env, Server, cb, data) => {
var id = Array.isArray(data) && data[1];
Invitation.delete(Env, id, cb);
};
var commands = {
ACTIVE_SESSIONS: getActiveSessions,
ACTIVE_PADS: getActiveChannelCount,
@ -912,6 +973,15 @@ var commands = {
GET_USER_TOTAL_SIZE: getUserTotalSize,
REMOVE_DOCUMENT: removeDocument,
GET_ALL_INVITATIONS: getInvitations,
CREATE_INVITATION: createInvitation,
DELETE_INVITATION: deleteInvitation,
GET_ALL_USERS: getKnownUsers,
ADD_KNOWN_USER: addKnownUser,
DELETE_KNOWN_USER: deleteKnownUser,
UPDATE_KNOWN_USER: updateKnownUser,
};
// addFirstAdmin is an anon_rpc command

View File

@ -2,13 +2,14 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
/* globals Buffer*/
const Block = module.exports;
const Nacl = require("tweetnacl/nacl-fast");
const nThen = require("nthen");
const Util = require("../common-util");
const BlockStore = require("../storage/block");
const Invitation = require("./invitation");
const Users = require("./users");
var isString = s => typeof(s) === 'string';
Block.isValidBlockId = id => {
@ -109,13 +110,21 @@ Block.validateAncestorProof = function (Env, proof, _cb) {
Block.writeLoginBlock = function (Env, msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
const { publicKey, signature, ciphertext, registrationProof } = msg;
const { publicKey, signature, ciphertext, registrationProof, userData, inviteToken, isSSO } = msg;
var previousKey;
var validatedBlock, path;
var validatedInvite;
nThen(function (w) {
if (!inviteToken) { return; }
Invitation.check(Env, inviteToken, w((err, state) => {
if (err || !state) { return; } // Invalid token, don't abort, check registration proof
validatedInvite = true;
}));
}).nThen(function (w) {
if (!Env.restrictRegistration) { return; }
if (!registrationProof) {
var ssoAllowed = isSSO && !Env.restrictSsoRegistration;
if (!(registrationProof || validatedInvite || ssoAllowed)) {
// we allow users with existing blocks to create new ones
// call back with error if registration is restricted and no proof of an existing block was provided
w.abort();
@ -124,6 +133,7 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
});
return cb("E_RESTRICTED");
}
if (!registrationProof) { return; }
Block.validateAncestorProof(Env, registrationProof, w(function (err, provenKey) {
if (err || !provenKey) { // double check that a key was validated
w.abort();
@ -162,8 +172,47 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
path: path,
});
cb(err);
if (!err && registrationProof) {
Users.checkUpdate(Env, userData, publicKey, (err) => {
if (!err) { return; }
Env.Log.error('UPDATE_KNOWN_USER', {
userData,
publicKey
});
});
}
});
if (validatedInvite) {
Invitation.use(Env, inviteToken, publicKey, userData, (err) => {
if (!err) { return; }
Env.Log.error('USE_INVITATION_LINK', {
inviteToken,
userData,
publicKey
});
});
} else if (isSSO && !Env.dontStoreSSOUsers && !registrationProof) {
let edPublic = Array.isArray(userData) && userData[1];
let name = Array.isArray(userData) && userData[0];
if (!edPublic) { return; }
let data = {
block: publicKey,
name,
edPublic,
type: 'sso',
alias: name
};
Users.add(Env, edPublic, data, null, (err) => {
if (err) {
Env.Log.error('INVITATION_ADD_USER', {
error: err,
data: data
});
}
});
}
});
};
/*
@ -176,7 +225,7 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
information, we can just sign some constant and use that as proof.
*/
Block.removeLoginBlock = function (Env, publicKey, reason, _cb) {
Block.removeLoginBlock = function (Env, publicKey, reason, edPublic, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
BlockStore.archive(Env, publicKey, reason, function (err) {
@ -186,5 +235,25 @@ Block.removeLoginBlock = function (Env, publicKey, reason, _cb) {
});
cb(err);
});
if (edPublic && reason !== 'PASSWORD_CHANGE') {
Users.delete(Env, edPublic, (err) => {
if (err) { Env.Log.error('KNOWN_USER_DELETION_ERROR', { error: err, key: edPublic }); }
});
}
// We should also try to remove the SSO data. Errors will be logged
// but they don't have to be shown to the user. The account data
// is already deleted anyway.
// If this is NOT a password change, also delete sso user.
let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils;
if (!SSOUtils) { return; }
if (reason !== 'PASSWORD_CHANGE') {
SSOUtils.deleteAccount(Env, publicKey, () => {});
} else {
SSOUtils.deleteBlock(Env, publicKey, () => {});
}
};

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Channel = module.exports;
const Util = require("../common-util");

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
/* globals process */
const Core = module.exports;
const Util = require("../common-util");

View File

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Invitation = module.exports;
const Invite = require('../storage/invite');
const Util = require("../common-util");
const Users = require("./users");
const getUid = () => {
return Util.uid() + Util.uid() + Util.uid();
};
Invitation.getAll = (Env, cb) => {
Invite.getAll(Env, (err, data) => {
if (err) { return void cb(err); }
cb(null, data);
});
};
Invitation.create = (Env, alias, email, _cb, unsafeKey) => {
const cb = Util.once(Util.mkAsync(_cb));
const id = getUid();
const invitation = {
alias,
email,
createdBy: unsafeKey,
time: +new Date()
};
Invite.write(Env, id, invitation, (err) => {
if (err) { return void cb(err); }
cb(null, id);
});
};
Invitation.delete = (Env, id, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
Invite.delete(Env, id, (err) => {
if (err && err !== 'ENOENT') { return void cb(err); }
cb(void 0, true);
});
};
Invitation.check = (Env, id, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
Invite.read(Env, id, (err) => {
if (err) { return void cb(err); }
cb(void 0, true);
});
};
Invitation.use = (Env, id, blockId, userData, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
Invite.read(Env, id, (err, _data) => {
if (err) { return void cb(err); }
let data = Util.clone(_data);
if (!Array.isArray(userData)) { userData = []; }
let name = userData[0];
let edPublic = userData[1];
data.block = blockId;
data.name = name;
data.edPublic = edPublic;
data.type = 'invite:' + id;
let adminKey = data.createdBy;
if (!Env.dontStoreInvitedUsers) {
Users.add(Env, edPublic, data, adminKey, (err) => {
if (err) {
Env.Log.error('INVITATION_ADD_USER', {
error: err,
data: data
});
}
});
}
Invite.delete(Env, id, (err) => {
if (err) {
Env.Log.error('INVITATION_DELETE_USE', {
error: err,
id: id
});
}
});
});
};

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Data = module.exports;
const Meta = require("../metadata");

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Core = require("./core");
const Pinning = module.exports;

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
/* globals Buffer*/
const Quota = module.exports;

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Upload = module.exports;
const Util = require("../common-util");
const Pinning = require("./pin-rpc");

79
lib/commands/users.js Normal file
View File

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Users = module.exports;
const User = require('../storage/user');
const Util = require("../common-util");
Users.getAll = (Env, cb) => {
User.getAll(Env, (err, data) => {
if (err) { return void cb(err); }
cb(null, data);
});
};
Users.add = (Env, edPublic, data, adminKey, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
data.createdBy = adminKey;
data.time = +new Date();
const safeKey = Util.escapeKeyCharacters(edPublic);
User.write(Env, safeKey, data, (err) => {
if (err) { return void cb(err); }
cb();
});
};
Users.delete = (Env, id, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
User.delete(Env, id, (err) => {
if (err && err !== 'ENOENT') { return void cb(err); }
cb(void 0, true);
});
};
Users.read = (Env, edPublic, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
User.read(Env, edPublic, (err, data) => {
if (err) { return void cb(err); }
cb(void 0, data);
});
};
Users.update = (Env, edPublic, changes, _cb) => {
const cb = Util.once(Util.mkAsync(_cb));
Users.read(Env, edPublic, (err, data) => {
if (err === 'ENOENT') { return void cb(); }
if (err) { return void cb(err); }
if (typeof(changes) !== "object") { return void cb('EINVAL'); }
// User exists, update their data
var aborted = Object.keys(changes || {}).some((key) => {
if (changes[key] === false) {
delete data[key];
return;
}
if (String(changes[key]).length > 300) {
cb('E_TOO_LONG');
return true;
}
data[key] = changes[key];
});
if (aborted) { return; }
User.update(Env, edPublic, data, cb);
});
};
// On password change, update the block
Users.checkUpdate = (Env, userData, newBlock, cb) => {
if (!Array.isArray(userData)) { userData = []; }
let edPublic = userData[1];
if (!edPublic) { return void cb('INVALID_PUBLIC_KEY'); }
Users.read(Env, edPublic, (err, data) => {
if (err === 'ENOENT') { return void cb(); }
if (err) { return void cb(err); }
// User exists, update their block
data.block = newBlock;
User.update(Env, edPublic, data, cb);
});
};

View File

@ -10,6 +10,7 @@ var Core = require("./commands/core");
IMPLEMENTED:
RESTRICT_REGISTRATION(<boolean>)
RESTRICT_SSO_REGISTRATION(<boolean>)
UPDATE_DEFAULT_STORAGE(<number>)
// QUOTA MANAGEMENT
@ -107,8 +108,14 @@ var makeBooleanSetter = function (attr) {
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_EMBEDDING', [true]]], console.log)
commands.ENABLE_EMBEDDING = makeBooleanSetter('enableEmbedding');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['ENFORCE_MFA', [true]]], console.log)
commands.ENFORCE_MFA = makeBooleanSetter('enforceMFA');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
commands.RESTRICT_REGISTRATION = makeBooleanSetter('restrictRegistration');
commands.RESTRICT_SSO_REGISTRATION = makeBooleanSetter('restrictSsoRegistration');
commands.DISABLE_STORE_INVITED_USERS = makeBooleanSetter('dontStoreInvitedUsers');
commands.DISABLE_STORE_SSO_USERS = makeBooleanSetter('dontStoreSSOUsers');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_INTEGRATED_EVICTION', [true]]], console.log)
commands.DISABLE_INTEGRATED_EVICTION = makeBooleanSetter('disableIntegratedEviction');
@ -466,7 +473,7 @@ Decrees.load = function (Env, _cb) {
Decrees.write = function (Env, decree, _cb) {
var path = Path.join(Env.paths.decree, 'decree.ndjson');
Env.scheduleDecree.ordered('', function (next) {
var cb = Util.both(_cb, next);
var cb = Util.both(Util.mkAsync(_cb), next);
Fs.appendFile(path, JSON.stringify(decree) + '\n', cb);
});
};

View File

@ -52,10 +52,6 @@ Default.padContentSecurity = function (Env) {
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
};
Default.diagramContentSecurity = function (Env) {
return (Default.commonCSP(Env).join('; ') + "script-src 'self' 'sha256-dLMFD7ijAw6AVaqecS7kbPcFFzkxQ+yeZSsKpOdLxps=' 'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=' resource: " + Env.httpUnsafeOrigin).replace(/\s+/g, ' ');
};
Default.httpHeaders = function (Env) {
return {
"X-XSS-Protection": "1; mode=block",

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6 */
/* globals process */
const Crypto = require('crypto');
@ -18,6 +17,8 @@ const Package = require("../package.json");
const Default = require("./defaults");
const Path = require("path");
const plugins = require('./plugin-manager');
const Nacl = require("tweetnacl/nacl-fast");
var canonicalizeOrigin = function (s) {
@ -83,6 +84,7 @@ module.exports.create = function (config) {
const curve = Nacl.box.keyPair();
const Env = {
plugins: plugins,
logFeedback: Boolean(config.logFeedback),
mainPages: config.mainPages || Default.mainPages(),
@ -227,6 +229,9 @@ module.exports.create = function (config) {
evictionReport: {},
commandTimers: {},
sso: config.sso,
enforceMFA: config.enforceMFA,
// initialized as undefined
bearerSecret: void 0,
curvePrivate: curve.secretKey,

View File

@ -200,6 +200,11 @@ var evictArchived = function (Env, cb) {
// but if it's been stored for the configured time...
// expire it
if (Env.DRY_RUN) {
if (item.channel.length === 32) { removed++; }
else if (item.channel.length === 44) { accounts++; }
return void Log.info("EVICT_ARCHIVED_CHANNEL_DRY_RUN", item.channel, cb);
}
store.removeArchivedChannel(item.channel, w(function (err) {
if (err) {
return Log.error('EVICT_ARCHIVED_CHANNEL_REMOVAL_ERROR', {
@ -245,7 +250,11 @@ var evictArchived = function (Env, cb) {
Log.error("EVICT_BLOB_LIST_ARCHIVED_PROOF_ERROR", err);
return void next();
}
if (item && item.mtime > retentionTime) { return void next(); }
if (item && item.ctime > retentionTime) { return void next(); }
if (Env.DRY_RUN) {
removed++;
return void Log.info("EVICT_ARCHIVED_BLOB_PROOF_DRY_RUN", item, next);
}
blobs.remove.archived.proof(item.safeKey, item.blobId, (function (err) {
if (err) {
Log.error("EVICT_ARCHIVED_BLOB_PROOF_ERROR", item);
@ -272,7 +281,11 @@ var evictArchived = function (Env, cb) {
Log.error("EVICT_BLOB_LIST_ARCHIVED_BLOBS_ERROR", err);
return void next();
}
if (item && item.mtime > retentionTime) { return void next(); }
if (item && item.ctime > retentionTime) { return void next(); }
if (Env.DRY_RUN) {
removed++;
return void Log.info("EVICT_ARCHIVED_BLOB_DRY_RUN", item, next);
}
blobs.remove.archived.blob(item.blobId, function (err) {
if (err) {
Log.error("EVICT_ARCHIVED_BLOB_ERROR", item);
@ -288,6 +301,7 @@ var evictArchived = function (Env, cb) {
}));
};
if (Env.DRY_RUN) { Env.Log.info('DRY RUN'); }
nThen(loadStorage)
.nThen(migrateIncorrectBlobs)
.nThen(removeArchivedChannels)
@ -544,6 +558,9 @@ module.exports = function (Env, cb) {
}
// remove the pin logs of inactive accounts if inactive account removal is configured
if (Env.DRY_RUN) {
return void Log.info("EVICT_INACTIVE_ACCOUNT_DRY_RUN", id, next);
}
pinStore.archiveChannel(id, undefined, function (err) {
if (err) {
return Log.error('EVICT_INACTIVE_ACCOUNT_PIN_LOG', err, next);
@ -602,7 +619,12 @@ module.exports = function (Env, cb) {
// unless we address this race condition with this last-minute double-check
if (item.mtime > inactiveTime) { return void next(); }
if (Env.DRY_RUN) {
removed++;
return void Log.info("EVICT_ARCHIVE_BLOB_DRY_RUN", {
item: item,
}, next);
}
blobs.archive.blob(item.blobId, 'INACTIVE', function (err) {
if (err) {
return Log.error("EVICT_ARCHIVE_BLOB_ERROR", {
@ -610,6 +632,7 @@ module.exports = function (Env, cb) {
item: item,
}, next);
}
removed++;
Log.info("EVICT_ARCHIVE_BLOB", {
item: item,
}, next);
@ -658,6 +681,10 @@ module.exports = function (Env, cb) {
}
}));
}).nThen(function () {
if (Env.DRY_RUN) {
removed++;
return void Log.info("EVICT_BLOB_PROOF_LONELY_DRY_RUN", item, next);
}
blobs.remove.proof(item.safeKey, item.blobId, function (err) {
if (err) {
return Log.error("EVICT_BLOB_PROOF_LONELY_ERROR", item, next);
@ -698,6 +725,9 @@ module.exports = function (Env, cb) {
// check if the database has any ephemeral channels
// if it does it's because of a bug, and they should be removed
if (item.channel.length === 34) {
if (Env.DRY_RUN) {
return void Log.info("EVICT_EPHEMERAL_DRY_RUN", item.channel, cb);
}
return void store.removeChannel(item.channel, w(function (err) {
if (err) {
return Log.error('EVICT_EPHEMERAL_CHANNEL_REMOVAL_ERROR', {
@ -728,6 +758,11 @@ module.exports = function (Env, cb) {
// else fall through to the archival
}));
}).nThen(function (w) {
if (Env.DRY_RUN) {
archived++;
w.abort();
return void Log.info("EVICT_CHANNEL_ARCHIVAL_DRY_RUN", item.channel, cb);
}
return void store.archiveChannel(item.channel, 'INACTIVE', w(function (err) {
if (err) {
Log.error('EVICT_CHANNEL_ARCHIVAL_ERROR', {
@ -736,8 +771,8 @@ module.exports = function (Env, cb) {
}, w());
return;
}
Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel, w());
archived++;
Log.info('EVICT_CHANNEL_ARCHIVAL', item.channel, w());
}));
}).nThen(cb);
};
@ -754,6 +789,7 @@ module.exports = function (Env, cb) {
store.listChannels(handler, w(done), true); // using a hacky "fast mode" since we only need the channel id
};
if (Env.DRY_RUN) { Env.Log.info('DRY RUN'); }
nThen(loadStorage)
// iterate over all documents and add them to a bloom filter if they have been active

View File

@ -2,8 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6 */
const nThen = require('nthen');
const RPC = require("./rpc");
const HK = require("./hk-util.js");

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6 */
/* global Buffer */
var HK = module.exports;

View File

@ -4,6 +4,7 @@
var Nacl = require("tweetnacl/nacl-fast");
var Util = require('./common-util.js');
const plugins = require("./plugin-manager");
var Challenge = require("./storage/challenge.js");
// C.read(Env, id, cb)
@ -66,17 +67,26 @@ var COMMANDS = {};
// and to authenticate new sessions once a TOTP secret has been associated with their account,
const NOAUTH = require("./challenge-commands/base.js");
COMMANDS.MFA_CHECK = NOAUTH.MFA_CHECK;
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK;
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK; // Account creation + password change
COMMANDS.REMOVE_BLOCK = NOAUTH.REMOVE_BLOCK;
const TOTP = require("./challenge-commands/totp.js");
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
COMMANDS.TOTP_CHECK = TOTP.TOTP_CHECK;
COMMANDS.TOTP_MFA_CHECK = TOTP.TOTP_MFA_CHECK;
COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE;
COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK;
COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK; // Password change only for now (v5.5.0)
COMMANDS.TOTP_REMOVE_BLOCK = TOTP.TOTP_REMOVE_BLOCK;
try {
// SSO plugin may not be installed
const SSO = plugins.SSO && plugins.SSO.challenge;
COMMANDS.SSO_AUTH = SSO.SSO_AUTH;
COMMANDS.SSO_AUTH_CB = SSO.SSO_AUTH_CB;
COMMANDS.SSO_WRITE_BLOCK = SSO.SSO_WRITE_BLOCK; // Account creation only
COMMANDS.SSO_UPDATE_BLOCK = SSO.SSO_UPDATE_BLOCK; // Password change
COMMANDS.SSO_VALIDATE = SSO.SSO_VALIDATE;
} catch (e) {}
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');
@ -145,7 +155,7 @@ var handleCommand = function (Env, req, res) {
date: date,
});
});
});
}, req);
} catch (err) {
Env.Log.error("CHALLENGE_COMMAND_THROWN_ERROR", {
error: Util.serializeError(err),
@ -295,7 +305,7 @@ var handleResponse = function (Env, req, res) {
});
}
res.status(200).json(content);
});
}, req, res);
});
};

View File

@ -13,13 +13,19 @@ const Logger = require("./log");
const AuthCommands = require("./http-commands");
const MFA = require("./storage/mfa");
const Sessions = require("./storage/sessions");
const cookieParser = require("cookie-parser");
const bodyParser = require('body-parser');
const BlobStore = require("./storage/blob");
const BlockStore = require("./storage/block");
const plugins = require("./plugin-manager");
const DEFAULT_QUERY_TIMEOUT = 5000;
const PID = process.pid;
let SSOUtils = plugins.SSO && plugins.SSO.utils;
var Env = JSON.parse(process.env.Env);
Env.plugins = plugins;
const response = Util.response(function (errLabel, info) {
if (!Env.Log) { return; }
Env.Log.error(errLabel, info);
@ -64,6 +70,7 @@ EVENTS.ENV_UPDATE = function (data /*, cb */) {
try {
Env = JSON.parse(data);
Env.Log = Log;
Env.plugins = plugins;
Env.incrementBytesWritten = function () {};
} catch (err) {
Log.error('HTTP_WORKER_ENV_UPDATE', Util.serializeError(err));
@ -127,8 +134,6 @@ var getHeaders = function (Env, type) {
var csp;
if (type === 'office') {
csp = Default.padContentSecurity(Env);
} else if (type === 'diagram') {
csp = Default.diagramContentSecurity(Env);
} else {
csp = Default.contentSecurity(Env);
}
@ -151,8 +156,6 @@ var setHeaders = function (req, res) {
type = 'office';
} else if (/^\/api\/(broadcast|config)/.test(req.url)) {
type = 'api';
} else if (/^\/components\/drawio\/src\/main\/webapp\/index.html.*$/.test(req.url)) {
type = 'diagram';
} else {
type = 'standard';
}
@ -165,6 +168,11 @@ const Express = require("express");
Express.static.mime.define({'application/wasm': ['wasm']});
var app = Express();
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(cookieParser());
(function () {
if (!Env.logFeedback) { return; }
@ -182,7 +190,8 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
const { createProxyMiddleware } = require("http-proxy-middleware");
var proxyTarget = new URL('', `ws:${Env.httpAddress}`);
var httpAddress = Env.httpAddress === '::' ? 'localhost' : Env.httpAddress;
var proxyTarget = new URL('', `ws:${httpAddress}`);
proxyTarget.port = Env.websocketPort;
const wsProxy = createProxyMiddleware({
@ -200,6 +209,30 @@ const wsProxy = createProxyMiddleware({
app.use('/cryptpad_websocket', wsProxy);
app.use('/ssoauth', (req, res, next) => {
if (SSOUtils && req && req.body && req.body.SAMLResponse) {
req.method = 'GET';
let token = Util.uid();
let smres = req.body.SAMLResponse;
return SSOUtils.writeRequest(Env, {
id: token,
type: 'saml',
content: smres
}, (err) => {
if (err) {
Log.error('E_SSO_WRITE_REQ', err);
return res.sendStatus(500);
}
let value = `samltoken="${token}"; SameSite=Strict; HttpOnly`;
res.setHeader('Set-Cookie', value);
next();
});
}
next();
});
app.use('/blob', function (req, res, next) {
/* Head requests are used to check the size of a blob.
@ -265,6 +298,7 @@ app.use(function (req, res, next) {
next();
});
// serve custom app content from the customize directory
// useful for testing pages customized with opengraph data
app.use(Express.static(Path.resolve('./customize/www')));
@ -303,7 +337,7 @@ app.use('/block/', function (req, res, next) {
var authorization = req.headers.authorization;
var mfa_params;
var mfa_params, sso_params;
nThen(function (w) {
// First, check whether the block id in question has any MFA settings stored
MFA.read(Env, name, w(function (err, content) {
@ -312,8 +346,7 @@ app.use('/block/', function (req, res, next) {
// in either case you can abort and fall through
// allowing the static webserver to handle either case
if (err && err.code === 'ENOENT') {
w.abort();
return void next();
return;
}
// we're not expecting other errors. the sensible thing is to fail
@ -344,10 +377,31 @@ app.use('/block/', function (req, res, next) {
});
}
}));
// Same for SSO settings
if (!SSOUtils) { return; }
SSOUtils.readBlock(Env, name, w(function (err, content) {
if (err && (err.code === 'ENOENT' || err === 'ENOENT')) {
return;
}
if (err) {
Log.error('GET_BLOCK_METADATA', err);
return void res.status(500).json({
code: 500,
error: "UNEXPECTED_ERROR",
});
}
sso_params = content;
}));
}).nThen(function (w) {
if (!mfa_params && !sso_params) {
w.abort();
next();
}
}).nThen(function (w) {
// We should only be able to reach this logic
// if we successfully loaded and parsed some JSON
// representing the user's MFA settings.
// representing the user's MFA and/or SSO settings.
// Failures at this point relate to insufficient or incorrect authorization.
// This function standardizes how we reject such requests.
@ -359,18 +413,19 @@ app.use('/block/', function (req, res, next) {
var no = function () {
w.abort();
res.status(401).json({
method: mfa_params.method,
sso: Boolean(sso_params),
method: mfa_params && mfa_params.method,
code: 401
});
};
// if you are here it is because this block is protected by MFA.
// if you are here it is because this block is protected by MFA or SSO.
// they will need to provide a JSON Web Token, so we can reject them outright
// if one is not present in their authorization header
if (!authorization) { return void no(); }
// The authorization header should be of the form
// "Authorization: Bearer <JWT>"
// "Authorization: Bearer <SessionId>"
// We can reject the request if it is malformed.
let token = authorization.replace(/^Bearer\s+/, '').trim();
if (!token) { return void no(); }
@ -379,14 +434,18 @@ app.use('/block/', function (req, res, next) {
if (err) {
Log.error('SESSION_READ_ERROR', err);
return res.status(401).json({
method: mfa_params.method,
sso: Boolean(sso_params),
method: mfa_params && mfa_params.method,
code: 401,
});
}
let content = Util.tryParse(contentStr);
if (content.mfa && content.mfa.exp && ((+new Date()) > content.mfa.exp)) {
if (mfa_params && !content.mfa) { return void no(); }
if (sso_params && !content.sso) { return void no(); }
if (content.mfa && content.mfa.exp && (+new Date()) > content.mfa.exp) {
Log.error("OTP_SESSION_EXPIRED", content.mfa);
Sessions.delete(Env, name, token, function (err) {
if (err) {
@ -398,12 +457,20 @@ app.use('/block/', function (req, res, next) {
return void no();
}
// we could also check whether the content of the file matches the token,
// but clients don't have any influence over the reference and can only
// request to create tokens that are scoped to a public key they control.
// I don' think there's any practical benefit to such a check.
// So, interpret the existence of a file in that location as the continued
if (content.sso && content.sso.exp && (+new Date()) > content.sso.exp) {
Log.error("SSO_SESSION_EXPIRED", content.sso);
Sessions.delete(Env, name, token, function (err) {
if (err) {
Log.error('SSO_SESSION_DELETE_EXPIRED_ERROR', err);
return;
}
Log.info('SSO_SESSION_DELETE_EXPIRED', err);
});
return void no();
}
// Interpret the existence of a file in that location as the continued
// validity of the session. Fall through and let the built-in webserver
// handle the 404 or serving the file.
next();
@ -481,6 +548,13 @@ var makeRouteCache = function (template, cacheName) {
};
};
const ssoList = Env.sso && Env.sso.enabled && Array.isArray(Env.sso.list) &&
Env.sso.list.map(function (obj) { return obj.name; }) || [];
const ssoCfg = (SSOUtils && ssoList.length) ? {
force: (Env.sso && Env.sso.enforced && 1) || 0,
password: (Env.sso && Env.sso.cpPassword && (Env.sso.forceCpPassword ? 2 : 1)) || 0,
list: ssoList
} : false;
var serveConfig = makeRouteCache(function () {
return [
'define(function(){',
@ -501,12 +575,15 @@ var serveConfig = makeRouteCache(function () {
maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize,
restrictRegistration: Env.restrictRegistration,
restrictSsoRegistration: Env.restrictSsoRegistration,
httpSafeOrigin: Env.httpSafeOrigin,
enableEmbedding: Env.enableEmbedding,
fileHost: Env.fileHost,
shouldUpdateNode: Env.shouldUpdateNode || undefined,
listMyInstance: Env.listMyInstance,
accounts_api: Env.accounts_api,
sso: ssoCfg,
enforceMFA: Env.enforceMFA
}, null, '\t'),
'});'
].join(';\n');

View File

@ -46,5 +46,12 @@ if (!isPositiveNumber(config.premiumUploadSize) || config.premiumUploadSize < co
delete config.premiumUploadSize;
}
config.sso = {};
try {
config.sso = require("../config/sso");
} catch (e) {
//console.log("SSO config not found");
}
module.exports = config;

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
var Store = require("./storage/file");
var Util = require("./common-util");

View File

@ -2,8 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
var Pins = module.exports;
const Fs = require("fs");
@ -90,7 +88,6 @@ var createLineHandler = Pins.createLineHandler = function (ref, errorHandler) {
});
}
ref.surplus = ref.index;
//jshint -W086
// fallthrough
}
case 'PIN': {

23
lib/plugin-manager.js Normal file
View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const fs = require('node:fs');
const plugins = {};
try {
let pluginsDir = fs.readdirSync(__dirname + '/plugins');
pluginsDir.forEach((name) => {
if (name=== "README.md") { return; }
try {
let plugin = require(`./plugins/${name}/index`);
plugins[plugin.name] = plugin.modules;
} catch (err) {
console.error(err);
}
});
} catch (err) {
if (err.code !== 'ENOENT') { console.error(err); }
}
module.exports = plugins;

View File

@ -4,5 +4,4 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and cont
SPDX-License-Identifier: AGPL-3.0-or-later
-->
* compare your conf against `cryptpad/docs/example.nginx.conf`
*
# CryptPad's plugins directory

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Util = require("./common-util");
const Core = require("./commands/core");

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Stats = module.exports;
var truthyStringOrNothing = function (s) {

View File

@ -25,6 +25,7 @@ Feel free to migrate all of these to a relational DB at some point in the future
const Basic = module.exports;
const Fs = require("node:fs");
const Fse = require("fs-extra");
const Path = require("node:path");
var pathError = (cb) => {
@ -70,4 +71,17 @@ Basic.deleteDir = function (Env, path, cb) {
Fs.rm(path, { recursive: true, force: true }, cb);
};
Basic.archive = function (Env, path, archivePath, cb) {
Fse.move(path, archivePath, {
overwrite: true,
}, (err) => {
cb(err);
});
};
Basic.restore = function (Env, archivePath, path, cb) {
Fse.move(archivePath, path, {
//overwrite: true,
}, (err) => {
cb(err);
});
};

View File

@ -153,8 +153,12 @@ var clearActivity = function (Env, blobId, cb) {
};
var updateActivity = function (Env, blobId, cb) {
var path = makeActivityPath(Env, blobId);
var blobPath = makeBlobPath(Env, blobId);
isFile(blobPath, (err, state) => {
if (err || !state) { return void cb(); }
var s_data = String(+new Date());
Fs.writeFile(path, s_data, cb);
});
};
var archiveActivity = function (Env, blobId, cb) {
@ -464,7 +468,7 @@ var makeWalker = function (n, handleChild, done) {
// do no more than 20 jobs at a time
var tasks = Semaphore.create(n);
var recurse = function (path) {
var recurse = function (path, dir) {
tasks.take(function (give) {
var next = give(W());
@ -477,7 +481,19 @@ var makeWalker = function (n, handleChild, done) {
}
if (!stats.isDirectory()) {
w.abort();
return void handleChild(void 0, path, next);
if (/\.activity$/.test(path)) {
// NOTE: some activity files were created for deleted blobs due to
// a bug. We're going to detect them here in order to be able to clean
// them.
if (!dir.includes(Path.basename(path.replace(/\.activity$/, '')))) {
return void handleChild(void 0, path, next, true);
}
// Ignore valid activity files
return next();
}
// Ignore placeholder files
if (/\.placeholder$/.test(path)) { return next(); }
return void handleChild(void 0, path, next, false);
}
// fall through
}));
@ -487,7 +503,7 @@ var makeWalker = function (n, handleChild, done) {
if (err) { return next(); }
// everything is fine and it's a directory...
dir.forEach(function (d) {
recurse(Path.join(path, d));
recurse(Path.join(path, d), dir);
});
next();
});
@ -502,7 +518,8 @@ var listProofs = function (root, handler, cb) {
Fs.readdir(root, function (err, dir) {
if (err) { return void cb(err); }
var walk = makeWalker(20, function (err, path, next) {
var walk = makeWalker(20, function (err, path, next, loneActivity) {
if (loneActivity) { return void next(); }
// path is the path to a child node on the filesystem
// next handles the next job in a queue
@ -537,12 +554,20 @@ var listProofs = function (root, handler, cb) {
});
};
var getActivityStat = function (path, base, cb) {
var suffix = base ? '' : '.activity';
Fs.stat(path+suffix, function (err, stats) {
if (err && err.code === 'ENOENT' && !base) { return getActivityStat(path, true, cb); }
cb(err, stats);
});
};
var listBlobs = function (root, handler, cb) {
// iterate over files
Fs.readdir(root, function (err, dir) {
if (err) { return void cb(err); }
var walk = makeWalker(20, function (err, path, next) {
Fs.stat(path, function (err, stats) {
var walk = makeWalker(20, function (err, path, next, loneActivity) {
if (loneActivity) { return void next(); }
getActivityStat(path, false, function (err, stats) {
if (err) {
return void handler(err, void 0, next);
}
@ -565,6 +590,30 @@ var listBlobs = function (root, handler, cb) {
});
};
var cleanLoneActivity = function (root, cb) {
// iterate over files
Fs.readdir(root, function (err, dir) {
if (err) { return void cb(err); }
var walk = makeWalker(20, function (err, path, next, loneActivity) {
if (!loneActivity) { return void next(); }
Fs.unlink(path, function (err) {
if (err) {
return console.error('ERROR', path, err);
}
console.log('DELETED', path);
next();
});
}, function () {
cb();
});
dir.forEach(function (d) {
if (d.length !== 2) { return; }
walk(Path.join(root, d));
});
});
};
BlobStore.create = function (config, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (typeof(config.getSession) !== 'function') {
@ -651,6 +700,10 @@ BlobStore.create = function (config, _cb) {
removeArchivedProof(Env, safeKey, blobId, cb);
},
},
loneActivity: function (_cb) {
var cb = Util.once(Util.mkAsync(_cb));
cleanLoneActivity(Env.blobPath, cb);
}
},
archive: {

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Block = module.exports;
const Util = require("../common-util");
const Path = require("path");

View File

@ -3,7 +3,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
/*@flow*/
/* jshint esversion: 6 */
/* globals Buffer */
var Fs = require("fs");
var Fse = require("fs-extra");
@ -268,7 +267,7 @@ var getMetadataAtPath = function (Env, path, _cb) {
// if you can't parse, that's bad
return void cb("INVALID_METADATA");
}
readMore();
readMore(); // eslint-disable-line no-unreachable
}, function (err) {
cb(err);
});

82
lib/storage/invite.js Normal file
View File

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Basic = require("./basic.js");
const Path = require("node:path");
const nThen = require('nthen');
const Util = require('../common-util');
const Invite = module.exports;
/* This module manages storage used to implement instance invitations when registration
is closed. This "database" will store individual invitation and their state.
An invitation is created with a random uid and an alias (username, email, etc.)
Once it is used by the user, their newly created blockId is added which will mark
it as completed.
*/
const pathFromId = function (Env, id) {
if (!id || typeof(id) !== 'string') { return void console.error('INVITE_BAD_ID', id); }
return Path.join(Env.paths.base, "invitations", id.slice(0, 2), id);
};
Invite.read = function (Env, id, cb) {
var path = pathFromId(Env, id);
Basic.read(Env, path, (err, data) => {
if (err) { return void cb(err.code); }
cb(void 0, Util.tryParse(data));
});
};
Invite.getAll = function (Env, cb) {
let invitations = {};
nThen((waitFor) => {
let dirPath = Path.join(Env.paths.base, "invitations");
Basic.readDir(Env, dirPath, waitFor((err, prefixes) => {
if (err && err.code === 'ENOENT') { return void cb(void 0, {}); }
if (err) { waitFor.abort(); return void cb(err.code); }
prefixes.forEach((prefix) => {
var dirPath2 = Path.join(Env.paths.base, "invitations", prefix);
Basic.readDir(Env, dirPath2, waitFor((err, files) => {
if (err) { waitFor.abort(); return void cb(err.code); }
files.forEach((id) => {
Invite.read(Env, id, waitFor((err, data) => {
invitations[id] = data || { error: err };
}));
});
}));
});
}));
}).nThen(() => {
cb(null, invitations);
});
};
Invite.write = function (Env, id, data, cb) {
var path = pathFromId(Env, id);
Basic.write(Env, path, JSON.stringify(data), (err) => {
if (err) { return void cb(err.code); }
cb();
});
};
Invite.delete = function (Env, id, cb) {
var path = pathFromId(Env, id);
Basic.delete(Env, path, (err) => {
if (err) { return void cb(err.code); }
cb();
});
};
Invite.update = function (Env, id, data, cb) {
Invite.delete(Env, id, (err) => {
if (err) { return void cb(err); }
Invite.write(Env, id, data, cb);
});
};

View File

@ -46,6 +46,19 @@ Sessions.delete = function (Env, id, ref, cb) {
Basic.delete(Env, path, cb);
};
Sessions.update = function (Env, id, oldId, ref, dataStr, cb) {
var data = Util.tryParse(dataStr);
Sessions.read(Env, oldId, ref, (err, oldData) => {
let content = Util.tryParse(oldData) || {};
Object.keys(data || {}).forEach((type) => {
content[type] = data[type];
});
Sessions.delete(Env, oldId, ref, () => {
Sessions.write(Env, id, ref, JSON.stringify(content), cb);
});
});
};
Sessions.deleteUser = function (Env, id, cb) {
if (!id || typeof(id) !== 'string') { return; }
id = Util.escapeKeyCharacters(id);

110
lib/storage/sso.js Normal file
View File

@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Basic = require("./basic");
const Path = require("node:path");
const Util = require("../common-util");
const SSO = module.exports;
/* This module manages storage related to Single Sign-On (SSO) settings.
A first part (sso-requests) contains temporary files for sso authentication with a remote service
A second part (sso-users) is a database of accounts registered via SSO (SSO id ==> block seed)
A third part (sso-blocks) is a database of blocks that are sso-protected (block id ==> SSO id...)
The path for requests is based on the "authentication request" token depending on the type of SSO.
The path for the user database is based on their persistent identifier (id) from the SSO.
*/
var pathFromId = function (Env, id, subPath) {
if (!id || typeof(id) !== 'string') { return; }
id = Util.escapeKeyCharacters(id);
return Path.join(Env.paths.base, subPath, id.slice(0, 2), `${id}.json`);
};
var reqPathFromId = function (Env, id) {
return pathFromId(Env, id, 'sso_request');
};
var blockPathFromId = function (Env, id) {
return pathFromId(Env, id, 'sso_block');
};
var userPathFromId = function (Env, id, provider) {
if (!id || typeof(id) !== 'string') { return; }
if (!provider || typeof(provider) !== 'string') { return; }
id = Util.escapeKeyCharacters(id);
return Path.join(Env.paths.base, 'sso_user', provider, id.slice(0, 2), `${id}.json`);
};
var blockArchivePath = function (Env, id) {
return Path.join(Env.paths.archive, 'sso_block', id.slice(0, 2), `${id}.json`);
};
var userArchivePath = function (Env, id, provider) {
return Path.join(Env.paths.archive, 'sso_user', provider, id.slice(0, 2), `${id}.json`);
};
const Req = SSO.request = {};
Req.read = function (Env, id, cb) {
var path = reqPathFromId(Env, id);
Basic.read(Env, path, cb);
};
Req.write = function (Env, id, data, cb) {
var path = reqPathFromId(Env, id);
Basic.write(Env, path, data, cb);
};
Req.delete = function (Env, id, cb) {
var path = reqPathFromId(Env, id);
Basic.delete(Env, path, cb);
};
const User = SSO.user = {};
User.read = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
Basic.read(Env, path, cb);
};
User.write = function (Env, provider, id, data, cb) {
var path = userPathFromId(Env, id, provider);
Basic.write(Env, path, data, cb);
};
User.delete = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
Basic.delete(Env, path, cb);
};
User.archive = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
var archivePath = userArchivePath(Env, id, provider);
Basic.archive(Env, path, archivePath, cb);
};
User.restore = function (Env, provider, id, cb) {
var path = userPathFromId(Env, id, provider);
var archivePath = userArchivePath(Env, id, provider);
Basic.restore(Env, archivePath, path, cb);
};
const Block = SSO.block = {};
Block.read = function (Env, id, cb) {
var path = blockPathFromId(Env, id);
Basic.read(Env, path, cb);
};
Block.write = function (Env, id, data, cb) {
var path = blockPathFromId(Env, id);
Basic.write(Env, path, data, cb);
};
Block.delete = function (Env, id, cb) {
var path = blockPathFromId(Env, id);
Basic.delete(Env, path, cb);
};
Block.archive = function (Env, id, cb) {
var path = blockPathFromId(Env, id);
var archivePath = blockArchivePath(Env, id);
Basic.archive(Env, path, archivePath, cb);
};
Block.restore = function (Env, id, cb) {
var path = blockPathFromId(Env, id);
var archivePath = blockArchivePath(Env, id);
Basic.restore(Env, archivePath, path, cb);
};

79
lib/storage/user.js Normal file
View File

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Basic = require("./basic.js");
const Path = require("node:path");
const nThen = require('nthen');
const Util = require('../common-util');
const User = module.exports;
/* This module manages storage used to implement user management. "Known users" can
be added here in order to store their public key, their block ID and an alias
used to recognize them.
*/
const pathFromId = function (Env, id) {
if (!id || typeof(id) !== 'string') { return void console.error('KNWONUSER_BAD_ID', id); }
return Path.join(Env.paths.base, "users", id.slice(0, 2), id);
};
User.read = function (Env, id, cb) {
var path = pathFromId(Env, id);
Basic.read(Env, path, (err, data) => {
if (err) { return void cb(err.code); }
cb(void 0, Util.tryParse(data));
});
};
User.getAll = function (Env, cb) {
let users = {};
nThen((waitFor) => {
let dirPath = Path.join(Env.paths.base, "users");
Basic.readDir(Env, dirPath, waitFor((err, prefixes) => {
if (err && err.code === 'ENOENT') { return void cb(void 0, {}); }
if (err) { waitFor.abort(); return void cb(err.code); }
prefixes.forEach((prefix) => {
var dirPath2 = Path.join(Env.paths.base, "users", prefix);
Basic.readDir(Env, dirPath2, waitFor((err, files) => {
if (err) { waitFor.abort(); return void cb(err.code); }
files.forEach((id) => {
User.read(Env, id, waitFor((err, data) => {
users[id] = data || { error: err };
}));
});
}));
});
}));
}).nThen(() => {
cb(null, users);
});
};
User.write = function (Env, id, data, cb) {
var path = pathFromId(Env, id);
Basic.write(Env, path, JSON.stringify(data), (err) => {
if (err) { return void cb(err.code); }
cb();
});
};
User.delete = function (Env, id, cb) {
var path = pathFromId(Env, id);
Basic.delete(Env, path, (err) => {
if (err) { return void cb(err.code); }
cb();
});
};
User.update = function (Env, id, data, cb) {
User.delete(Env, id, (err) => {
if (err) { return void cb(err); }
User.write(Env, id, data, cb);
});
};

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6 */
/* global Buffer */
const ToPull = require('stream-to-pull-stream');

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6 */
/* globals process, Buffer */
const HK = require("../hk-util");

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6 */
/* global process */
const Util = require("../common-util");
const nThen = require('nthen');

2203
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "5.5.0",
"description": "realtime collaborative visual editor with zero knowledge server",
"version": "5.7.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
@ -13,58 +13,62 @@
},
"dependencies": {
"@mcrowe/minibloom": "^0.2.0",
"@node-saml/node-saml": "^4.0.5",
"alertify.js": "1.0.11",
"body-parser": "^1.20.2",
"bootstrap": "^4.0.0",
"bootstrap-tokenfield": "^0.12.0",
"chainpad": "^5.2.6",
"chainpad-crypto": "^0.2.5",
"chainpad-listmap": "^1.0.0",
"chainpad-netflux": "^1.0.0",
"chainpad-server": "^5.1.0",
"ckeditor": "npm:ckeditor4@~4.22.1",
"codemirror": "^5.19.0",
"components-font-awesome": "^4.6.3",
"cookie-parser": "^1.4.6",
"croppie": "^2.5.0",
"dragula": "3.7.2",
"drawio": "github:cryptpad/drawio-npm#npm-21.8.2+4",
"express": "~4.18.2",
"file-saver": "1.3.1",
"fs-extra": "^7.0.0",
"get-folder-size": "^2.0.1",
"netflux-websocket": "^1.0.0",
"html2canvas": "^1.4.0",
"http-proxy-middleware": "^2.0.6",
"hyper-json": "~1.4.0",
"jquery": "3.6.0",
"json.sortify": "~2.1.0",
"jsonwebtoken": "^9.0.0",
"jszip": "3.10.1",
"localforage": "^1.5.2",
"marked": "^4.3.0",
"mathjax": "3.0.5",
"netflux-websocket": "^1.0.0",
"notp": "^2.0.3",
"nthen": "0.1.8",
"open-sans-fontface": "^1.4.0",
"openid-client": "^5.4.2",
"pako": "^2.1.0",
"prompt-confirm": "^2.0.4",
"pull-stream": "^3.6.1",
"require-css": "0.1.10",
"requirejs": "2.3.5",
"requirejs-plugins": "^1.0.2",
"saferphore": "0.0.1",
"scrypt-async": "1.2.0",
"sortablejs": "^1.6.0",
"sortify": "^1.0.4",
"stream-to-pull-stream": "^1.7.2",
"thirty-two": "^1.0.2",
"tweetnacl": "~0.12.2",
"ulimit": "0.0.2",
"ws": "^3.3.1",
"alertify.js": "1.0.11",
"bootstrap": "^4.0.0",
"bootstrap-tokenfield": "^0.12.0",
"chainpad": "^5.2.6",
"chainpad-listmap": "^1.0.0",
"chainpad-netflux": "^1.0.0",
"ckeditor": "npm:ckeditor4@~4.22.1",
"codemirror": "^5.19.0",
"components-font-awesome": "^4.6.3",
"croppie": "^2.5.0",
"file-saver": "1.3.1",
"hyper-json": "~1.4.0",
"jquery": "3.6.0",
"json.sortify": "~2.1.0",
"jszip": "3.10.1",
"dragula": "3.7.2",
"html2canvas": "^1.4.0",
"localforage": "^1.5.2",
"marked": "^4.3.0",
"mathjax": "3.0.5",
"open-sans-fontface": "^1.4.0",
"require-css": "0.1.10",
"requirejs": "2.3.5",
"requirejs-plugins": "^1.0.2",
"scrypt-async": "1.2.0",
"sortablejs": "^1.6.0",
"drawio": "cryptpad/drawio-npm#npm",
"pako": "^2.1.0",
"x2js": "^3.4.4"
},
"devDependencies": {
"jshint": "^2.13.4",
"eslint": "^8.57.0",
"eslint-plugin-compat": "^4.2.0",
"lesshint": "6.3.7"
},
"overrides": {
@ -82,9 +86,8 @@
"offline": "FRESH=1 OFFLINE=1 node server.js",
"offlinedev": "DEV=1 OFFLINE=1 node server.js",
"package": "PACKAGE=1 node server.js",
"lint": "jshint --config .jshintrc --exclude-path .jshintignore . && ./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
"lint:js": "jshint --config .jshintrc --exclude-path .jshintignore .",
"lint:server": "jshint --config .jshintrc lib",
"lint": "eslint . && ./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
"lint:js": "eslint .",
"lint:less": "./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
"lint:translations": "node ./scripts/translations/lint-translations.js",
"unused-translations": "node ./scripts/translations/unused-translations.js",
@ -94,5 +97,6 @@
"build": "node scripts/build.js",
"clear": "node scripts/clear.js",
"installtoken": "node scripts/install.js"
}
},
"browserslist": ["> 0.5%, last 2 versions, Firefox ESR, not dead, not op_mini all"]
}

View File

@ -32,7 +32,7 @@ You can find `Dockerfile`, `docker-compose.yml` and `docker-entrypoint.sh` files
Previously, Docker images were community maintained, had their own repository and weren't official supported. We changed that with v5.4.0 during July 2023. Thanks to @promasu for all the work on the community images.
# Security
# Privacy / Security
CryptPad offers a variety of collaborative tools that encrypt your data in your browser
before it is sent to the server and your collaborators. In the event that the server is
@ -95,7 +95,7 @@ This project is tested with [BrowserStack](https://www.browserstack.com/).
This software is and will always be available under the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the License, or (at your option)
any later version. If you wish to use this technology in a proprietary product, please contact
sales@xwiki.com.
sales@cryptpad.org
[Tor browser]: https://www.torproject.org/download/
[active attack]: https://en.wikipedia.org/wiki/Attack_(computing)#Types_of_attack

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6, node: true */
const Fs = require('fs');
const nThen = require('nthen');
const Nacl = require('tweetnacl/nacl-fast');

20
scripts/clean-activity.js Normal file
View File

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/**
* Some .activity file were created for deleted blob due to a bug.
* This script can be run once to remove these invalid activity file.
**/
var config = require("../lib/load-config");
var BlobStore = require("../lib/storage/blob");
config.getSession = function () {};
BlobStore.create(config, function (err, store) {
if (err) { return console.error('ERROR', err); }
console.log('Cleaning lone .activity files...');
store.remove.loneActivity(function (err) {
if (err) { return console.error('ERROR', err); }
console.log('Done');
});
});

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6, node: true */
const nThen = require("nthen");
const Pins = require("../lib/pins");
const Assert = require("assert");

View File

@ -50,5 +50,6 @@ Fse.rmSync(oldComponentsPath, { recursive: true, force: true });
].forEach(l => {
const source = Path.join("node_modules", l);
const destination = Path.join(componentsPath, l);
Fs.rmSync(destination, { recursive: true, force: true });
Fs.cpSync(source, destination, { recursive: true });
});

View File

@ -15,6 +15,10 @@ var config = require("../lib/load-config");
var Env = Environment.create(config);
// Set DRY_RUN to true to run the script without deleting anything. A log file
// will be created.
Env.DRY_RUN = false;
var loadPremiumAccounts = function (Env, cb) {
nThen(function (w) {
// load premium accounts

View File

@ -15,6 +15,10 @@ var config = require("../lib/load-config");
var Env = Environment.create(config);
// Set DRY_RUN to true to run the script without deleting anything. A log file
// will be created.
Env.DRY_RUN = false;
var loadPremiumAccounts = function (Env, cb) {
nThen(function (w) {
// load premium accounts

View File

@ -2,8 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6, node: true */
const Nacl = require('tweetnacl/nacl-fast');
const keyPair = Nacl.box.keyPair();

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 6, node: true */
const Fs = require('fs');
const Path = require("path");
const Semaphore = require('saferphore');

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// jshint esversion: 6, browser: false, node: true
// This file is for automated testing, it should probably not be invoked for any other purpose.
// It will:
// 1. npm install

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Pins = require("../../lib/pins");
var stats = {

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*jshint esversion: 6 */
const Plan = require("../../lib/plan");
var rand_delay = function (f) {

View File

@ -53,7 +53,7 @@ special_rules.fr = function (s) {
ignore instances where the following character is a '/'
because this is probably a URL (http(s)://)
*/
return /\S[:;\?\!][^\/]{1,}/.test(s);
return /\S[:;\?\!][^\/]{1,}/.test(s.replace(/mailto:/g, " :"));
};
var noop = function () {};

View File

@ -167,7 +167,7 @@ nThen(function (w) {
var throttledEnvChange = Util.throttle(function () {
Env.Log.info('WORKER_ENV_UPDATE', 'Updating HTTP workers with latest state');
broadcast('ENV_UPDATE', Environment.serialize(Env));
}, 250);
}, 250); // NOTE: changing this value will impact lib/commands/admin-rpc.js#adminDecree callback
var throttledCacheFlush = Util.throttle(function () {
Env.Log.info('WORKER_CACHE_FLUSH', 'Instructing HTTP workers to flush cache');

View File

@ -37,12 +37,12 @@
}
}
.cp-admin-setlimit-form, .cp-admin-broadcast-form {
label {
.cp-sidebarlayout-element {
label:not(.cp-admin-label) {
font-weight: normal !important;
}
input {
max-width: 400px;
max-width: 25rem;
}
nav {
display: flex;
@ -246,6 +246,12 @@
}
}
.cp-admin-users {
.cp-admin-store-invited, .cp-admin-store-sso {
margin-bottom: 0 !important;
}
}
.cp-admin-broadcast-form {
input.flatpickr-input {
width: 307.875px !important; // same width as flatpickr calendar

View File

@ -61,8 +61,8 @@ define([
'general': [ // Msg.admin_cat_general
'cp-admin-flush-cache',
'cp-admin-update-limit',
'cp-admin-registration',
'cp-admin-enableembeds',
'cp-admin-forcemfa',
'cp-admin-email',
'cp-admin-instance-info-notice',
@ -72,6 +72,11 @@ define([
'cp-admin-jurisdiction',
'cp-admin-notice',
],
'users': [ // Msg.admin_cat_quota
'cp-admin-registration',
'cp-admin-invitation',
'cp-admin-users',
],
'quota': [ // Msg.admin_cat_quota
'cp-admin-defaultlimit',
'cp-admin-setlimit',
@ -131,7 +136,7 @@ define([
// Convert to camlCase for translation keys
var safeKey = keyToCamlCase(key);
var $div = $('<div>', {'class': 'cp-admin-' + key + ' cp-sidebarlayout-element'});
$('<label>', {'id': 'cp-admin-' + key}).text(Messages['admin_'+safeKey+'Title'] || key).appendTo($div);
$('<label>', {'id': 'cp-admin-' + key, 'class':'cp-admin-label'}).text(Messages['admin_'+safeKey+'Title'] || key).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages['admin_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div);
if (addButton) {
@ -1203,6 +1208,25 @@ define([
return tableObj.table;
};
var getBlockId = (val) => {
var url;
try {
url = new URL(val, ApiConfig.httpUnsafeOrigin);
} catch (err) { }
var getKey = function () {
var parts = val.split('/');
return parts[parts.length - 1];
};
var isValidBlockURL = function (url) {
if (!url) { return; }
return /* url.origin === ApiConfig.httpUnsafeOrigin && */ /^\/block\/.*/.test(url.pathname) && getKey().length === 44;
};
if (isValidBlockURL(url)) {
return getKey();
}
return;
};
create['block-metadata'] = function () {
var key = 'block-metadata';
var $div = makeBlock(key, true); // Msg.admin_blockMetadataHint.admin_blockMetadataTitle
@ -1235,21 +1259,10 @@ define([
key: '',
};
var url;
try {
url = new URL(val, ApiConfig.httpUnsafeOrigin);
} catch (err) { }
var getKey = function () {
var parts = val.split('/');
return parts[parts.length - 1];
};
var isValidBlockURL = function (url) {
if (!url) { return; }
return /* url.origin === ApiConfig.httpUnsafeOrigin && */ /^\/block\/.*/.test(url.pathname) && getKey().length === 44;
};
if (isValidBlockURL(url)) {
var key = getBlockId(val);
if (key) {
state.valid = true;
state.key = getKey();
state.key = key;
}
return state;
};
@ -1482,7 +1495,11 @@ Example
};
// Msg.admin_registrationHint, .admin_registrationTitle
create['registration'] = makeAdminCheckbox({
// Msg.admin_registrationSsoTitle
create['registration'] = function () {
var refresh = function () {};
var $div = makeAdminCheckbox({
key: 'registration',
getState: function () {
return APP.instanceStatus.restrictRegistration;
@ -1498,11 +1515,454 @@ Example
}
APP.updateStatus(function () {
setState(APP.instanceStatus.restrictRegistration);
refresh();
flushCacheNotice();
});
});
}
})();
var $sso = makeAdminCheckbox({
key: 'registration-sso',
getState: function () {
return APP.instanceStatus.restrictSsoRegistration;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['RESTRICT_SSO_REGISTRATION', [val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(APP.instanceStatus.restrictSsoRegistration);
flushCacheNotice();
});
});
}
})();
var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
if (ssoEnabled) {
$sso.find('#cp-admin-registration-sso').hide();
$sso.find('> span.cp-sidebarlayout-description').hide();
$div.append($sso);
}
refresh = () => {
var closed = APP.instanceStatus.restrictRegistration;
if (closed) {
$sso.show();
} else {
$sso.hide();
}
};
refresh();
return $div;
};
create['invitation'] = function () {
var key = 'invitation';
var $div = makeBlock(key); // Msg.admin_invitationHint, admin_invitationTitle
var list = h('table.cp-admin-all-limits');
var input = h('input#cp-admin-invitation-alias');
var inputEmail = h('input#cp-admin-invitation-email');
var button = h('button.btn.btn-primary', Messages.admin_invitationCreate);
var $b = $(button);
var refreshInvite = function () {};
var refresh = h('button.btn.btn-secondary', Messages.oo_refresh);
Util.onClickEnter($(refresh), function () {
refreshInvite();
});
var add = h('div', [
h('label', { for: 'cp-admin-invitation-alias' }, Messages.admin_invitationAlias),
input,
h('label', { for: 'cp-admin-invitation-email' }, Messages.admin_invitationEmail),
inputEmail,
h('nav', [button, refresh])
]);
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var deleteInvite = function (id) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'DELETE_INVITATION',
data: id
}, function (e, response) {
$b.prop('disabled', false);
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
refreshInvite();
});
};
var $list = $(list);
refreshInvite = function () {
$list.empty();
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'GET_ALL_INVITATIONS',
}, function (e, response) {
if (e || response.error) {
if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
console.error(e, response);
return;
}
if (!Array.isArray(response)) { return; }
var all = response[0];
Object.keys(all).forEach(function (key, i) {
if (!i) { // First item: add header to table
var trHead = h('tr', [
h('th', Messages.admin_invitationLink),
h('th', Messages.admin_invitationAlias),
h('th', Messages.admin_invitationEmail),
h('th', Messages.admin_documentCreationTime),
h('th')
]);
$list.append(trHead);
}
var data = all[key];
var url = privateData.origin + Hash.hashToHref(key, 'register');
var del = h('button.btn.btn-danger', [
h('i.fa.fa-trash'),
h('span', Messages.kanban_delete)
]);
var $del = $(del);
Util.onClickEnter($del, function () {
$del.attr('disabled', 'disabled');
UI.confirm(Messages.admin_invitationDeleteConfirm, function (yes) {
$del.attr('disabled', '');
if (!yes) { return; }
deleteInvite(key);
});
});
var copy = h('button.btn.btn-secondary', [
h('i.fa.fa-clipboard'),
h('span', Messages.admin_invitationCopy)
]);
Util.onClickEnter($(copy), function () {
Clipboard.copy(url, () => {
UI.log(Messages.genericCopySuccess);
});
});
var line = h('tr', [
h('td', UI.dialog.selectable(url)),
h('td', data.alias),
h('td', data.email),
h('td', new Date(data.time).toLocaleString()),
//h('td', data.createdBy),
h('td', [
copy,
del
])
]);
$list.append(line);
});
});
};
refreshInvite();
$b.on('click', () => {
var alias = $(input).val().trim();
if (!alias) { return void UI.warn(Messages.error); } // FIXME better error message
$b.prop('disabled', true);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'CREATE_INVITATION',
data: {
alias,
email: $(inputEmail).val()
}
}, function (e, response) {
$b.prop('disabled', false);
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
$(input).val('').focus();
$(inputEmail).val('');
refreshInvite();
});
});
$div.append([add, list]);
return $div;
};
create['users'] = function () {
var key = 'users';
var $div = makeBlock(key); // Msg.admin_usersHint, admin_usersTitle
var list = h('table.cp-admin-all-limits');
var userAlias = h('input#cp-admin-users-alias');
var userEmail = h('input#cp-admin-users-email');
var userEdPublic = h('input#cp-admin-users-key');
var userBlock = h('input#cp-admin-users-block');
var button = h('button.btn.btn-primary', Messages.admin_usersAdd);
var $b = $(button);
var refreshUsers = function () {};
var refresh = h('button.btn.btn-secondary', Messages.oo_refresh);
Util.onClickEnter($(refresh), function () {
refreshUsers();
});
var add = h('div', [
h('label', { for: 'cp-admin-users-alias' }, Messages.admin_invitationAlias),
userAlias,
h('label', { for: 'cp-admin-users-email' }, Messages.admin_invitationEmail),
userEmail,
h('label', { for: 'cp-admin-users-key' }, Messages.admin_limitUser),
userEdPublic,
h('label', { for: 'cp-admin-users-block' }, Messages.admin_usersBlock),
userBlock,
h('nav', [button, refresh])
]);
var $invited = makeAdminCheckbox({
key: 'store-invited',
getState: function () {
return !APP.instanceStatus.dontStoreInvitedUsers;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['DISABLE_STORE_INVITED_USERS', [!val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(!APP.instanceStatus.dontStoreInvitedUsers);
flushCacheNotice();
});
});
}
})();
$invited.find('#cp-admin-store-invited').hide();
$invited.find('> span.cp-sidebarlayout-description').hide();
$div.append($invited);
var $sso = makeAdminCheckbox({
key: 'store-sso',
getState: function () {
return !APP.instanceStatus.dontStoreSSOUsers;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['DISABLE_STORE_SSO_USERS', [!val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(!APP.instanceStatus.dontStoreSSOUsers);
flushCacheNotice();
});
});
}
})();
var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
if (ssoEnabled) {
$sso.find('#cp-admin-store-sso').hide();
$sso.find('> span.cp-sidebarlayout-description').hide();
$div.append($sso);
}
var deleteUser = function (id) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'DELETE_KNOWN_USER',
data: id
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
refreshUsers();
});
};
var updateUser = function (key, changes) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'UPDATE_KNOWN_USER',
data: {
edPublic: key,
changes: changes
}
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
refreshUsers();
});
};
var $list = $(list);
refreshUsers = function () {
$list.empty();
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'GET_ALL_USERS',
}, function (e, response) {
if (e || response.error) {
if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
console.error(e, response);
return;
}
if (!Array.isArray(response)) { return; }
var all = response[0];
Object.keys(all).forEach(function (key, i) {
if (!i) { // First item: add header to table
var trHead = h('tr', [
h('th', Messages.admin_invitationAlias),
h('th', Messages.admin_invitationEmail),
h('th', Messages.admin_limitUser),
h('th', Messages.admin_documentCreationTime),
h('th')
]);
$list.append(trHead);
}
var data = all[key];
var editUser = () => {};
var del = h('button.btn.btn-danger', [
h('i.fa.fa-trash'),
Messages.admin_usersRemove
]);
var $del = $(del);
Util.onClickEnter($del, function () {
$del.attr('disabled', 'disabled');
UI.confirm(Messages.admin_usersRemoveConfirm, function (yes) {
$del.attr('disabled', '');
if (!yes) { return; }
deleteUser(key);
});
});
var edit = h('button.btn.btn-secondary', [
h('i.fa.fa-pencil'),
h('span', Messages.tag_edit)
]);
Util.onClickEnter($(edit), function () {
editUser();
});
var alias = h('td', data.alias);
var email = h('td', data.email);
var actions = h('td', [edit, del]);
var $alias = $(alias);
var $email = $(email);
var $actions = $(actions);
editUser = () => {
var aliasInput = h('input');
var emailInput = h('input');
$(aliasInput).val(data.alias);
$(emailInput).val(data.email);
var save = h('button.btn.btn-primary', Messages.settings_save);
var cancel = h('button.btn.btn-secondary', Messages.cancel);
Util.onClickEnter($(save), function () {
var aliasVal = $(aliasInput).val().trim();
if (!aliasVal) { return void UI.warn(Messages.error); }
var changes = {
alias: aliasVal,
email: $(emailInput).val().trim()
};
updateUser(key, changes);
});
Util.onClickEnter($(cancel), function () {
refreshUsers();
});
$alias.html('').append(aliasInput);
$email.html('').append(emailInput);
$actions.html('').append([save, cancel]);
console.warn(alias, email, $alias, $email, aliasInput);
};
var infoButton = h('button.btn.primary.cp-report', {
style: 'margin-left: 10px; cursor: pointer;',
}, [
h('i.fa.fa-database'),
h('span', Messages.admin_diskUsageButton)
]);
$(infoButton).click(() => {
getAccountData(key, (err, data) => {
if (err) { return void console.error(err); }
var table = renderAccountData(data);
UI.alert(table, () => {
}, {
wide: true,
});
});
});
var line = h('tr', [
alias,
email,
h('td', [
h('code', key),
infoButton
]),
h('td', new Date(data.time).toLocaleString()),
//h('td', data.createdBy),
actions
]);
$list.append(line);
});
});
};
refreshUsers();
$b.on('click', () => {
var alias = $(userAlias).val().trim();
if (!alias) { return void UI.warn(Messages.error); }
$b.prop('disabled', true);
var done = () => { $b.prop('disabled', false); };
// TODO Get "block" from pin log?
var keyStr = $(userEdPublic).val().trim();
var edPublic = keyStr && Keys.canonicalize(keyStr);
if (!edPublic) {
done();
return void UI.warn(Messages.admin_invalKey);
}
var block = getBlockId($(userBlock).val());
var obj = {
alias,
email: $(userEmail).val(),
block: block,
edPublic: edPublic,
};
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADD_KNOWN_USER',
data: obj
}, function (e, response) {
done();
if (e || response.error) {
UI.warn(Messages.error);
return void console.error(e, response);
}
$(userAlias).val('').focus();
$(userEmail).val('');
$(userBlock).val('');
$(userEdPublic).val('');
refreshUsers();
});
});
$div.append([add, list]);
return $div;
};
// Msg.admin_enableembedsHint, .admin_enableembedsTitle
create['enableembeds'] = makeAdminCheckbox({
@ -1527,6 +1987,29 @@ Example
},
});
// Msg.admin_forcemfaHint, .admin_forcemfaTitle
create['forcemfa'] = makeAdminCheckbox({
key: 'forcemfa',
getState: function () {
return APP.instanceStatus.enforceMFA;
},
query: function (val, setState) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['ENFORCE_MFA', [val]]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
}
APP.updateStatus(function () {
setState(APP.instanceStatus.enforceMFA);
flushCacheNotice();
});
});
},
});
create['email'] = function () {
var key = 'email';
var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle
@ -1889,19 +2372,23 @@ Example
var key = 'setlimit';
var $div = makeBlock(key); // Msg.admin_setlimitHint, .admin_setlimitTitle
var user = h('input.cp-setlimit-key', { id: 'user-input' });
var user = h('input.cp-setlimit-key#cp-admin-setlimit-user');
var $key = $(user);
var limit = h('input.cp-setlimit-quota', { type: 'number', min: 0, value: 0, id: 'limit-input' });
var note = h('input.cp-setlimit-note', { id: 'note-input' });
var limit = h('input.cp-setlimit-quota#cp-admin-setlimit-value', {
type: 'number',
min: 0,
value: 0
});
var note = h('input.cp-setlimit-note#cp-admin-setlimit-note');
var remove = h('button.btn.btn-danger', Messages.fc_remove);
var set = h('button.btn.btn-primary', Messages.admin_setlimitButton);
var form = h('div.cp-admin-setlimit-form', [
h('label', { for: 'user-input' }, Messages.admin_limitUser),
h('label', { for: 'cp-admin-setlimit-user' }, Messages.admin_limitUser),
user,
h('label', { for: 'limit-input' }, Messages.admin_limitMB),
h('label', { for: 'cp-admin-setlimit-value' }, Messages.admin_limitMB),
limit,
h('label', { for: 'note-input' }, Messages.admin_limitSetNote),
h('label', { for: 'cp-admin-setlimit-note' }, Messages.admin_limitSetNote),
note,
h('nav', [set, remove])
]);
@ -3421,6 +3908,7 @@ Example
var SIDEBAR_ICONS = {
general: 'fa fa-user-o',
stats: 'fa fa-line-chart',
users: 'fa fa-address-card-o',
quota: 'fa fa-hdd-o',
support: 'fa fa-life-ring',
broadcast: 'fa fa-bullhorn',

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/* jshint esversion: 7 */
define([], function () {
// Based on https://gist.github.com/bellbind/871b145110c458e83077a718aef9fa0e

View File

@ -81,7 +81,7 @@ define(['/api/config'], function (ApiConfig) {
// Inform the user that we won't navigate and that the 'bounce tab' will be closed.
// our linter warns when it sees 'javascript:' because it doesn't distinguish between
// detecting this pattern and using it, so we ignore this line
if (['javascript:', 'vbscript:', 'data:', 'blob:'].includes(target.protocol)) { // jshint ignore:line
if (['javascript:', 'vbscript:', 'data:', 'blob:'].includes(target.protocol)) {
window.alert(Messages._getKey('bounce_danger', [target.href]));
return void reject();
}

View File

@ -162,9 +162,12 @@
color: @cryptpad_text_col;
border-radius: @variables_radius_L;
font-weight: normal;
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox) {
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox):not(.fa-map-marker) {
display: none;
}
.tui-full-calendar-icon {
text-align:center;
}
.tui-full-calendar-popup-detail-item {
a {
color: @cryptpad_color_link;
@ -250,7 +253,7 @@
}
.CodeMirror {
margin-top: 5px;
background: @cp_forms-bg;
background: @cp_forms-bg !important;
color: @cryptpad_text_col;
border: 1px solid @cp_forms-border;
border-radius: @variables_radius;

View File

@ -23,6 +23,7 @@ define([
'/calendar/export.js',
'/calendar/recurrence.js',
'/lib/datepicker/flatpickr.js',
'tui-date-picker',
'/common/inner/share.js',
'/common/inner/access.js',
@ -64,17 +65,13 @@ define([
Export,
Rec,
Flatpickr,
DatePicker,
Share, Access, Properties,
diffMk,
SFCodeMirror,
CodeMirror
)
{
// XXX New translation keys
Messages.calendar_rec_change_first = "You moved the first repeating event to different calendar. You can only apply this change to all repeated events."; // XXX New translation key
Messages.calendar_rec_change = "You moved a repeating event to different calendar. You can only apply this change to this event or all repeated events."; // XXX New translation key
Messages.calendar_desc = "Description"; // XXX maybe rename in `description`?
Messages.calendar_description = "Description:{0}{1}"; // XXX
var SaveAs = window.saveAs;
var APP = window.APP = {
@ -124,6 +121,8 @@ define([
var startDate = event.start._date;
var endDate = event.end._date;
var timeZone;
try { timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) {}
var data = {
id: Util.uid(),
calendarId: event.calendarId,
@ -135,6 +134,7 @@ define([
end: +endDate,
reminders: reminders,
body: eventBody,
timeZone: timeZone,
recurrenceRule: event.recurrenceRule
};
@ -306,9 +306,9 @@ define([
obj.title = obj.title || "";
obj.location = obj.location || "";
obj.body = obj.body || "";
if (obj.isAllDay && obj.startDay) { obj.start = +Flatpickr.parseDate((obj.startDay)); }
if (obj.isAllDay && obj.startDay) { obj.start = +DatePicker.parseDate((obj.startDay)); }
if (obj.isAllDay && obj.endDay) {
var endDate = Flatpickr.parseDate(obj.endDay);
var endDate = DatePicker.parseDate(obj.endDay);
endDate.setHours(23);
endDate.setMinutes(59);
endDate.setSeconds(59);
@ -408,13 +408,13 @@ define([
str = `<a href="${l}" id="${uid}">${str}</a>`;
APP.nextLocationUid = uid;
}
return Messages._getKey('calendar_location', [str]);
let location_icon = h('i.fa.fa-map-marker.tui-full-calendar-icon', { 'aria-label': Messages.calendar_loc }, []);
return location_icon.outerHTML + str;
},
popupDetailBody: function(schedule) {
var str = schedule.body;
delete APP.eventBody;
return Messages._getKey('calendar_description', ['<br />', diffMk.render(str, true)]);
return diffMk.render(str, true);
},
popupIsAllDay: function() { return Messages.calendar_allDay; },
titlePlaceholder: function() { return Messages.calendar_title; },
@ -1012,13 +1012,12 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
makeLeftside(cal, $(leftside));
cal.on('beforeCreateSchedule', function(event) {
event.recurrenceRule = APP.recurrenceRule; // XXX Not sure about the consistency of data structures
event.recurrenceRule = APP.recurrenceRule;
newEvent(event, function (err) {
if (err) {
console.error(err);
return void UI.warn(err);
}
//cal.createSchedules([schedule]); XXX Remove these occurrences elsewhere
});
});
cal.on('beforeUpdateSchedule', function(event) {
@ -1127,7 +1126,6 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
console.error(err);
return void UI.warn(err);
}
//cal.updateSchedule(old.id, old.calendarId, changes);
});
}
};
@ -1309,23 +1307,20 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
updateDateRange();
updateRecurring();
});
$(goDate).click(function () {
var f = Flatpickr(goDate, {
enableTime: false,
defaultDate: APP.calendar.getDate()._date,
clickOpens: false,
//dateFormat: dateFormat,
onChange: function (date) {
date[0].setHours(12);
f.destroy();
APP.moveToDate(+date[0]);
updateDateRange();
updateRecurring();
},
onClose: function () {
setTimeout(f.destroy);
}
});
f.open();
$(goDate).click(function () {
return f.isOpen ? f.close() : f.open();
});
APP.toolbar.$bottomL.append(h('div.cp-calendar-browse', [
goLeft, goToday, goRight, goDate
@ -1382,6 +1377,8 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
}
if (m) {
m = m.map(function (n) {
tmp.setDate(15);
tmp.setHours(12);
tmp.setMonth(n-1);
return tmp.toLocaleDateString(getDateLanguage(), { month: 'long' });
});
@ -1468,7 +1465,7 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
dayStr,
monthStr
]),
last: last ? '-1' + dayStr : undefined,
last: last ? '-1' + dayCode : undefined,
lastStr: Messages._getKey('calendar_rec_'+key+'_nth', [
Messages['calendar_nth_last'],
dayStr,
@ -1670,7 +1667,7 @@ APP.recurrenceRule = {
minDate: date,
//dateFormat: dateFormat,
onChange: function () {
//endPickr.set('minDate', startPickr.parseDate(s.value));
//endPickr.set('minDate', DatePicker.parseDate(s.value));
}
});
var endDate = new Date(+date);
@ -1848,7 +1845,7 @@ APP.recurrenceRule = {
if (until === "count") {
rec.count = $(radioCount).find('input[type="number"]').val();
} else if (until === "date") {
var _date = Flatpickr.parseDate(pickr.value);
var _date = DatePicker.parseDate(pickr.value);
_date.setDate(_date.getDate()+1);
rec.until = +_date - 1;
}
@ -2182,7 +2179,7 @@ APP.recurrenceRule = {
var $button = $el.find('.tui-full-calendar-section-button-save');
var $startDate = $el.find('#tui-full-calendar-schedule-start-date');
var startDate = Flatpickr.parseDate($startDate.val());
var startDate = DatePicker.parseDate($startDate.val());
var divRec = getRecurrenceInput(startDate);
$button.before(divRec);

View File

@ -582,6 +582,27 @@ define([
return r;
}).filter(Boolean);
};
var fixTimeZone = function (evTimeZone, origin, target) {
var getOffset = function (date, tz) {
// Get an ISO string using Canadian local format
let iso = date.toLocaleString('en-CA', { timeZone:tz, hour12: false }).replace(', ', 'T');
iso += '.' + date.getMilliseconds().toString().padStart(3, '0');
// Get a UTC version of this time
let utcDate = new Date(iso + 'Z');
// Return the difference in timestamps, as minutes (60*1000)
return -(utcDate - date);
};
var myTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
var offset = getOffset(origin, evTimeZone) - getOffset(target, evTimeZone);
var myOffset = getOffset(origin, myTimeZone) - getOffset(target, myTimeZone);
return myOffset - offset;
};
Rec.getRecurring = function (months, events) {
if (window.CP_DEV_MODE) { debug = console.warn; }
@ -739,6 +760,11 @@ define([
}
// Add this event
if (_origin.timeZone && !_ev.isAllDay) {
var offset = fixTimeZone(_origin.timeZone, _start, _evS);
_ev.start += offset;
_ev.end += offset;
}
toAdd.push(_ev);
if (newrule) {
useNewRule();

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