Merge remote-tracking branch 'origin/staging' into insert-onlyoffice-api

This commit is contained in:
Wolfgang Ginolas 2025-01-22 09:29:00 +01:00
commit c37dfb1eec
No known key found for this signature in database
226 changed files with 7863 additions and 2045 deletions

View File

@ -1,40 +0,0 @@
# SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
#
# SPDX-License-Identifier: AGPL-3.0-or-later
node_modules/
www/components/
www/bower_components/
www/common/onlyoffice/dist
www/common/onlyoffice/x2t
onlyoffice-dist/
www/scratch
www/accounts
www/lib
www/accounts
www/worker
www/todo
#lib/plugins/
www/common/hyperscript.js
www/pad/wysiwygarea-plugin.js
www/pad/mediatag-plugin.js
www/pad/mediatag-plugin-dialog.js
www/pad/disable-base64.js
www/pad/wordcount/
www/kanban/jkanban.js
www/common/jscolor.js
www/common/media-tag-nacl.min.js
customize/
www/debug/chainpad.dist.js
www/pad/mathjax/
www/code/mermaid*.js
www/code/orgmode.js

View File

@ -1,58 +0,0 @@
// SPDX-FileCopyrightText: 2024 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
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-self-assign': ['off'],
'no-empty': ['off'],
'no-useless-escape': ['off'],
'no-extra-boolean-cast': ['off'],
'no-prototype-builtins': ['off'],
}
};

View File

@ -89,19 +89,13 @@ body:
label: Version
description: What version of CryptPad are you running?
options:
- 2024.12.0
- 2024.9.1
- 2024.9.0
- 2024.6.1
- 2024.6.0
- 2024.3.1
- 2024.3.0
- 5.7.0
- 5.6.0
- 5.5.0
- 5.4.1
- 5.4.0
- 5.3.0
- 5.2.1
- 5.2.0
- 5.1.0
- 5.0.0
- Other
validations:
required: true

13
.gitignore vendored
View File

@ -17,12 +17,11 @@ customize
messages.log
www/scratch
data
pins/
blob/
block/
blobstage/
block/
logs/
pins
blob
block
blobstage
logs
privileged.conf
config/config.js
config/sso.js
@ -33,6 +32,8 @@ www/common/onlyoffice/v*
/onlyoffice-conf/
/onlyoffice-dist/
_build
# ---> Node
# Logs
logs

View File

@ -29,6 +29,10 @@ Files: .stylelintrc.js
Copyright: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
License: AGPL-3.0-or-later
Files: scripts/tests/test-data/*
Copyright: 2024 XWiki CryptPad Team <contact@cryptpad.org> and contributors
License: AGPL-3.0-or-later
## Dependencies
Files: www/common/theme/*

View File

@ -19,6 +19,7 @@ module.exports = {
"declaration-block-no-shorthand-property-overrides": null,
"comment-whitespace-inside": null,
"no-invalid-double-slash-comments": null,
"property-no-vendor-prefix": null,
"selector-no-vendor-prefix": null,

View File

@ -4,6 +4,145 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and cont
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# Autumn release (2024.9.0)
## Goals
This release improves the performance of CryptPad with server optimizations and an optional cryptography plugin. We also include lots of improvements and fixes across various areas.
## Features
- Add support for cryptography plugins to replace tweetnacl on the server [#1667](https://github.com/cryptpad/cryptpad/pull/1667)
- We use our [CryptPad Sodium plugin](https://github.com/cryptpad/cryptpad-sodium-plugin) to improve the performance of our flagship instance cryptpad.fr.
- Documentation will be available shortly in the form of a blog post and section in the admin guide.
## Improvements
- Server memory improvements [#1543](https://github.com/cryptpad/cryptpad/pull/1543)
- Server optimization when computing metadata [#1566](https://github.com/cryptpad/cryptpad/pull/1566)
- Team roster channels slow down team members accounts [#1614](https://github.com/cryptpad/cryptpad/pull/1614)
- Deployment
- Add example configs for Caddy [#1603](https://github.com/cryptpad/cryptpad/pull/1603)
- Add support for HTTP -> HTTPS (80 -> 443 ports) redirection [#1582](https://github.com/cryptpad/cryptpad/pull/1582)
- Added "git config --add safe.directory" [#1539](https://github.com/cryptpad/cryptpad/pull/1539)
- Add confirmation modal when admins turn on mandatory 2FA [#1552](https://github.com/cryptpad/cryptpad/pull/1552)
- Developers on Windows can now launch their local instance with `npm run windev`
## Fixes
- Modals accessibility
- `Shift-Tab` option for `Ctrl-E` modals [#1647](https://github.com/cryptpad/cryptpad/pull/1647)
- Remove focus from disabled elements on modals [#1618](https://github.com/cryptpad/cryptpad/pull/1618)
- `+New` button fixes on Drive [#1610](https://github.com/cryptpad/cryptpad/pull/1610)
- Accessibility improvements to modals [#1563](https://github.com/cryptpad/cryptpad/pull/1563)
- `Ctrl+E` modal fixes [#1559](https://github.com/cryptpad/cryptpad/pull/1559)
- Focus style fixes [#1560](https://github.com/cryptpad/cryptpad/pull/1560)
- Make password change confirmation button responsive on mobile [#1569](https://github.com/cryptpad/cryptpad/pull/1569)
- Calendar
- Fix calendar ownership sharing bug [#1655](https://github.com/cryptpad/cryptpad/pull/1655)
- Calendar modal UI fixes [#1615](https://github.com/cryptpad/cryptpad/pull/1615)
- Make 'New Event' modal appear in user view for small screens [#1583](https://github.com/cryptpad/cryptpad/pull/1583)
- Teams
- Improve Teams card list accessibility [#1585](https://github.com/cryptpad/cryptpad/pull/1585)
- Fix emoji avatar handling and team name overflowing in sidebar teams [#1598](https://github.com/cryptpad/cryptpad/pull/1598)
- Toolbar/menus
- Make code history buttons not overlap on small screens [#1586](https://github.com/cryptpad/cryptpad/pull/1586)
- Fix misaligned/missing file dropdown menu items [#1578](https://github.com/cryptpad/cryptpad/pull/1578)
- Diagram
- Ignore unknown fields in diagram document [#1666](https://github.com/cryptpad/cryptpad/commit/02da76d3de76455a5573dd43d7ef6e68bf62c959)
- Forms
- Fix overflowing check and radio items in form app conditional [#1591](https://github.com/cryptpad/cryptpad/pull/1591)
- Kanban
- fixed a bug that was causing duplicate cards when many editors were collaborating [02da76d](https://github.com/cryptpad/cryptpad/commit/02da76d3de76455a5573dd43d7ef6e68bf62c959)
- OnlyOffice
- OnlyOffice document out of sync with multiple tabs as guest [#1671](https://github.com/cryptpad/cryptpad/issues/1671)
- Miscellaneous
- Fix ownership/sharing issues with password-protected pads [#1565](https://github.com/cryptpad/cryptpad/pull/1565)
- Make performance table responsive for mobile (admin panel) [#1555](https://github.com/cryptpad/cryptpad/pull/1555)
- Fix bash indentation and trailing whitespaces [#1548](https://github.com/cryptpad/cryptpad/pull/1548)
- Drive focus fixes [#1611](https://github.com/cryptpad/cryptpad/pull/1611)
## Dependencies
We upgraded the following packages
- cryptpad
- chainpad-server to `5.2.2`
- netflux-websocket to `1.2.1`
- third-party
- express to `4.21.0`
- http-proxy-middleware to `3.0.2`
- requirejs to `2.3.7`
- stylelint to `16.9.0`
## Upgrade notes
If you are upgrading from a version older than `2024.6.1` please read the upgrade notes of all versions between yours and `2024.9.0` to avoid configuration issues.
To upgrade:
1. Stop your server
2. Get the latest code with git
```bash
git fetch origin --tags
git checkout 2024.9.0
npm ci
npm run install:components
./install-onlyoffice.sh
```
3. Restart your server
4. Review your instance's checkup page to ensure that you are passing all tests
# 2024.6.1
## Goals
This is a bugfix release to address issues that were reported by Cryptpad.fr users. We took the opportunity to update the translations with some new languages contributed by the community.
## Improvements
- Translations update from CryptPad Translations [#1575](https://github.com/cryptpad/cryptpad/pull/1575)
- Added: Español cubano, اَلْعَرَبِيَّةُ Arabic, Svenska
- Removed some languages without enough coverage
- Greek (16%)
- Romanian (36%)
## Fixes
- Calendar events sometimes dont appear when created [#1551](https://github.com/cryptpad/cryptpad/issues/1551) fixed by [072dba2](https://github.com/cryptpad/cryptpad/commit/072dba254e3c2be32cd6b261d84510909deb713f)
- Revert the new method of counting registered users in the admin panel [4544be6](https://github.com/cryptpad/cryptpad/commit/4544be6b4d9fa7291b19cb366f7dd492dfe07340)
- Fix broken OnlyOffice Document [#1572](https://github.com/cryptpad/cryptpad/issues/1572)
- Fix printing in Code documents [#1557](https://github.com/cryptpad/cryptpad/pull/1557) [#1478](https://github.com/cryptpad/cryptpad/pull/1478)
- Fix OnlyOffice undefined functions [#1550](https://github.com/cryptpad/cryptpad/pull/1550)
- Fix keyboard operation of confirm modals [#1576](https://github.com/cryptpad/cryptpad/issues/1576)
- Pressing Enter on the "Cancel" button triggered the "OK" button instead
## Upgrade notes
If you are upgrading from a version older than `2024.6.0` please read the upgrade notes of all versions between yours and `2024.6.1` to avoid configuration issues.
To upgrade:
1. Stop your server
2. Get the latest code with git
```bash
git fetch origin --tags
git checkout 2024.6.1
npm ci
npm run install:components
./install-onlyoffice.sh
```
3. Restart your server
4. Review your instance's checkup page to ensure that you are passing all tests
# 2024.6.0
## Goals
@ -14,7 +153,7 @@ This release introduces a new onboarding flow to guide administrators through th
- Onboarding screens & app configuration [#1513](https://github.com/cryptpad/cryptpad/pull/1513)
- Bahasa Indonesia is a new available language [fe78b6a](https://github.com/cryptpad/cryptpad/commit/fe78b6ab1dc76ce9eb8d5361c309db8e92117fa8)
- Thanks to our [Weblate](https://weblate.cryptpad.org) contributors who made that happen!
- Thanks to our [Weblate](https://weblate.cryptpad.org) contributors who made that happen!
## Improvements
@ -35,6 +174,7 @@ This release introduces a new onboarding flow to guide administrators through th
- Remove x2t from the CryptPad repo [#1454](https://github.com/cryptpad/cryptpad/issues/1454)
- Other OnlyOffice users are shown as "Guest" [#1446](https://github.com/cryptpad/cryptpad/issues/1446)
- Document PDF exports are empty when remote embedding is disabled [#1472](https://github.com/cryptpad/cryptpad/issues/1472)
- Sometimes images of a presentation are not exported to PDF [#1500](https://github.com/cryptpad/cryptpad/issues/1500)
- Automatic upgrade of an OnlyOffice document fails sometimes [#1534](https://github.com/cryptpad/cryptpad/issues/1534)
- Import/Export is broken [#1532](https://github.com/cryptpad/cryptpad/issues/1532)
- Print is broken [#1533](https://github.com/cryptpad/cryptpad/issues/1533)
@ -46,7 +186,7 @@ This release introduces a new onboarding flow to guide administrators through th
- Switch to new `http2` Nginx option [#1516](https://github.com/cryptpad/cryptpad/pull/1516)
- Server fixes and aggregated stats [#1509](https://github.com/cryptpad/cryptpad/pull/1509)
- Create the block folder at boot [#911](https://github.com/cryptpad/cryptpad/pull/911)
- Remove obsolete `version` from `docker-compose.yml` [2e716eb](https://github.com/cryptpad/cryptpad/commit/2e716eb4e39fb835f95a1fa1a340e01142d11b1c)
- Remove obsolete `version` from `docker-compose.yml` [2e716eb](https://github.com/cryptpad/cryptpad/commit/2e716eb4e39fb835f95a1fa1a340e01142d11b1c)
- Other
- Unsharp the corners when hovering the dismiss button on notification drop-down menu [#1466](https://github.com/cryptpad/cryptpad/pull/1466)
- Fix contextual menu `Open` on anonymous drive [#1464](https://github.com/cryptpad/cryptpad/pull/1464)

View File

@ -14,7 +14,7 @@ COPY . /cryptpad
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
RUN npm install --production \
&& npm run install:components

View File

@ -1,4 +1,4 @@
Copyright (c) <year> <owner>.
Copyright (c) <year> <owner>.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

View File

@ -35,7 +35,7 @@ Mozilla Public License Version 2.0
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script async data-bootload="/customize/four-oh-four.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/customize/four-oh-four.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body class="html">
<noscript>

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script async data-bootload="/customize/four-oh-four.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/customize/four-oh-four.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body class="html">
<noscript>

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body class="html">
<noscript></noscript>

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body class="html">
<noscript></noscript>

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body class="html">
<noscript></noscript>

View File

@ -5,11 +5,13 @@
(function () {
// add your module to this map so it gets used
var map = {
'ar': 'اَلْعَرَبِيَّةُ',
'ca': 'Català',
'cs': 'Čeština',
'de': 'Deutsch',
'el': 'Ελληνικά',
//'el': 'Ελληνικά',
'es': 'Español',
'es_CU': 'Español cubano',
'eu': 'Euskara',
'fi': 'Suomi',
'fr': 'Français',
@ -22,9 +24,9 @@ var map = {
'pl': 'Polski',
'pt-br': 'Português do Brasil',
'pt-pt': 'Português do Portugal',
'ro': 'Română',
//'ro': 'Română',
'ru': 'Русский',
//'sv': 'Svenska',
'sv': 'Svenska',
//'te': 'తెలుగు',
'uk': 'Українська',
'zh': '中文(簡體)',
@ -133,7 +135,6 @@ define(req, function(AppConfig, Default, Language) {
return text;
}
};
return Messages;
});

View File

@ -28,8 +28,9 @@ define([
var premiumButton = h('a', {
href: accounts.upgradeURL,
target: '_blank',
rel: 'noopener noreferrer'
}, h('button.cp-features-register-button', Msg.features_f_subscribe));
rel: 'noopener noreferrer',
class: 'cp-features-register-button',
}, Msg.features_f_subscribe);
var groupItemTemplate = function (title, content) {
return h('li.list-group-item', [
@ -119,14 +120,15 @@ define([
h('div.card-body',[
h('div.cp-features-register#cp-features-register', [
h('a', {
href: '/register/'
}, h('button.cp-features-register-button', Msg.features_f_register))
href: '/register/',
class: 'cp-features-register-button',
}, Msg.features_f_register)
]),
]),
]),
]);
var premiumFeatures =
h('div.col-12.col-sm-4.cp-anon-user',[
h('div.col-12.col-sm-4.cp-premium-user',[
h('div.card',[
h('div.title-card',[
h('h3.text-center',Msg.features_premium)

View File

@ -153,10 +153,10 @@ define([
if (Pages.areSubscriptionsAllowed() && !LocalStore.getPremium()) {
var sub = h('div.cp-sub-prompt', [
h('span', Msg.home_morestorage),
h('a', {href:"/accounts/"}, h('button', [
h('a', {href:"/accounts/", class:'subscribe-btn'}, [
h('i.fa.fa-ticket'),
Msg.features_f_subscribe
]))
])
]);
return sub;
} else {
@ -167,13 +167,20 @@ define([
let popup = h('div.cp-extensions-popups');
let utils = { h, Util, Hash };
Extensions.getExtensions('HOMEPAGE_POPUP').forEach(ext => {
if (typeof(ext.check) === "function" && !ext.check()) { return; }
ext.getContent(utils, content => {
$(popup).append(h('div.cp-extensions-popup', content));
Extensions.getExtensions('HOMEPAGE_POPUP').forEach(_ext => {
_ext.then(ext => {
if (ext) {
ext.getContent(utils, content => {
$(popup).append(h('div.cp-extensions-popup', content));
});
}
}).catch(error => {
console.error(error);
});
});
return [
h('div#cp-main', [
Pages.infopageTopbar(),

View File

@ -200,8 +200,8 @@
}
.cp-usergrid-user, textarea, a, .fa-times {
outline: none;
&:focus {
outline: @cryptpad_color_brand solid 2px;
&:focus-visible {
outline: @variables_focus_style;
}
}
}
@ -244,8 +244,8 @@
}
}
outline: none;
&:focus {
outline: @cryptpad_color_brand solid 2px;
&:focus-visible {
outline: @variables_focus_style;
}
}
span.alertify-tabs-active {
@ -275,7 +275,7 @@
.tools_placeholder-color();
outline: none;
&:focus-visible {
outline: @cryptpad_color_brand solid 2px;
outline: @variables_focus_style;
}
}

View File

@ -129,8 +129,8 @@
box-sizing: border-box;
}
outline: none;
&:focus {
outline: @cryptpad_color_brand solid 2px;
&:focus-visible {
outline: @variables_focus_style;
}
}
@ -217,8 +217,8 @@
height: var(--checkmark-dim1);
}
outline: none;
&:focus {
outline: @cryptpad_color_brand solid 2px;
&:focus-visible {
outline: @variables_focus_style;
}
}

View File

@ -69,7 +69,7 @@
outline: none;
width: 700px;
max-width: 90vw;
height: 500px;
//height: 500px;
border-radius: @variables_radius_L;
max-height: ~"calc(100vh - 20px)";
margin: 0px;
@ -85,6 +85,7 @@
}
.cp-creation-checkboxes {
min-width: 300px;
flex-flow: column;
align-items: baseline !important;
max-height: 150px;
@ -156,11 +157,10 @@
flex: 1 0 auto;
justify-content: space-around;
& > div {
width: 300px;
//width: 300px;
max-width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 16px;
//margin: 10px 0;
min-height: 28px;
@ -293,6 +293,11 @@
}
}
}
.cp-creation-password-warning {
font-size: 0.75em;
line-height: 120%;
margin: 0.4rem 1rem calc(0.4rem + 6px);
}
.cp-creation-settings {
button {
margin: 0;
@ -421,7 +426,6 @@
#cp-creation-form {
& > div {
width: 95%;
margin: 0 auto;
}
.cp-creation-expire {
&.active {
@ -431,7 +435,6 @@
.cp-creation-slider {
flex: none;
order: 10;
width: 100%;
}
}
}
@ -440,7 +443,7 @@
}
@media screen and (max-width: 800px) {
#cp-creation {
height: 550px;
//height: 550px;
#cp-creation-form {
div.cp-creation-template {
flex-flow: column;

View File

@ -30,6 +30,11 @@
.cp-unselectable {
.tools_unselectable();
}
.btn-primary{
&:focus-visible {
outline: @variables_focus_style;
}
}
/* local mixins */
@drive_icon-margin: 10px;
@ -329,11 +334,10 @@
overflow: hidden;
text-overflow: ellipsis;
.leftside-menu-category_main();
margin: 0;
display: flex;
align-items: center;
cursor: pointer;
margin-left: -5px;
margin: 0 0 2px -5px;
padding-left: 5px;
.fa, .cptools {
display: inline-block;
@ -348,6 +352,9 @@
overflow: hidden;
text-overflow: ellipsis;
}
&:focus-visible {
outline: @variables_focus_style;
}
}
}
}
@ -373,6 +380,9 @@
padding-left: 20px;
.leftside-menu-category_main();
margin: 0;
&:focus-visible {
outline: @variables_focus_style;
}
}
}
}
@ -399,6 +409,10 @@
position: relative;
top: -1px;
}
&:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius_S;
}
}
.cp-app-drive-tree-docs {
box-shadow: @cryptpad_ui_shadow;
@ -953,6 +967,9 @@
li, li .fa, li .cptools {
cursor: pointer;
border-radius: @variables_radius;
&:focus {
outline: @cryptpad_color_brand solid 2px;
}
}
&> p {
display: flex;

View File

@ -160,12 +160,12 @@
li[role="menuitem"] {
border-radius: @variables_radius;
white-space: nowrap;
&:hover {
background-color: @cp_dropdown-bg-hover !important;
}
&:focus-visible {
outline-color: @cp_dropdown-fg;
}
&:hover {
background-color: @cp_dropdown-bg-hover;
}
}
&> span {
box-sizing: border-box;

View File

@ -28,6 +28,9 @@
}
&.tui-full-calendar-content {
font-size: @colortheme_app-font-size;
&:focus-visible {
outline: @variables_focus_style;
}
}
&[readonly] {
//margin-top:1rem;
@ -138,8 +141,8 @@
color: @cryptpad_text_col;
}
outline: none;
&:focus {
outline: @cryptpad_color_brand solid 2px;
&:focus-visible {
outline: @variables_focus_style;
}
}
}
@ -148,8 +151,8 @@
background-color: @cp_buttons-cancel;
box-sizing: border-box;
align-items: center;
padding: 0 6px;
line-height: 36px;
line-height: 20px;
padding: 8px 6px;
white-space: nowrap;
text-align: center;
text-transform: uppercase;
@ -257,8 +260,8 @@
outline: none;
&:focus {
outline: @cryptpad_color_brand solid 2px;
&:focus-visible {
outline: @variables_focus_style;
}
&::-moz-focus-inner {
border: 0;

View File

@ -33,8 +33,8 @@
color: @cp_drive-fg;
}
&.cp-icons-element-selected {
background: @cp_drive-icon-hover;
color: @cp_drive-fg;
outline: @cryptpad_color_brand solid 2px;
}
.fa, .cptools {
display: block;

View File

@ -6,6 +6,7 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./font.less";
@import (reference) "./variables.less";
@infopages-radius: 5px;
@infopages-radius-L: 10px;
@ -46,6 +47,10 @@ body.html {
a:hover {
opacity: 1;
}
a:focus-visible {
outline: @variables_focus_style;
border-radius: @variables_radius;
}
border: 0;
padding: 0;
margin: 0;
@ -231,6 +236,9 @@ body.html {
border: 0px;
border-radius: @infopages-radius;
padding: 0.5em 0.7em;
&:focus-visible {
outline: @variables_focus_style;
}
}
}
.cp-footer-version {
@ -293,6 +301,9 @@ body.html {
&:hover {
color: @cryptpad_text_col;
}
&:focus-visible {
outline: @variables_focus_style;
}
i {
margin-right: 5px;
}

View File

@ -97,6 +97,26 @@
margin: 0;
}
}
.cp-dropdown-container .cp-dropdown-content {
li[role="menuitem"]{
.cp-notification-dismiss:hover {
border-radius: @variables_radius;
background-color: @cp_dropdown-bg-hover;
}
}
li[role="menuitem"].cp-notification-avatar {
&:hover {
background-color: transparent;
}
.cp-avatar:hover {
background-color: @cp_dropdown-bg-hover;
}
.cp-notification-content:hover {
border-radius: @variables_radius;
background-color: @cp_dropdown-bg-hover;
}
}
}
}

View File

@ -47,6 +47,9 @@
align-items: center;
.leftside-menu-category_main();
box-shadow: @cryptpad_ui_shadow;
&:focus-visible {
outline: @variables_focus_style;
}
}
}
&.cp-leftside-narrow {

View File

@ -81,8 +81,8 @@
padding-left: 4px;
vertical-align: middle;
outline: none;
&:focus {
outline: @cryptpad_color_brand solid 2px;
&:focus-visible {
outline: @variables_focus_style;
}
}
.close {

View File

@ -29,35 +29,6 @@
font: @colortheme_app-font;
}
@media screen and (max-width: 870px) {
flex-flow: column;
.cp-toolbar-history-actions {
width: 100%;
.cp-history-actions-first {
margin-right: 0 !important;
}
}
.cp-toolbar-history-timeline {
width: ~"calc(100% - 20px)";
margin-right: 10px !important;
}
}
@media screen and (max-height: 500px) {
padding-top: 0px;
.cp-history-timeline-line {
display: none !important;
}
.cp-toolbar-history-timeline {
width: 100% !important;
margin: 0 !important;
}
.cp-history-timeline-actions {
margin-left: 0 !important;
}
}
&.cp-history-init {
padding: 0;
height: 32px;
@ -102,8 +73,8 @@
text-transform: uppercase;
display: inline-flex;
align-items: center;
.fa:not(:last-child) {
margin-right: 5px;
i:not(:last-child) {
margin-right: 0.5rem;
}
&:hover {
background-color: fade(@cp_toolbar-fg, 30%);
@ -293,6 +264,39 @@
left: ~"calc(50% - 6px)";
}
}
@media screen and (max-width: 870px) {
flex-flow: column;
.cp-toolbar-history-actions {
width: 100%;
margin: 0.6rem 0;
height: auto;
.cp-history-actions-first {
margin-right: 0 !important;
}
button {
margin: 0.1rem;
}
}
.cp-toolbar-history-timeline {
width: ~"calc(100% - 20px)";
margin-right: 10px !important;
}
}
@media screen and (max-height: 500px) {
padding-top: 0px;
.cp-history-timeline-line {
display: none !important;
}
.cp-toolbar-history-timeline {
width: 100% !important;
margin: 0 !important;
}
.cp-history-timeline-actions {
margin-left: 0 !important;
}
}
}
}

View File

@ -77,8 +77,8 @@
&:hover {
background-color: contrast(@cp_toolbar-bg, darken(@cp_toolbar-bg, 5%), lighten(@cp_toolbar-bg, 5%));
}
&:focus {
outline: @cryptpad_color_brand solid 2px;
&:focus-visible {
outline: @variables_focus_style;
}
}
button:nth-of-type(1) {
@ -373,7 +373,7 @@
* {
outline-style: none;
&:focus-visible {
outline: @cryptpad_color_brand solid 2px;
outline: @variables_focus_style;
}
}
@ -676,6 +676,10 @@
overflow: hidden;
text-overflow: ellipsis;
border-collapse: collapse;
&:focus {
margin: 2px;
border-radius: @variables_radius;
}
}
input {
color: @cryptpad_text_col;
@ -730,10 +734,6 @@
color: inherit;
}
}
.cp-notifications-empty {
color: @cp_dropdown-fg;
padding: 5px;
}
button {
position: relative;
.cp-dropdown-button-title {
@ -761,6 +761,10 @@
margin: 0 -5px;
padding: 0;
}
.cp-notifications-empty {
color: @cp_dropdown-fg;
padding: 0.3em;
}
}
}
.cp-toolbar-link {
@ -983,11 +987,8 @@
height: @toolbar_line-height;
display: inline-flex;
align-items: center;
.fa, .cptools {
margin-right: 5px;
}
.cp-dropdown-button-title .cp-icon {
margin-left: 5px;
i ~ span {
margin-left: 0.5rem;
}
&:hover {
background-color: fade(@cp_toolbar-bottom-bg, 70%);
@ -1109,15 +1110,6 @@
}
}
.cp-toolbar-bottom-left {
.cp-toolbar-appmenu, .cp-toolbar-file {
button {
&::before, .fa {
min-width: 20px;
text-align: center;
margin-right: 0;
}
}
}
.cp-toolbar-file {
order: 1;
}

View File

@ -18,6 +18,8 @@
@variables_shadow: 0 8px 32px 0 @cp_shadow-color;
// Rounded corners
@variables_radius_S: 3px;
@variables_radius: 5px;
@variables_radius_L: 10px;
@variables_focus_style: @cryptpad_color_brand solid 2px;

View File

@ -15,12 +15,19 @@
padding: 20px;
}
.cp-features-register-button {
text-decoration: none;
text-align: center;
background-color: @cp_buttons-primary;
color: @cryptpad_text_col;
font-size: 20px;
color: @cryptpad_color_white;
background: @cryptpad_color_brand;
border-radius: 0;
&:hover {
cursor: pointer;
margin: 10px 0;
border-radius: @infopages-radius;
padding: 10px 20px;
&:hover, &:focus {
background-color: contrast(@cp_buttons-primary-text, darken(@cp_buttons-primary, 10%), lighten(@cp_buttons-primary, 10%));
}
&:visited {
color: @cryptpad_text_col;
}
}
.cp-features-web {
@ -114,5 +121,10 @@
margin-top: 3em;
}
}
.cp-premium-user {
@media (max-width:575px) {
margin-top: 3em;
}
}
}

View File

@ -255,9 +255,10 @@
* {
display: block;
}
button {
background-color: @cryptpad_color_link;
color: @cryptpad_text_col_inv;
.subscribe-btn {
text-align: center;
background-color: @cp_buttons-primary;
color: @cryptpad_text_col;
font-size: 1.4rem;
margin: 10px 0;
border-radius: @infopages-radius-L;
@ -267,8 +268,7 @@
margin-right: 10px;
}
&:hover, &:focus {
background-color: @cp_static-card-bg;
color: @cryptpad_text_col;
background-color: contrast(@cp_buttons-primary-text, darken(@cp_buttons-primary, 10%), lighten(@cp_buttons-primary, 10%));
}
}
}

View File

@ -43,13 +43,10 @@
nav {
display: flex;
align-items: center;
justify-content: flex-end;
}
@media screen and (max-width: 600px) {
nav .btn-danger {
line-height: inherit;
align-items: flex-end;
.btn-confirm {
white-space: normal;
}
}

View File

@ -68,13 +68,10 @@
nav {
display: flex;
align-items: center;
justify-content: flex-end;
}
@media screen and (max-width: 600px) {
nav .btn-danger {
line-height: inherit;
align-items: flex-end;
.btn-confirm {
white-space: normal;
}
}

View File

@ -8,3 +8,8 @@
margin: 3cm;
size: A4 portrait;
}
@media print {
body {
background: white !important;
}
}

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body class="html">
<noscript></noscript>

View File

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.ar.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

View File

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.es_CU.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

View File

@ -5,7 +5,7 @@
---
services:
cryptpad:
image: "cryptpad/cryptpad:version-2024.6.0"
image: "cryptpad/cryptpad:version-2024.12.0"
hostname: cryptpad
environment:

View File

@ -22,9 +22,9 @@ if [ ! -f "$CPAD_CONF" ]; then
eg: docker run -v /path/to/config.js:/cryptpad/config/config.js \n\
#################################################################### \n"
cp "$CPAD_HOME"/config/config.example.js "$CPAD_CONF"
cp "$CPAD_HOME"/config/config.example.js "$CPAD_CONF"
sed -i -e "s@\(httpUnsafeOrigin:\).*[^,]@\1 '$CPAD_MAIN_DOMAIN'@" \
sed -i -e "s@\(httpUnsafeOrigin:\).*[^,]@\1 '$CPAD_MAIN_DOMAIN'@" \
-e "s@\(^ *\).*\(httpSafeOrigin:\).*[^,]@\1\2 '$CPAD_SANDBOX_DOMAIN'@" "$CPAD_CONF"
fi

10
docs/community/README.md Normal file
View File

@ -0,0 +1,10 @@
<!--
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# Community configuration files
This folder is listing community-contributed configuration files. They **aren't** supported and are provided as-is, without any warranty.
If you are using CryptPad in production and require professional support please contact sales@cryptpad.org

View File

@ -0,0 +1,245 @@
# SPDX-FileCopyrightText: 2024 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 Caddy can be configured
# to work with CryptPad. This example WILL NOT WORK AS IS. For best results,
# compare the sections of this configuration file against a working CryptPad
# installation (http server by the Nodejs process). If you are using CryptPad
# in production and require professional support please contact sales@cryptpad.org
(trustedProxies) {
# Force Caddy to accept `X-Forwarded-For` and other origin headers.
# Modify the line below if you want to restrict the scope of direct downstream sending these headers.
trusted_proxies 0.0.0.0/0 ::/0
}
# Caddy does not have variables for server names, so domains need to be hardcoded.
# You can bulk replace "your-main-domain.com" and "your-sandbox-domain.com" safely.
your-main-domain.com:443,
your-sandbox-domain.com:443 {
# Define your certificates below.
# No need to adjust TLS configurations, as the defaults in Caddy are already secure.
tls /path/to/fullchain/publicKey.pem /path/to/certificate/privateKey.pem
# Enable HSTS.
# Do not enable this line when configuring over mixnet, e.g. Tor.
header Strict-Transport-Security "max-age=63072000; includeSubDomains"
# Security headers
header X-XSS-Protection "1; mode=block"
header X-Content-Type-Options "nosniff"
header Access-Control-Allow-Credentials "true"
#header X-Frame-Options "SAMEORIGIN"
# OnlyOffice fonts may be loaded from both domains.
@onlyOfficeFonts {
path_regexp "^\\/common\\/onlyoffice\\/.*\\/fonts\\/.*$"
}
header Access-Control-Allow-Origin "*"
# By default CryptPad forbids remote domains from embedding CryptPad documents in iframes.
# The sandbox domain must always be permitted in order for the platform to function.
# If you wish to enable remote embedding you may change the value below to "*"
# as per the commented value.
header ?Access-Control-Allow-Origin "https://your-sandbox-domain.com"
#header ?Access-Control-Allow-Origin "*"
# Opt out of Google's FLoC Network
header Permissions-Policy "interest-cohort=()"
# Enable SharedArrayBuffer in Firefox (for .xlsx export)
header ?Cross-Origin-Resource-Policy "cross-origin"
header ?Cross-Origin-Embedder-Policy "require-corp"
# Specify the relative path to root of your custom error page.
# This error page won't only be served for 404 errors.
handle_errors {
rewrite * /error.htm
header Cache-Control "no-cache, no-store"
file_server
templates
}
# Insert the path to your CryptPad repository root here
root /home/cryptpad/cryptpad
# Any static assets loaded with "vers=" in their URL will be cached for a year
@staticAssets {
query "ver=*"
}
header @staticAssets Cache-Control "max-age=31536000"
vars {
# CSS can be dynamically set inline, loaded from the same domain, or from your main domain.
styleSrc "'unsafe-inline' 'self' https://your-main-domain.com"
# connect-src restricts URLs which can be loaded using script interfaces.
# If you have configured your instance to use a dedicated file delivery domain or API domain,
# you will need to add them below.
connectSrc "'self' https://your-main-domain.com blob: wss://api.your-main-domain.com https://your-sandbox-domain.com"
# Fonts can be loaded from data-URLs or the main domain.
fontSrc "'self' data: https://your-main-domain.com"
# Images can be loaded from anywhere, though we'd like to deprecate this as it allows
# the use of images for tracking.
imgSrc "'self' data: blob: https://your-main-domain.com"
# frame-src specifies valid sources for nested browsing contexts.
# This prevents loading any iframes from anywhere other than the sandbox domain.
frameSrc "'self' https://your-sandbox-domain.com blob:"
# media-src specifies valid sources for loading media using video or audio.
mediaSrc "blob:"
# child-src defines valid sources for webworkers and nested browser contexts.
# It is deprecated in favour of worker-src and frame-src.
childSrc "https://your-main-domain.com"
# worker-src valid sources for Worker, Shared Worker, or Service Worker scripts.
# Supercedes child-src, but is unfortunately not yet universally supported.
workerSrc "'self'"
# script-src specifies valid sources for JavaScript, including inline handlers.
scriptSrc "'self' resource: https://your-main-domain.com"
# frame-ancestors specifies which origins can embed your CryptPad instance.
# This must include 'self' and your main domain (over HTTPS) in order for CryptPad to work,
# if you have enabled remote embedding via the admin panel, then this must be more permissive.
# Note: cryptpad.fr permits web pages served via https: and vector: (element desktop app)
frameAncestors "'self' https://your-main-domain.com"
#frameAncestors "'self' https: vector:"
# A few assets are loaded via the sandbox domain.
# They unfortunately still require exceptions to the sandboxing to work correctly.
# Everything except the sandbox domain is a privileged scope, as they might be used to handle keys.
# Unsafe iframes are exceptions. Office file formats are converted outside of the sandboxed scope,
# because of bugs in Chromium-based browsers that incorrectly ignore headers supposed to enable
# the use of some modern APIs, that are required when JavaScript is run in a cross-origin context.
# We've applied other sandboxing techniques to mitigate the risk of running WebAssembly
# in this privileged scope.
# Privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied.
scriptSrcUnsafe "'self' 'unsafe-eval' 'unsafe-inline' resource: https://your-main-domain.com"
}
# Finally, set all the security rules you have composed above.
@privilegedScope1 {
host "your-sandbox-domain.com"
path_regexp "^\\/(sheet|doc|presentation)\\/inner.html.*$"
}
@privilegedScope2 {
host "your-sandbox-domain.com"
path_regexp "^\\/common\\/onlyoffice\\/.*\\/.*\\.html.*$"
}
@privilegedScope3 {
host "your-sandbox-domain.com"
path_regexp "^\\/unsafeiframe\\/inner\\.html.*$"
}
header @privilegedScope1 Content-Security-Policy "default-src 'none'; child-src {vars.childSrc}; worker-src {vars.workerSrc}; media-src {vars.mediaSrc}; style-src {vars.styleSrc}; script-src {vars.scriptSrcUnsafe}; connect-src {vars.connectSrc}; font-src {vars.fontSrc}; img-src {vars.imgSrc}; frame-src {vars.frameSrc}; frame-ancestors {vars.frameAncestors}"
header @privilegedScope2 Content-Security-Policy "default-src 'none'; child-src {vars.childSrc}; worker-src {vars.workerSrc}; media-src {vars.mediaSrc}; style-src {vars.styleSrc}; script-src {vars.scriptSrcUnsafe}; connect-src {vars.connectSrc}; font-src {vars.fontSrc}; img-src {vars.imgSrc}; frame-src {vars.frameSrc}; frame-ancestors {vars.frameAncestors}"
header @privilegedScope3 Content-Security-Policy "default-src 'none'; child-src {vars.childSrc}; worker-src {vars.workerSrc}; media-src {vars.mediaSrc}; style-src {vars.styleSrc}; script-src {vars.scriptSrcUnsafe}; connect-src {vars.connectSrc}; font-src {vars.fontSrc}; img-src {vars.imgSrc}; frame-src {vars.frameSrc}; frame-ancestors {vars.frameAncestors}"
header ?Content-Security-Policy "default-src 'none'; child-src {vars.childSrc}; worker-src {vars.workerSrc}; media-src {vars.mediaSrc}; style-src {vars.styleSrc}; script-src {vars.scriptSrc}; connect-src {vars.connectSrc}; font-src {vars.fontSrc}; img-src {vars.imgSrc}; frame-src {vars.frameSrc}; frame-ancestors {vars.frameAncestors}"
# Add support for .mjs files used by pdfjs
@fileModuleJS {
path "*.mjs"
}
header @fileModuleJS Content-Type "application/javascript"
# The Node.js process can handle all traffic, whether accessed over websocket or as static assets.
# We prefer to serve static content from Caddy directly, and to leave the API server to handle the
# the dynamic content that only it can manage. This is primarily for optimization.
handle /cryptpad_websocket/* {
reverse_proxy * {
to 127.0.0.1:3003
header_up Host "{host}"
header_up X-Real-IP "{remote_host}"
# Caddy supports WebSockets directly. No additional headers are needed.
import trustedProxies
}
}
handle_path /customize.dist/* {
# This is needed in order to prevent infinite recursion between /customize/ and the root.
}
# Try to load customizeable content via /customize/ and fall back to the default content located
# at /customize.dist/ .
# This is what allows you to override behaviour.
handle_path /customize/* {
try_files /customize/{path} /customize.dist/{path}
file_server {
index index.html index.htm default.html default.htm
}
}
# /api/config is loaded once per page load, and is used to retrieve the caching variable,
# which is applied to every other resource loaded during that session.
@sharedReverseProxy {
path /api/*
path /extensions.js
}
handle @sharedReverseProxy {
reverse_proxy * {
to 127.0.0.1:3000
header_up Host "{host}"
header_up X-Real-IP "{remote_host}"
# These settings prevent both Caddy and the API server from setting duplicate headers.
header_down Cross-Origin-Resource-Policy cross-origin
header_down Cross-Origin-Embedder-Policy require-corp
import trustedProxies
}
}
# Requests for blobs and blocks are now proxied to the API server.
# This simplifies Caddy path configuration, in the event they are being hosted in a non-standard location
# or with odd unexpected permissions. Serving blobs in this manner also means that it will be possible to
# enforce access control for them, though this is not yet implemented.
# Access control (via TOTP 2FA) has been added to blocks, so they can be handled with the same directives.
@blobsAndBlocks {
path /blob/*
path /block/*
}
handle @blobsAndBlocks {
@corsPreflight {
method OPTIONS
}
handle @corsPreflight {
header Access-Control-Allow-Origin "https://your-sandbox-domain.com"
header Access-Control-Allow-Credentials "true"
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range"
header Access-Control-Max-Age "1728000"
header Content-Type "application/octet-stream; charset=utf-8"
header Content-Length "0"
respond 204
}
reverse_proxy * {
to 127.0.0.1:3000
# Preventing these headers from getting duplicated, since we are proxying to the API server.
header_down -X-Content-Type-Options
header_down -Access-Control-Allow-Origin
header_down -Permissions-Policy
header_down -X-XSS-Protection
header_down -Cross-Origin-Resource-Policy
header_down -Cross-Origin-Embedder-Policy
}
}
# The Node.JS server has some built-in forwarding rulesets to prevent URLs not suffixed with a slash
# from resulting in a 404 error. This simply adds a trailing slash to a variety of applications.
@preventNotFound {
path_regexp "^/(register|login|recovery|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc|form|report|convert|checkup|diagram)$"
}
redir @preventNotFound "{path}/"
# Enable file serving
file_server {
index index.html index.htm default.html default.htm
}
}

View File

@ -0,0 +1,64 @@
# SPDX-FileCopyrightText: 2024 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 Caddy can be configured
# to work with CryptPad. This example WILL NOT WORK AS IS. For best results,
# compare the sections of this configuration file against a working CryptPad
# installation (http server by the Nodejs process). If you are using CryptPad
# in production and require professional support please contact sales@cryptpad.org
(trustedProxies) {
# Force Caddy to accept `X-Forwarded-For` and other origin headers.
# Modify the line below if you want to restrict the scope of direct downstream sending these headers.
trusted_proxies 0.0.0.0/0 ::/0
}
# Caddy does not have variables for server names, so domains need to be hardcoded.
# You can bulk replace "your-main-domain.com" and "your-sandbox-domain.com" safely.
your-main-domain.com:443,
your-sandbox-domain.com:443 {
# Define your certificates below.
# No need to adjust TLS configurations, as the defaults in Caddy are already secure.
tls /path/to/fullchain/publicKey.pem /path/to/certificate/privateKey.pem
# Enable HSTS.
# Do not enable this line when configuring over mixnet, e.g. Tor.
header Strict-Transport-Security "max-age=63072000; includeSubDomains"
# Specify the relative path to root of your custom error page.
# This error page won't only be served for 404 errors.
handle_errors {
rewrite * /error.htm
header Cache-Control "no-cache, no-store"
file_server
templates
}
# The Node.js process can handle all traffic, whether accessed over websocket or as static assets.
reverse_proxy /cryptpad_websocket/* {
to 127.0.0.1:3003
header_up Host "{host}"
header_up X-Real-IP "{remote_host}"
# Caddy supports WebSockets directly. No additional headers are needed.
import trustedProxies
}
reverse_proxy * {
to 127.0.0.1:3000
header_up Host "{host}"
header_up X-Real-IP "{remote_host}"
# These settings prevent both Caddy and the API server from setting duplicate headers.
header_down Cross-Origin-Resource-Policy cross-origin
header_down Cross-Origin-Embedder-Policy require-corp
import trustedProxies
}
# Enable file serving
file_server {
index index.html index.htm default.html default.htm
}
}

View File

@ -4,7 +4,7 @@
# 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
# and require professional support please contact sales@cryptpad.org
# This configuration requires mod_ssl, mod_socache_shmcb, mod_proxy,
# mod_proxy_http and mod_headers

View File

@ -8,6 +8,21 @@
# installation (http server by the Nodejs process). If you are using CryptPad
# in production and require professional support please contact sales@cryptpad.fr
server {
listen 80;
listen [::]:80;
server_name your-main-domain.com your-sandbox-domain.com;
access_log /dev/null;
error_log /dev/null emerg;
# Let's Encrypt webroot
include letsencrypt-webroot;
# Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
@ -85,7 +100,7 @@ server {
resolver 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 9.9.9.9 149.112.112.112 208.67.222.222 208.67.220.220;
# OnlyOffice fonts may be loaded from both domains
if ($uri ~ ^\/common\/onlyoffice\/.*\/fonts\/.*$) { set $allowed_origins "*"; }
if ($uri ~ ^/common/onlyoffice/.*/fonts/) { set $allowed_origins "*"; }
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
@ -113,7 +128,7 @@ server {
# We had inverted them as an optimization, but Safari 16 introduced a bug that interpreted
# some important headers incorrectly when loading these files from cache.
# This is why we can't have nice things :(
if ($uri ~ ^(\/|.*\/|.*\.html)$) {
if ($uri ~ ^(?:/|.*/|.*\.html)$) {
set $cacheControl no-cache;
}
@ -162,8 +177,8 @@ server {
set $unsafe 0;
# the following assets are loaded via the sandbox domain
# they unfortunately still require exceptions to the sandboxing to work correctly.
if ($uri ~ ^\/(sheet|doc|presentation)\/inner.html.*$) { set $unsafe 1; }
if ($uri ~ ^\/common\/onlyoffice\/.*\/.*\.html.*$) { set $unsafe 1; }
if ($uri ~ ^/(?:sheet|doc|presentation)/inner.html) { set $unsafe 1; }
if ($uri ~ ^/common/onlyoffice/.*/.*\.html) { set $unsafe 1; }
# everything except the sandbox domain is a privileged scope, as they might be used to handle keys
if ($host != $sandbox_domain) { set $unsafe 0; }
@ -171,7 +186,7 @@ server {
# because of bugs in Chromium-based browsers that incorrectly ignore headers that are supposed to enable
# the use of some modern APIs that we require when javascript is run in a cross-origin context.
# 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; }
if ($uri ~ ^/unsafeiframe/inner\.html) { set $unsafe 1; }
# privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied
if ($unsafe) {
@ -220,7 +235,7 @@ server {
# /api/config is loaded once per page load and is used to retrieve
# the caching variable which is applied to every other resource
# which is loaded during that session.
location ~ ^/api/.*$ {
location ^~ /api/ {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
@ -234,7 +249,7 @@ server {
add_header Cross-Origin-Embedder-Policy require-corp;
}
location ~ ^/extensions.js {
location = /extensions.js/ {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
@ -253,7 +268,7 @@ server {
# or with odd unexpected permissions. Serving blobs in this manner also means that it will be possible to
# enforce access control for them, though this is not yet implemented.
# Access control (via TOTP 2FA) has been added to blocks, so they can be handled with the same directives.
location ~ ^/(blob|block)/.*$ {
location ~ ^/(?:blob|block)/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' "${allowed_origins}";
add_header 'Access-Control-Allow-Credentials' true;
@ -278,8 +293,8 @@ server {
# The nodejs server has some built-in forwarding rules to prevent
# URLs like /pad from resulting in a 404. This simply adds a trailing slash
# to a variety of applications.
location ~ ^/(register|login|recovery|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc|form|report|convert|checkup|diagram)$ {
rewrite ^(.*)$ $1/ redirect;
location ~ ^/(?:register|login|recovery|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams|calendar|presentation|doc|form|report|convert|checkup|diagram)$ {
return 301 https://$host$uri/;
}
# Finally, serve anything the above exceptions don't govern.

View File

@ -8,6 +8,21 @@
# installation (http server by the Nodejs process). If you are using CryptPad
# in production and require professional support please contact sales@cryptpad.fr
server {
listen 80;
listen [::]:80;
server_name your-main-domain.com your-sandbox-domain.com;
access_log /dev/null;
error_log /dev/null emerg;
# Let's Encrypt webroot
include letsencrypt-webroot;
# Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;

93
eslint.config.js Normal file
View File

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2024 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const compatPlugin = require("eslint-plugin-compat");
const globals = require("globals");
const js = require("@eslint/js");
const FlatCompat = require("@eslint/eslintrc").FlatCompat;
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
module.exports = [{
ignores: [
"**/node_modules/",
"www/components/",
"www/bower_components/",
"www/common/onlyoffice/dist",
"www/common/onlyoffice/x2t",
"**/onlyoffice-dist/",
"www/scratch",
"www/accounts",
"www/lib",
"www/accounts",
"www/worker",
"www/todo",
"**/*worker.bundle.js",
"**/_build",
"www/common/hyperscript.js",
"www/pad/wysiwygarea-plugin.js",
"www/pad/mediatag-plugin.js",
"www/pad/mediatag-plugin-dialog.js",
"www/pad/disable-base64.js",
"www/pad/wordcount/",
"www/kanban/jkanban.js",
"www/common/jscolor.js",
"www/common/media-tag-nacl.min.js",
"**/customize/",
"www/debug/chainpad.dist.js",
"www/pad/mathjax/",
"www/code/mermaid*.js",
"www/code/orgmode.js",
],
}, ...compat.extends("eslint:recommended", "plugin:compat/recommended"), {
plugins: {
compatPlugin,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
define: "readonly",
},
ecmaVersion: "latest",
sourceType: "commonjs",
},
rules: {
indent: ["off", 4],
"linebreak-style": ["off", "unix"],
quotes: ["off", "single"],
semi: ["error", "always"],
"no-irregular-whitespace": ["off"],
"no-self-assign": ["off"],
"no-empty": ["off"],
"no-useless-escape": ["off"],
"no-extra-boolean-cast": ["off"],
"no-prototype-builtins": ["off"],
"no-use-before-define": ["error"],
"no-unused-vars": [
"error",
{
caughtErrors: "none"
}
]
},
}, {
files: ["**/.eslintrc.{js,cjs}"],
languageOptions: {
globals: {
...globals.node,
},
ecmaVersion: 5,
sourceType: "commonjs",
},
}];

View File

@ -33,7 +33,7 @@ main() {
install_version v4 6ebc6938
install_version v5 88a356f0
install_version v6 abd8a309
install_version v7 2792e6e1
install_version v7 e1267803
install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1
rm -rf "$BUILDS_DIR"

View File

@ -108,7 +108,7 @@ nThen(function (w) {
};
// spawn ws server and attach netflux event handlers
let Server = NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer}))
let Server = Env.Server = NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer}))
.on('channelClose', historyKeeper.channelClose)
.on('channelMessage', historyKeeper.channelMessage)
.on('channelOpen', historyKeeper.channelOpen)
@ -190,6 +190,9 @@ nThen(function (w) {
removed++;
}
});
if (Env.store) {
Env.store.closeInactiveChannels(active);
}
if (removed) {
Env.Log.info("CLEANED_ACTIVE_CHANNELS_MAP", {removed});
}

View File

@ -104,10 +104,8 @@ var shutdown = function (Env, Server, cb) {
var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) {
Env.batchRegisteredUsers('', cb, function (done) {
var dir = Env.paths.pin;
var dirB = Env.paths.block;
var folders, foldersB;
var folders;
var users = 0;
var blocks = 0;
nThen(function (waitFor) {
Fs.readdir(dir, waitFor(function (err, list) {
if (err) {
@ -116,13 +114,6 @@ var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) {
}
folders = list;
}));
Fs.readdir(dirB, waitFor(function (err, list) {
if (err) {
waitFor.abort();
return void done(err);
}
foldersB = list;
}));
}).nThen(function (waitFor) {
folders.forEach(function (f) {
var dir = Env.paths.pin + '/' + f;
@ -135,20 +126,8 @@ var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) {
users += list.length;
}));
});
}).nThen(function (waitFor) {
foldersB.forEach(function (f) {
var dir = Env.paths.block + '/' + f;
Fs.readdir(dir, waitFor(function (err, list) {
if (err) { return; }
// Don't count placeholders
list = list.filter(name => {
return !/\.placeholder$/.test(name);
});
blocks += list.length;
}));
});
}).nThen(function () {
done(void 0, {users, blocks});
done(void 0, {users});
});
});
};

View File

@ -161,13 +161,11 @@ var queryAccountServer = function (Env, cb) {
nThen(waitFor => {
Admin.getRegisteredUsers(Env, null, waitFor((err, data) => {
if (err) { return; }
stats.registered = data.blocks;
stats.registered = data.users;
if (Env.lastPingRegisteredUsers) {
stats.usersDiff = stats.registered - Env.lastPingRegisteredUsers;
}
Env.lastPingRegisteredUsers = stats.registered;
let teams = (data.users - data.blocks);
if (teams > 0) { stats.teams = teams; }
}));
}).nThen(() => {
if (Env.maxConcurrentWs) {

28
lib/crypto.js Normal file
View File

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2024 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Nacl = require('tweetnacl/nacl-fast');
const CPCrypto = module.exports;
const plugins = require('./plugin-manager');
CPCrypto.init = (cb) => {
const crypto = {};
crypto.open = (signedMsg, validateKey) => {
return Nacl.sign.open(signedMsg, validateKey);
};
crypto.detachedVerify = (signedBuffer, signatureBuffer, validateKey) => {
return Nacl.sign.detached.verify(signedBuffer, signatureBuffer, validateKey);
};
if (plugins.SODIUM && plugins.SODIUM.crypto) {
let c = plugins.SODIUM.crypto;
if (c.open) { crypto.open = c.open; }
if (c.detachedVerify) { crypto.detachedVerify = c.detachedVerify; }
}
// Make async because we might need it later with libsodium's promise
// libsodium.ready.then(() => {});
setTimeout(() => {
cb(void 0, crypto);
});
};

View File

@ -669,6 +669,7 @@ module.exports = function (Env, cb) {
if (item.mtime > inactiveTime) { return void next(); }
nThen(function (w) {
blobs.size(item.blobId, w(function (err, size) {
if (err && err === 'ENOENT') { return; } // XXX delete the proof
if (err) {
w.abort();
return void Log.error("EVICT_BLOB_LIST_PROOFS_ERROR", err, next);

View File

@ -146,6 +146,7 @@ const dropChannel = HK.dropChannel = function (Env, chanName) {
expireChannel(Env, chanName);
}, TEMPORARY_CHANNEL_LIFETIME);
}
if (Env.store) { Env.store.closeChannel(chanName, function () {}); }
};
/* checkExpired
@ -320,6 +321,7 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash, tim
// Store the message first, and update the index only once it's stored.
// store.messageBin can be async so updating the index first may
// result in a wrong cpIndex
Env.plugins?.MONITORING?.increment(`storeMessage`);
nThen((waitFor) => {
Env.store.messageBin(id, msgBin, waitFor(function (err) {
if (err) {
@ -391,6 +393,8 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash, tim
var msgLength = msgBin.length;
index.size += msgLength;
Env.plugins?.MONITORING?.increment('broadcastMessage', (channel.length-1));
// handle the next element in the queue
next();
@ -521,6 +525,7 @@ const getHistoryAsync = (Env, channelName, lastKnownHash, beforeHash, handler, c
const store = Env.store;
let offset = -1;
Env.plugins?.MONITORING?.increment(`getHistoryAsync`);
nThen((waitFor) => {
getHistoryOffset(Env, channelName, lastKnownHash, waitFor((err, os) => {
if (err) {
@ -772,6 +777,10 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) {
Env.getOlderHistory(channelName, oldestKnownHash, untilHash, desiredMessages, desiredCheckpoint, function (err, toSend) {
if (err && err.code !== 'ENOENT') {
Env.Log.error("HK_GET_OLDER_HISTORY", err);
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId,
JSON.stringify(['HISTORY_RANGE_ERROR', txid, err])
]);
return;
}
if (Array.isArray(toSend)) {

View File

@ -35,7 +35,7 @@ const guid = () => {
return Util.guid(response._pending);
};
const sendMessage = (msg, cb, opt) => {
const sendMessage = Env.sendMessage = (msg, cb, opt) => {
var txid = guid();
var timeout = (opt && opt.timeout) || DEFAULT_QUERY_TIMEOUT;
var obj = {
@ -71,6 +71,7 @@ EVENTS.ENV_UPDATE = function (data /*, cb */) {
Env = JSON.parse(data);
Env.Log = Log;
Env.plugins = plugins;
Env.sendMessage = sendMessage;
Env.incrementBytesWritten = function () {};
} catch (err) {
Log.error('HTTP_WORKER_ENV_UPDATE', Util.serializeError(err));
@ -91,6 +92,23 @@ EVENTS.FLUSH_CACHE = function (data) {
});
};
Object.keys(plugins || {}).forEach(name => {
let plugin = plugins[name];
if (!plugin.addHttpEvents) { return; }
try {
let events = plugin.addHttpEvents(Env);
Object.keys(events || {}).forEach(cmd => {
// Uppercase event name?
if (cmd !== cmd.toUpperCase()) { return; }
// Event is a function?
if (typeof(events[cmd]) !== "function") { return; }
// Event doesn't already exists?
if (EVENTS[cmd]) { return; }
EVENTS[cmd] = events[cmd];
});
} catch (e) {}
});
process.on('message', msg => {
if (!(msg && msg.txid)) { return; }
if (msg.type === 'REPLY') {
@ -799,7 +817,12 @@ nThen(function (w) {
}));
}).nThen(function () {
// TODO inform the parent process that this worker is ready
Object.keys(Env.plugins || {}).forEach(name => {
let plugin = plugins[name];
if (!plugin.initialize) { return; }
try { plugin.initialize(Env, "http-worker"); }
catch (e) {}
});
});
process.on('uncaughtException', function (err) {

View File

@ -34,6 +34,8 @@ var isUnauthenticateMessage = function (msg) {
var handleUnauthenticatedMessage = function (Env, msg, respond, Server, netfluxId) {
Env.Log.silly('LOG_RPC', msg[0]);
Env.plugins?.MONITORING?.increment(`rpc_${msg[0]}`);
var method = UNAUTHENTICATED_CALLS[msg[0]];
method(Env, msg[1], function (err, value) {
if (err) {
@ -170,6 +172,8 @@ var rpc = function (Env, Server, userId, data, respond) {
var command = msg[1];
Env.plugins?.MONITORING?.increment(`rpc_${command}`);
if (command === 'UPLOAD') {
// UPLOAD is a special case that skips signature validation
// intentional fallthrough behaviour

View File

@ -286,6 +286,16 @@ var closeChannel = function (env, channelName, cb) {
}
};
var closeInactiveChannels = function (env, schedule, active) {
Object.keys(env.channels).forEach(channelName => {
if (!active.includes(channelName)) {
schedule.ordered(channelName, function (next) {
closeChannel(env, channelName, next);
});
}
});
};
var clearOffset = function (env, channelId, cb) {
var path = mkOffsetPath(env, channelId);
// we should always be able to recover from invalid offsets, so failure to delete them
@ -429,7 +439,27 @@ How to proceed
*/
let requiresChannel = true;
let all = [];
nThen(function (w) {
let first = true;
getDedicatedMetadata(env, channelId, (err, line) => {
if (first && !err) {
if (!Array.isArray(line)) {
requiresChannel = false;
}
first = false;
}
all.push({err, line});
}, w(function (err) {
if (err) {
// stream errors?
w.abort();
return void cb(err);
}
}));
}).nThen(function (w) {
if (!requiresChannel) { return; }
// returns the first line of a channel, parsed...
getChannelMetadata(env, channelId, w(function (err, data) {
if (err) {
@ -445,13 +475,10 @@ How to proceed
handler(null, data);
}));
}).nThen(function () {
getDedicatedMetadata(env, channelId, handler, function (err) {
if (err) {
// stream errors?
return void cb(err);
}
cb();
all.forEach(({err, line}) => {
handler(err, line);
});
cb();
});
};
@ -1445,6 +1472,9 @@ module.exports.create = function (conf, _cb) {
closeChannel(env, channelName, Util.both(cb, next));
});
},
closeInactiveChannels: function (active) {
closeInactiveChannels(env, schedule, active);
},
// write to a log file
log: function (channelName, content, cb) {
// you probably want the events in your log to be in the correct order.

View File

@ -16,11 +16,19 @@ const Logger = require("../log");
const Tasks = require("../storage/tasks");
const Nacl = require('tweetnacl/nacl-fast');
const Eviction = require("../eviction");
const CPCrypto = require('../crypto');
const plugins = require("../plugin-manager");
const Env = {
Log: {},
};
const Monitoring = plugins && plugins.MONITORING;
const monitoringIncrement = key => {
if (!Monitoring || !Monitoring.increment) { return; }
Monitoring.increment(key);
};
// support the usual log API but pass it to the main process
Logger.levels.forEach(function (level) {
Env.Log[level] = function (label, info) {
@ -32,6 +40,8 @@ Logger.levels.forEach(function (level) {
};
});
const HISTORY_SIZE_LIMIT = 1024 * 1024 * 1024; // XXX 1GB
var DETAIL = 1000;
var round = function (n) {
return Math.floor(n * DETAIL) / DETAIL;
@ -56,6 +66,17 @@ const init = function (config, _cb) {
Env.archiveRetentionTime = config.archiveRetentionTime;
Env.accountRetentionTime = config.accountRetentionTime;
Env.sendMessage = data => {
process.send(data);
};
Object.keys(plugins || {}).forEach(name => {
let plugin = plugins[name];
if (!plugin.initialize) { return; }
try { plugin.initialize(Env, "db-worker"); }
catch (e) {}
});
nThen(function (w) {
Store.create(config, w(function (err, _store) {
if (err) {
@ -101,6 +122,10 @@ const init = function (config, _cb) {
}
Env.tasks = tasks;
}));
}).nThen(function (w) {
CPCrypto.init(w(function (err, crypto) {
Env.crypto = crypto;
}));
}).nThen(function () {
cb();
});
@ -272,6 +297,8 @@ const computeIndex = function (data, cb) {
const channelName = data.channel;
const CB = Util.once(cb);
monitoringIncrement('computeIndex');
var start = 0;
nThen(function (w) {
store.getOffset(channelName, w(function (err, obj) {
@ -293,6 +320,7 @@ const computeIndex = function (data, cb) {
});
}
w.abort();
monitoringIncrement('computeIndexFromOffset');
CB(err, index);
}));
}).nThen(function (w) {
@ -301,6 +329,7 @@ const computeIndex = function (data, cb) {
store.clearOffset(channelName, w());
}).nThen(function () {
// now get the history as though it were the first time
monitoringIncrement('computeIndexFromStart');
computeIndexFromOffset(channelName, 0, CB);
});
};
@ -308,6 +337,7 @@ const computeIndex = function (data, cb) {
const computeMetadata = function (data, cb) {
const ref = {};
const lineHandler = Meta.createLineHandler(ref, Env.Log.error);
monitoringIncrement('computeMetadata');
return void store.readChannelMetadata(data.channel, lineHandler, function (err) {
if (err) {
// stream errors?
@ -317,6 +347,31 @@ const computeMetadata = function (data, cb) {
});
};
const _getFileSize = function (channel, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length === HK.STANDARD_CHANNEL_LENGTH ||
channel.length === HK.ADMIN_CHANNEL_LENGTH) {
return void store.getChannelSize(channel, function (e, size) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, 0); }
return void cb(e.code);
}
cb(void 0, size);
});
}
// 'channel' refers to a file, so you need another API
blobStore.size(channel, function (e, size) {
if (typeof(size) === 'undefined') { return void cb(e); }
cb(void 0, size);
});
};
const getFileSize = function (data, cb) {
_getFileSize(data.channel, cb);
};
/* getOlderHistory
* allows clients to query for all messages until a known hash is read
* stores all messages in history as they are read
@ -334,6 +389,7 @@ const getOlderHistory = function (data, cb) {
const desiredMessages = data.desiredMessages;
const desiredCheckpoint = data.desiredCheckpoint;
var next = () => {
var messages = [];
var found = false;
store.getMessages(channelName, function (msgStr) {
@ -379,6 +435,15 @@ const getOlderHistory = function (data, cb) {
}
cb(err, toSend);
});
};
_getFileSize(channelName, function (err, size) {
if (err) { return void cb(err); }
if (size > HISTORY_SIZE_LIMIT) {
return void cb('HISTORY_TOO_LARGE');
}
next();
});
};
const getPinState = function (data, cb) {
@ -388,6 +453,7 @@ const getPinState = function (data, cb) {
var lineHandler = Pins.createLineHandler(ref, Env.Log.error);
// if channels aren't in memory. load them from disk
monitoringIncrement('getPin');
pinStore.readMessagesBin(safeKey, 0, (msgObj, readMore) => {
lineHandler(msgObj.buff.toString('utf8'));
readMore();
@ -396,31 +462,6 @@ const getPinState = function (data, cb) {
});
};
const _getFileSize = function (channel, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length === HK.STANDARD_CHANNEL_LENGTH ||
channel.length === HK.ADMIN_CHANNEL_LENGTH) {
return void store.getChannelSize(channel, function (e, size) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, 0); }
return void cb(e.code);
}
cb(void 0, size);
});
}
// 'channel' refers to a file, so you need another API
blobStore.size(channel, function (e, size) {
if (typeof(size) === 'undefined') { return void cb(e); }
cb(void 0, size);
});
};
const getFileSize = function (data, cb) {
_getFileSize(data.channel, cb);
};
const _iterateFiles = function (channels, handler, cb) {
if (!Array.isArray(channels)) { return cb('INVALID_LIST'); }
var L = channels.length;
@ -444,6 +485,7 @@ const _iterateFiles = function (channels, handler, cb) {
const getTotalSize = function (data, cb) {
var bytes = 0;
monitoringIncrement('getTotalSize');
_iterateFiles(data.channels, function (channel, next) {
_getFileSize(channel, function (err, size) {
if (!err) { bytes += size; }
@ -489,6 +531,7 @@ const getHashOffset = function (data, cb) {
const lastKnownHash = data.hash;
if (typeof(lastKnownHash) !== 'string') { return void cb("INVALID_HASH"); }
monitoringIncrement('getHashOffset');
var offset = -1;
store.readMessagesBin(channelName, 0, (msgObj, readMore, abort) => {
// tryParse return a parsed message or undefined
@ -587,6 +630,8 @@ const completeUpload = function (data, cb) {
var arg = data.arg;
var size = data.size;
monitoringIncrement('uploadedBlob');
var method;
var label;
if (owned) {
@ -666,6 +711,7 @@ const COMMANDS = {
};
COMMANDS.INLINE = function (data, cb) {
monitoringIncrement('inlineValidation');
var signedMsg;
try {
signedMsg = Nacl.util.decodeBase64(data.msg);
@ -680,7 +726,8 @@ COMMANDS.INLINE = function (data, cb) {
return void cb("E_BADKEY");
}
// validate the message
const validated = Nacl.sign.open(signedMsg, validateKey);
//const validated = Nacl.sign.open(signedMsg, validateKey);
const validated = Env.crypto.open(signedMsg, validateKey);
if (!validated) {
return void cb("FAILED");
}
@ -689,6 +736,7 @@ COMMANDS.INLINE = function (data, cb) {
const checkDetachedSignature = function (signedMsg, signature, publicKey) {
if (!(signedMsg && publicKey)) { return false; }
monitoringIncrement('detachedValidation');
var signedBuffer;
var pubBuffer;
@ -720,7 +768,8 @@ const checkDetachedSignature = function (signedMsg, signature, publicKey) {
throw new Error("INVALID_SIGNATURE_LENGTH");
}
if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
//if (Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
if (Env.crypto.detachedVerify(signedBuffer, signatureBuffer, pubBuffer) !== true) {
throw new Error("FAILED");
}
};
@ -751,13 +800,28 @@ COMMANDS.HASH_CHANNEL_LIST = function (data, cb) {
};
COMMANDS.VALIDATE_ANCESTOR_PROOF = function (data, cb) {
monitoringIncrement('validateAncestorProof');
Block.validateAncestorProof(Env, data && data.proof, cb);
};
COMMANDS.VALIDATE_LOGIN_BLOCK = function (data, cb) {
monitoringIncrement('validateLoginBlock');
Block.validateLoginBlock(Env, data.publicKey, data.signature, data.block, cb);
};
Object.keys(plugins || {}).forEach(name => {
let plugin = plugins[name];
if (!plugin.addWorkerCommands) { return; }
try {
let events = plugin.addWorkerCommands(Env);
Object.keys(events || {}).forEach(cmd => {
if (typeof(events[cmd]) !== "function") { return; }
if (COMMANDS[cmd]) { return; }
COMMANDS[cmd] = events[cmd];
});
} catch (e) {}
});
process.on('message', function (data) {
if (!data || !data.txid || !data.pid) {
return void process.send({
@ -766,6 +830,13 @@ process.on('message', function (data) {
});
}
const command = COMMANDS[data.command];
// Command broadcasted to all workers: no callback expected
if (data.type === 'broadcast') {
return void command(data);
}
const cb = function (err, value) {
process.send({
error: Util.serializeError(err),
@ -783,7 +854,6 @@ process.on('message', function (data) {
});
}
const command = COMMANDS[data.command];
if (typeof(command) !== 'function') {
return void cb("E_BAD_COMMAND");
}

View File

@ -152,9 +152,34 @@ Workers.initialize = function (Env, config, _cb) {
// default to timing out affter 180s if no explicit timeout is passed
var timeout = typeof(opt.timeout) !== 'undefined'? opt.timeout: DEFAULT_QUERY_TIMEOUT;
response.expect(txid, cb, timeout);
delete msg._cb;
delete msg._opt;
state.worker.send(msg);
// Add original callback to message data in case we need
// to resend the command. setTimeout to avoid interfering
// with worker.send
setTimeout(function () {
msg._cb = _cb;
msg._opt = opt;
});
};
const pluginsResponses = {};
Object.keys(Env.plugins || {}).forEach(name => {
let plugin = Env.plugins[name];
if (!plugin.addWorkerResponses) { return; }
try {
let res = plugin.addWorkerResponses(Env);
Object.keys(res || {}).forEach(key => {
if (typeof(res[key]) !== "function") { return; }
if (pluginsResponses[key]) { return; }
pluginsResponses[key] = res[key];
});
} catch (e) {}
});
var handleResponse = function (state, res) {
if (!res) { return; }
// handle log messages before checking if it was addressed to your PID
@ -162,6 +187,17 @@ Workers.initialize = function (Env, config, _cb) {
if (res.log) {
return void handleLog(res.log, res.label, res.info);
}
// handle plugins
if (res.plugin) {
Object.keys(pluginsResponses).some(key => {
if (res.type !== key) { return; }
pluginsResponses[key](res.data);
return true;
});
return;
}
// but don't bother handling things addressed to other processes
// since it's basically guaranteed not to work
if (res.pid !== PID) {
@ -226,7 +262,15 @@ Workers.initialize = function (Env, config, _cb) {
handleResponse(state, res);
});
let pid = worker.pid;
var substituteWorker = Util.once(function () {
Object.keys(Env.plugins || {}).forEach(name => {
let plugin = Env.plugins[name];
if (!plugin.onWorkerClosed) { return; }
try { plugin.onWorkerClosed("db-worker", pid); }
catch (e) {}
});
Env.Log.info("SUBSTITUTE_DB_WORKER", '');
var idx = workers.indexOf(state);
if (idx !== -1) {
@ -237,26 +281,36 @@ Workers.initialize = function (Env, config, _cb) {
const cb = response.expectation(txid);
if (typeof(cb) !== 'function') { return; }
const task = state.tasks[txid];
if (!(task && task.msg)) { return; }
if (!task) { return; }
response.clear(txid);
Log.info('DB_WORKER_RESEND', task.msg);
sendCommand(task.msg, cb);
Log.info('DB_WORKER_RESEND', task);
sendCommand(task, task._cb || cb, task._opt);
});
var w = fork(DB_PATH);
initWorker(w, function (err, state) {
initWorker(w, function (err) {
if (err) {
throw new Error(err);
}
workers.push(state);
});
});
worker.on('exit', substituteWorker);
worker.on('close', substituteWorker);
worker.on('exit', function () {
substituteWorker();
Env.Log.error("DB_WORKER_EXIT", {
pid: state.pid,
});
});
worker.on('close', function () {
substituteWorker();
Env.Log.error("DB_WORKER_CLOSE", {
pid: state.pid,
});
});
worker.on('error', function (err) {
substituteWorker();
Env.Log.error("DB_WORKER_ERROR", {
pid: state.pid,
error: err,
});
});
@ -282,6 +336,18 @@ Workers.initialize = function (Env, config, _cb) {
}));
});
}).nThen(function () {
Env.broadcastWorkerCommand = (data) => {
workers.forEach(state => {
state.worker.send({
type: 'broadcast',
pid: PID,
command: data.command,
txid: data.txid
});
});
return workers;
};
Env.computeIndex = function (Env, channel, cb) {
Env.store.getWeakLock(channel, function (next) {
sendCommand({

1389
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "a collaborative office suite that is end-to-end encrypted and open-source",
"version": "2024.6.0",
"version": "2024.12.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
@ -22,29 +22,29 @@
"chainpad-crypto": "^0.2.5",
"chainpad-listmap": "^1.1.0",
"chainpad-netflux": "^1.2.0",
"chainpad-server": "^5.2.0",
"chainpad-server": "^5.2.4",
"ckeditor": "npm:ckeditor4@~4.22.1",
"codemirror": "^5.19.0",
"components-font-awesome": "^4.6.3",
"cookie-parser": "^1.4.6",
"cookie-parser": "^1.4.7",
"croppie": "^2.5.0",
"dragula": "3.7.2",
"drawio": "github:cryptpad/drawio-npm#npm-21.8.2+5",
"express": "~4.19.2",
"express": "~4.21.2",
"file-saver": "1.3.1",
"fs-extra": "^7.0.0",
"get-folder-size": "^2.0.1",
"html2canvas": "^1.4.0",
"http-proxy-middleware": "^2.0.6",
"http-proxy-middleware": "^3.0.3",
"hyper-json": "~1.4.0",
"jquery": "3.6.0",
"json.sortify": "~2.1.0",
"json.sortify": "github:cryptpad/JSON.sortify",
"jsonwebtoken": "^9.0.0",
"jszip": "3.10.1",
"localforage": "^1.5.2",
"marked": "^4.3.0",
"mathjax": "3.0.5",
"netflux-websocket": "^1.2.0",
"netflux-websocket": "^1.2.1",
"notp": "^2.0.3",
"nthen": "0.1.8",
"open-sans-fontface": "^1.4.0",
@ -53,7 +53,7 @@
"prompt-confirm": "^2.0.4",
"pull-stream": "^3.6.1",
"require-css": "0.1.10",
"requirejs": "2.3.5",
"requirejs": "2.3.7",
"requirejs-plugins": "^1.0.2",
"saferphore": "0.0.1",
"scrypt-async": "1.2.0",
@ -67,9 +67,9 @@
"x2js": "^3.4.4"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-plugin-compat": "^4.2.0",
"stylelint": "^16.6.1",
"eslint": "^9.16.0",
"eslint-plugin-compat": "^6.0.1",
"stylelint": "^16.9.0",
"stylelint-config-standard-less": "^3.0.1"
},
"overrides": {
@ -82,6 +82,7 @@
"install:components": "node scripts/copy-components.js",
"start": "node server.js",
"dev": "DEV=1 node server.js",
"windev": "set DEV=1& node server.js",
"fresh": "FRESH=1 node server.js",
"offline": "FRESH=1 OFFLINE=1 node server.js",
"offlinedev": "DEV=1 OFFLINE=1 node server.js",

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
# CryptPad
CryptPad is a collaboration suite that is end-to-end-encrypted and open-source. It is built to enable collaboration, synchronizing changes to documents in real time. Because all data are encrypted, in the eventuality of a breach, attackers have no way of seeing the stored content. Moreover, if the administrators dont alter the code, they and the service also cannot infer any piece of information about the users' content.
CryptPad is a collaboration suite that is end-to-end encrypted and open-source. It is designed to facilitate collaboration by synchronizing changes to documents in real time. Since all the user data is encrypted, in the event of a breach, attackers have no way of accessing the stored content. Furthermore, if the administrators do not modify the code, they and the service also cannot access any information about the users' content.
![Drive screenshot](screenshot.png "preview of the CryptDrive")
@ -20,7 +20,7 @@ Our [developer guide](https://docs.cryptpad.org/en/dev_guide/setup.html) provide
## For production
Configuring CryptPad for production requires a little more work, but the process is described in our [admin installation guide](https://docs.cryptpad.org/en/admin_guide/installation.html). From there you can find more information about customization and maintenance.
Configuring CryptPad for production requires additional steps. Refer to our [admin installation guide](https://docs.cryptpad.org/en/admin_guide/installation.html) for production-related instructions, customization, and maintenance details.
## Current version
@ -28,7 +28,7 @@ The most recent version and all past release notes can be found on the [releases
## Setup using Docker
You can find `Dockerfile`, `docker-compose.yml` and `docker-entrypoint.sh` files at the root of this repository. We also publish every release on [Docker Hub](https://hub.docker.com/r/cryptpad/cryptpad) as AMD64 & ARM64 official images.
You can find `Dockerfile`, `docker-compose.yml` and `docker-entrypoint.sh` files at the root of this repository. We also publish every release on [Docker Hub](https://hub.docker.com/r/cryptpad/cryptpad) as AMD64 & ARM64 official images.
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.

View File

@ -5,6 +5,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
for f in ../customize.dist/favicon/*.png; do
base="$(basename $f ".png")"
magick convert $f -define icon:auto-resize=16,24,32,48,64,72,96,128,256 "$base.ico"
base="$(basename $f ".png")"
magick convert $f -define icon:auto-resize=16,24,32,48,64,72,96,128,256 "$base.ico"
done

65
scripts/testcrypto.js Normal file
View File

@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2024 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
let SodiumNative = require('sodium-native');
let Nacl = require('tweetnacl/nacl-fast');
let LibSodium = require('libsodium-wrappers');
let msgStr = "This is a test";
let keys = Nacl.sign.keyPair();
let pub = keys.publicKey;
let msg = Nacl.util.decodeUTF8(msgStr);
let signedMsg = Nacl.sign(msg, keys.secretKey);
let sig = signedMsg.subarray(0, 64);
LibSodium.ready.then(() => {
/*
console.log('tweetnacl open');
console.log(!!Nacl.sign.open(signedMsg, pub));
console.log('tweetnacl detached');
console.log(Nacl.sign.detached.verify(msg, sig, pub));
console.log('sodium-native open');
console.log(SodiumNative.crypto_sign_open(msg, signedMsg, pub));
console.log('sodium-native detached');
console.log(SodiumNative.crypto_sign_verify_detached(sig, msg, pub));
LibSodium.ready.then(() => {
console.log('libsodium open');
console.log(!!LibSodium.crypto_sign_open(signedMsg, pub));
console.log('libsodium detached');
console.log(LibSodium.crypto_sign_verify_detached(sig, msg, pub));
});
*/
const n = 10000;
let a;
console.log('start sodium-native');
a = +new Date();
for (let i = 0; i < n; i++) {
SodiumNative.crypto_sign_open(msg, signedMsg, pub);
SodiumNative.crypto_sign_verify_detached(sig, msg, pub);
}
console.log('end sodium-native ', (+new Date() - a), ' ms');
console.log('start libsodium');
a = +new Date();
for (let i = 0; i < n; i++) {
LibSodium.crypto_sign_open(signedMsg, pub);
LibSodium.crypto_sign_verify_detached(sig, msg, pub);
}
console.log('end libsodium ', (+new Date() - a), ' ms');
console.log('start tweetnacl');
a = +new Date();
for (let i = 0; i < n; i++) {
Nacl.sign.open(signedMsg, pub);
Nacl.sign.detached.verify(msg, sig, pub);
}
console.log('end tweetnacl ', (+new Date() - a), ' ms');
});

View File

@ -0,0 +1,7 @@
{"owners":["TestOwner"],"validateKey":"TestKey","channel":"0","created":1721035117462}
[0,"test","MSG","test",1721035117969]
[0,"test","MSG","test2",1721035117969]
[0,"test","MSG","test3",1721035117969]
[0,"test","MSG","test4",1721035117969]
[0,"test","MSG","test5",1721035117969]
[0,"test","MSG","test6",1721035117969]

View File

@ -0,0 +1,5 @@
["RESTRICT_ACCESS",[true],1721035145090]
["ADD_ALLOWED",["NewAllowedKeyNewAllowedKeyNewAllowedKeyNewAl"],1721035148115]
["ADD_PENDING_OWNERS",["PendingOwner"],1721035151295]
["RESTRICT_ACCESS",[false],1721035155836]
["RESTRICT_ACCESS",[true],1721035156728]

View File

@ -0,0 +1,7 @@
{"owners":["TestOwner"],"validateKey":"TestKey","channel":"0","created":1721035117462}
[0,"test","MSG","test",1721035117969]
[0,"test","MSG","test2",1721035117969]
[0,"test","MSG","test3",1721035117969]
[0,"test","MSG","test4",1721035117969]
[0,"test","MSG","test5",1721035117969]
[0,"test","MSG","test6",1721035117969]

View File

@ -0,0 +1 @@
{"owners":["TestOwner"],"validateKey":"TestKey","channel":"0","created":1721035117462}

View File

@ -0,0 +1,6 @@
[0,"test","MSG","test",1721035117969]
[0,"test","MSG","test2",1721035117969]
[0,"test","MSG","test3",1721035117969]
[0,"test","MSG","test4",1721035117969]
[0,"test","MSG","test5",1721035117969]
[0,"test","MSG","test6",1721035117969]

View File

@ -0,0 +1,6 @@
{"owners":["TestOwner"],"validateKey":"TestKey","channel":"0","created":1721035117462}
["RESTRICT_ACCESS",[true],1721035145090]
["ADD_ALLOWED",["NewAllowedKeyNewAllowedKeyNewAllowedKeyNewAl"],1721035148115]
["ADD_PENDING_OWNERS",["PendingOwner"],1721035151295]
["RESTRICT_ACCESS",[false],1721035155836]
["RESTRICT_ACCESS",[true],1721035156728]

View File

@ -0,0 +1,6 @@
[0,"test","MSG","test",1721035117969]
[0,"test","MSG","test2",1721035117969]
[0,"test","MSG","test3",1721035117969]
[0,"test","MSG","test4",1721035117969]
[0,"test","MSG","test5",1721035117969]
[0,"test","MSG","test6",1721035117969]

View File

@ -0,0 +1,6 @@
[0,"test","MSG","test",1721035117969]
[0,"test","MSG","test2",1721035117969]
[0,"test","MSG","test3",1721035117969]
[0,"test","MSG","test4",1721035117969]
[0,"test","MSG","test5",1721035117969]
[0,"test","MSG","test6",1721035117969]

View File

@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const Store = require("../../lib/storage/file");
const Meta = require("../../lib/metadata");
const nThen = require('nthen');
let chanOld = '00000000000000000000000000000000';
let chanOldUpdated = '00000000000000000000000000000001';
let chanNew = '00000000000000000000000000000002';
let chanNewUpdated = '00000000000000000000000000000003';
let chanNoMeta = '00000000000000000000000000000004';
Store.create({
filePath: './test-data/'
}, (err, store) => {
if (err) { return void console.error(err); }
const readMetadata = (channel, cb) => {
const ref = {};
const h = Meta.createLineHandler(ref, console.error);
store.readChannelMetadata(channel, h, () => {
cb(ref && ref.meta);
});
};
nThen(w => {
readMetadata(chanOld, w(meta => {
if (!meta || meta.validateKey !== "TestKey") {
console.log('OldChanNoUpdate', meta);
throw new Error("Error with old channel without metadata update");
}
}));
readMetadata(chanOldUpdated, w(meta => {
if (!meta || meta.validateKey !== "TestKey" || !meta.restricted || !meta.allowed.includes('NewAllowedKeyNewAllowedKeyNewAllowedKeyNewAl')) {
console.log('OldChanUpdate', meta);
throw new Error("Error with old channel with metadata updates");
}
}));
readMetadata(chanNew, w(meta => {
if (!meta || meta.validateKey !== "TestKey") {
console.log('NewChanNoUpdate', meta);
throw new Error("Error with new channel without metadata update");
}
}));
readMetadata(chanNewUpdated, w(meta => {
if (!meta || meta.validateKey !== "TestKey" || !meta.restricted || !meta.allowed.includes('NewAllowedKeyNewAllowedKeyNewAllowedKeyNewAl')) {
console.log('NewChanUpdate', meta);
throw new Error("Error with new channel with metadata updates");
}
}));
readMetadata(chanNoMeta, w(meta => {
if (meta && Object.keys(meta).length) {
console.log('NoMetadataChan', meta);
throw new Error("Error with channel without metadata");
}
}));
}).nThen(() => {
console.log('Success');
process.exit(1);
});
});

View File

@ -25,6 +25,13 @@ var app = Express();
}
}());
Object.keys(Env.plugins || {}).forEach(name => {
let plugin = Env.plugins[name];
if (!plugin.initialize) { return; }
try { plugin.initialize(Env, "main"); }
catch (e) {}
});
var COMMANDS = {};
COMMANDS.LOG = function (msg, cb) {
@ -49,6 +56,25 @@ COMMANDS.GET_PROFILING_DATA = function (msg, cb) {
cb(void 0, Env.bytesWritten);
};
Object.keys(Env.plugins || {}).forEach(name => {
let plugin = Env.plugins[name];
if (!plugin.addMainCommands) { return; }
try {
let commands = plugin.addMainCommands(Env);
Object.keys(commands || {}).forEach(cmd => {
// Uppercase command name?
if (cmd !== cmd.toUpperCase()) { return; }
// Command is a function?
if (typeof(commands[cmd]) !== "function") { return; }
// Command doesn't already exists?
if (COMMANDS[cmd]) { return; }
COMMANDS[cmd] = commands[cmd];
});
} catch (e) {}
});
nThen(function (w) {
require("./lib/log").create(config, w(function (_log) {
Env.Log = _log;
@ -93,6 +119,7 @@ nThen(function (w) {
var launchWorker = (online) => {
var worker = Cluster.fork(workerState);
var pid = worker.process.pid;
worker.on('online', () => {
online();
});
@ -122,6 +149,13 @@ nThen(function (w) {
});
worker.on('exit', (code, signal) => {
Object.keys(Env.plugins || {}).forEach(name => {
let plugin = Env.plugins[name];
if (!plugin.onWorkerClosed) { return; }
try { plugin.onWorkerClosed("http-worker", pid); }
catch (e) {}
});
if (!signal && code === 0) { return; }
// relaunch http workers if they crash
Env.Log.error('HTTP_WORKER_EXIT', {
@ -147,10 +181,11 @@ nThen(function (w) {
});
};
var broadcast = (command, data/*, cb*/) => {
var broadcast = Env.broadcast = (command, data/*, cb*/) => {
for (const worker of Object.values(Cluster.workers)) {
sendCommand(worker, command, data /*, cb */);
}
return Object.values(Cluster.workers);
};
var throttledEnvChange = Util.throttle(function () {

View File

@ -22,12 +22,48 @@
flex-flow: column;
font: @colortheme_app-font;
input, textarea, .cp-appblock {
&:focus-visible {
outline: @variables_focus_style;
}
}
.cp-admin-customize-logo {
padding: 1em;
img {
max-height: 250px;
}
}
#cp-sidebarlayout-container {
#cp-sidebarlayout-rightside {
.cp-sidebarlayout-element[data-item] {
div#cp-admin-table-container {
overflow-x: auto;
.cp-sidebar-table#cp-admin-table {
tr {
display: flex;
}
th, td {
flex: 1;
word-wrap: break-word;
white-space: normal;
min-width: 13rem;
margin-right: 0px;
}
@media (max-width: @browser_media-not-small) {
width: 100%;
tr {
width: 100%;
}
th, td {
margin-right: 0;
min-width: 7rem;
}
}
}
}
}
}
}
.cp-admin-color-current {
width: 20px;
height: 20px;

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta name="referrer" content="no-referrer" />
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
</head>
<body>

View File

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/admin/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/admin/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.7"></script>
<style>
.loading-hidden { display: none; }
</style>

View File

@ -172,7 +172,7 @@ define([
const blocks = sidebar.blocks;
// EXTENSION_POINT:ADMIN_CATEGORY
common.getExtensions('ADMIN_CATEGORY').forEach(ext => {
common.getExtensionsSync('ADMIN_CATEGORY').forEach(ext => {
if (!ext || !ext.id || !ext.name || !ext.content) {
return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext);
}
@ -187,6 +187,7 @@ define([
};
});
const flushCache = (cb) => {
cb = cb || function () {};
sFrameChan.query('Q_ADMIN_RPC', {
@ -578,23 +579,38 @@ define([
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);
flushCache();
var isChecked = APP.instanceStatus.enforceMFA;
function showConfirmation(isChecked, setState) {
const confirmationContent = isChecked ? Messages.admin_mfa_confirm_disable : Messages.admin_mfa_confirm_enable;
UI.confirm(confirmationContent, function (confirmed) {
if (!confirmed) {
// User canceled their changes, restore the checkbox value
setState(isChecked);
return;
}
// User confirmed their changes, call the command and update the state
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);
} else {
APP.updateStatus(function () {
setState(APP.instanceStatus.enforceMFA);
flushCache();
});
}
});
});
});
},
}
showConfirmation(isChecked, setState);
}
});
var getInstanceString = function (attr) {
var val = APP.instanceStatus[attr];
var type = typeof(val);
@ -952,12 +968,7 @@ define([
});
};
let btn = blocks.activeButton('primary', '',
Messages.admin_colorChange, (done) => {
let color = $input.val();
setColor(color, done);
});
let $input = $();
let onColorPicked = () => {
require(['/lib/less.min.js'], (Less) => {
let color = $input.val();
@ -979,7 +990,14 @@ define([
$preview.find('.cp-admin-color-preview-light a').attr('style', `color: ${color} !important`);
});
};
let $input = $(input).on('change', onColorPicked).addClass('cp-admin-color-picker');
$input = $(input).on('change', onColorPicked).addClass('cp-admin-color-picker');
let btn = blocks.activeButton('primary', '',
Messages.admin_colorChange, (done) => {
let color = $input.val();
setColor(color, done);
});
UI.confirmButton($remove, {
classes: 'btn-danger',
@ -1101,6 +1119,9 @@ define([
""
];
var list = blocks.table(header, []);
list.setAttribute('id', 'cp-admin-table');
let div = blocks.block([list]);
div.setAttribute('id', 'cp-admin-table-container');
var nav = blocks.nav([button, refreshButton]);
var form = blocks.form([
@ -1201,7 +1222,7 @@ define([
});
});
cb([form, list]);
cb([form, div]);
});
var getBlockId = (val) => {
@ -1413,6 +1434,9 @@ define([
""
];
var list = blocks.table(header, []);
list.setAttribute('id', 'cp-admin-table');
let div = blocks.block([list]);
div.setAttribute('id', 'cp-admin-table-container');
var nav = blocks.nav([button, refreshButton]);
@ -1581,7 +1605,7 @@ define([
});
});
cb([form, list]);
cb([form, div]);
});
// Msg.admin_defaultlimitHint, .admin_defaultlimitTitle
@ -1741,6 +1765,9 @@ define([
Messages.admin_note
];
var table = blocks.table(header, []);
table.setAttribute('id', 'cp-admin-table');
let div = blocks.block([table]);
div.setAttribute('id', 'cp-admin-table-container');
let $table = $(table).hide();
APP.refreshLimits = function () {
@ -1798,7 +1825,7 @@ define([
});
};
APP.refreshLimits();
cb(table);
cb(div);
});
// Msg.admin_accountMetadataHint.admin_accountMetadataTitle
@ -2713,8 +2740,7 @@ define([
}, function (e, arr) {
pre.innerText = '';
let data = arr[0];
pre.append(String(data.blocks));
pre.append(' (old value including teams: ' + String(data.users) + ')'); // XXX
pre.append(String(data.users));
});
};
onRefresh();
@ -3161,6 +3187,7 @@ define([
labelEnd,
], blocks.nav([button]));
let send = function () {};
var refresh = getApi(function (Broadcast) {
$active.empty();
var removeButton = blocks.button('danger', '', Messages.admin_maintenanceCancel);
@ -3222,7 +3249,7 @@ define([
};
};
var send = function (data) {
send = function (data) {
disable($button);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
@ -3595,6 +3622,9 @@ define([
];
var table = blocks.table(header, []);
table.setAttribute('id', 'cp-admin-table');
let div = blocks.block([table]);
div.setAttribute('id', 'cp-admin-table-container');
const onRefresh = function () {
sFrameChan.query('Q_ADMIN_RPC', {
@ -3627,7 +3657,7 @@ define([
onRefresh();
onRefreshPerformance.reg(onRefresh);
cb(table);
cb(div);
});
@ -3898,16 +3928,15 @@ define([
// EXTENSION_POINT:ADMIN_ITEM
let utils = {
h, Util, Hash
h, Util, Hash, UIElements
};
common.getExtensions('ADMIN_ITEM').forEach(ext => {
common.getExtensionsSync('ADMIN_ITEM').forEach(ext => {
if (!ext || !ext.id || typeof(ext.getContent) !== "function") {
return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext);
}
if (sidebar.hasItem(ext.id)) {
return console.error('Extension point ID already used', ext);
}
sidebar.addItem(ext.id, cb => {
ext.getContent(common, blocks, utils, content => {
cb(content);
@ -3920,12 +3949,8 @@ define([
});
});
sidebar.makeLeftside(categories);
};
var updateStatus = APP.updateStatus = function (cb) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'INSTANCE_STATUS',

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-bootload="main.js" data-main="/common/boot.js" src="/components/requirejs/require.js"></script>
<script data-bootload="main.js" data-main="/common/boot.js" src="/components/requirejs/require.js?ver=2.3.7"></script>
<style>
.report {

View File

@ -9,6 +9,6 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-bootload="main.js" data-main="/common/boot.js" src="/components/requirejs/require.js"></script>
<script data-bootload="main.js" data-main="/common/boot.js" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body>

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body class="html">
<noscript></noscript>

View File

@ -39,7 +39,7 @@
.tui-full-calendar-confirm {
span, i {
padding: 0.5rem;
padding: 0 0.5rem;
}
span {
padding-left: 0;
@ -133,6 +133,13 @@
width: 540px;
max-width: 100%;
max-height: 100%;
border-radius: @variables_radius_L;
@media screen and (max-width : @browser_media-medium-screen){
position: fixed;
top: 5.5rem;
overflow-y: auto;
max-height: calc(100% - 6rem);
}
}
}
#tui-full-calendar-popup-arrow {
@ -153,10 +160,12 @@
border-color: @cp_calendar-border !important;
}
.tui-full-calendar-popup {
border-radius: @variables_radius_L;
}
.tui-full-calendar-popup-container {
.tui-full-calendar-section-allday{
&:focus {
outline: @cryptpad_color_brand solid 2px;
}
}
min-width: 100%;
background: @cp_flatpickr-bg;
color: @cryptpad_text_col;
@ -208,6 +217,9 @@
text-overflow: ellipsis;
font: @colortheme_app-font;
padding: 0 10px;
&:focus{
outline: @cryptpad_color_brand solid 2px;
}
}
input { flex: 1; }
}
@ -403,14 +415,16 @@
& > input {
height: 40px;
}
.tui-full-calendar-content{
&:focus{
outline: @cryptpad_color_brand solid 2px;
}
}
}
// margin-bottom: 20px;
input {
width: 80px;
margin-right: 0.3rem;
}
.fa-plus{
margin-left:0.3rem;
margin-right: 0.5rem;
}
}
}
@ -585,6 +599,18 @@
margin-top: 30px;
}
}
@media screen and (max-width: @browser_media-medium-screen) {
.cp-calendar-entries {
display: none;
}
.cp-calendar-entries.visible {
display: block;
}
}
.cp-calendar-entries {
margin-bottom: 10px;
}
.cp-calendar-entry {
display: flex;
align-items: center;
@ -614,6 +640,7 @@
}
&.cp-ghost {
padding: 0;
margin-top: 1rem;
button {
.tools_unselectable();
cursor: pointer;
@ -633,6 +660,9 @@
color: @cp_sidebar-left-active-fg;
border: 0px;
}
&:focus-visible {
outline: @variables_focus_style;
}
i {
margin-left: 5px;
}

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta name="referrer" content="no-referrer" />
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
</head>
<body>

View File

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/calendar/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/calendar/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.7"></script>
<style>
.loading-hidden { display: none; }
</style>

View File

@ -775,7 +775,7 @@ define([
var data = APP.calendars[id];
var edit;
if (data.loading) {
edit = h('i.fa.fa-spinner.fa-spin');
edit = h('i.fa.fa-spinner.fa-spin', {'aria-hidden': 'true'});
} else {
edit = makeEditDropdown(id, teamId);
}
@ -796,8 +796,8 @@ define([
h('i.cp-calendar-inactive.fa.fa-calendar-o')
]),
h('span.cp-calendar-title', md.title),
data.restricted ? h('i.fa.fa-ban', {title: Messages.fm_restricted}) :
(isReadOnly(id, teamId) ? h('i.fa.fa-eye', {title: Messages.readonly}) : undefined),
data.restricted ? h('i.fa.fa-ban', {title: Messages.fm_restricted, 'aria-hidden': 'true'}) :
(isReadOnly(id, teamId) ? h('i.fa.fa-eye', {title: Messages.readonly, 'aria-hidden': 'true'}) : undefined),
edit
]);
var $calendar = $(calendar).click(function () {
@ -820,10 +820,14 @@ define([
if (APP.$calendars) { APP.$calendars.append(calendar); }
return calendar;
};
var makeLeftside = function (calendar, $container) {
// Show calendars
var calendars = h('div.cp-calendar-list');
var $calendars = APP.$calendars = $(calendars).appendTo($container);
var isMobileView = window.innerWidth <= 600;
var visible = !isMobileView; // Initialize 'visible' state: true for large screens, false for mobile
onCalendarsUpdate.reg(function () {
$calendars.empty();
var privateData = metadataMgr.getPrivateData();
@ -831,7 +835,7 @@ define([
var LOOKUP = {};
return Object.keys(APP.calendars || {}).filter(function (id) {
var cal = APP.calendars[id] || {};
var teams = (cal.teams || []).map(function (tId) { return Number(tId); });
var teams = (cal.teams || []).map(function (tId) { return Number(tId); });
return teams.indexOf(typeof(teamId) !== "undefined" ? Number(teamId) : 1) !== -1;
}).map(function (k) {
// nearly constant-time pre-sort
@ -872,26 +876,33 @@ define([
}
return;
}
var myCalendars = filter(1);
var totalCalendars = myCalendars.length + Object.keys(privateData.teams).reduce((sum, teamId) => {
return sum + filter(teamId).length;
}, 0);
var $contentContainer = $(h('div.cp-calendar-content')).appendTo($calendars);
if (myCalendars.length) {
var user = metadataMgr.getUserData();
var avatar = h('span.cp-avatar');
var uid = user.uid;
var name = user.name || Messages.anonymous;
common.displayAvatar($(avatar), user.avatar, name, function(){}, uid);
APP.$calendars.append(h('div.cp-calendar-team', [
$contentContainer.append(h('div.cp-calendar-team', [
avatar,
h('span.cp-name', {title: name}, name)
]));
myCalendars.forEach((id) => {
var calendarEntry = makeCalendarEntry(id, 1);
$contentContainer.append(calendarEntry);
});
}
myCalendars.forEach(function (id) {
makeCalendarEntry(id, 1);
});
// Add new button
var $newContainer = $(h('div.cp-calendar-entry.cp-ghost')).appendTo($calendars);
// Add the new calendar button
var $newContainer = $(h('div.cp-calendar-entry.cp-ghost')).appendTo($contentContainer);
var newButton = h('button', [
h('i.fa.fa-calendar-plus-o'),
h('i.fa.fa-calendar-plus-o', {'aria-hidden': 'true'}),
h('span', Messages.calendar_new),
h('span')
]);
@ -905,19 +916,61 @@ define([
var team = privateData.teams[teamId];
var avatar = h('span.cp-avatar');
common.displayAvatar($(avatar), team.avatar, team.displayName || team.name);
APP.$calendars.append(h('div.cp-calendar-team', [
var $teamContainer = h('div.cp-calendar-team', [
avatar,
h('span.cp-name', {title: team.name}, team.name)
]));
calendars.forEach(function (id) {
makeCalendarEntry(id, teamId);
h('span.cp-name', {title: team.name}, team.name),
h('span')
]);
$contentContainer.append($teamContainer);
calendars.forEach((id) => {
var calendarEntry = makeCalendarEntry(id, teamId);
$contentContainer.append(calendarEntry);
});
});
if(isMobileView) {
// If initial number of calendars or current number > 2,
// hide the calendars list and display a "show" button
if (APP.numberCalendars > 2 || totalCalendars > 2) {
var $showContainer = $(h('div.cp-calendar-entry.cp-ghost')).appendTo($calendars);
var iconClass = visible ? 'fa-eye-slash' : 'fa-eye';
var buttonText = visible ? Messages.calendar_hide : Messages.calendar_show;
var showCalendarsBtn = h('button', [
h('i.fa.' + iconClass, {'aria-hidden': "true"}),
h('span.cp-calendar-title', buttonText),
h('span')
]);
$(showCalendarsBtn).click(() => {
visible = !visible;
$contentContainer.toggle(visible);
iconClass = visible ? 'fa-eye-slash' : 'fa-eye';
buttonText = visible ? Messages.calendar_hide : Messages.calendar_show;
$(showCalendarsBtn).find('i').attr('class', 'fa ' + iconClass).attr('aria-hidden', "true");
$(showCalendarsBtn).find('span').first().text(visible ? Messages.calendar_hide : Messages.calendar_show);
}).appendTo($showContainer);
}
else {visible = true;}
}
$contentContainer.toggle(visible);
$(window).resize(function () {
var newIsMobileView = window.innerWidth <= 600;
if (!newIsMobileView) {
visible = true;
$contentContainer.show();
}
if (newIsMobileView !== isMobileView) {
isMobileView = newIsMobileView;
if (isMobileView) {
visible = false;
$contentContainer.hide();
}
onCalendarsUpdate.fire();
}
});
});
onCalendarsUpdate.fire();
};
var _updateRecurring = function () {
var cal = APP.calendar;
if (!cal) { return; }
@ -1275,17 +1328,15 @@ ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a v
store.put('calendarView', mode, function () {});
});
APP.toolbar.$bottomR.append($block);
// New event button
var newEventBtn = h('button.cp-calendar-newevent', [
h('i.fa.fa-plus'),
h('i.fa.fa-plus', {'aria-hidden': 'true'}),
h('span', Messages.calendar_newEvent)
]);
$(newEventBtn).click(function (e) {
e.preventDefault();
cal.openCreationPopup({isAllDay:false});
}).appendTo(APP.toolbar.$bottomL);
// Change page
var goLeft = h('button.fa.fa-chevron-left',{'aria-label': Messages.goLeft});
var goRight = h('button.fa.fa-chevron-right', {'aria-label': Messages.goRight});
@ -2063,6 +2114,15 @@ APP.recurrenceRule = {
editor.setOption('readOnly', false);
editor.setOption('autoRefresh', true);
editor.setOption('gutters', []);
editor.on('keydown', function (editor, e) {
if (e.which === 27) {
let $next = $(e.target).closest('.tui-full-calendar-popup-section').next();
if ($next.length) {
$next.find('#tui-full-calendar-schedule-start-date').focus();
}
e.stopPropagation();
}
});
cm.configureTheme(common, function () {});
editor.setValue(oldEventBody);
@ -2143,10 +2203,17 @@ APP.recurrenceRule = {
// Customize creation/update popup
var onCalendarPopup = function (el) {
var $el = $(el);
$el.find('.tui-full-calendar-confirm').addClass('btn btn-primary').prepend(h('i.fa.fa-floppy-o'));
$el.find('.tui-full-calendar-confirm').addClass('btn btn-primary').prepend(h('i.fa.fa-floppy-o', {'aria-hidden': 'true'}));
$el.find('input').attr('autocomplete', 'off');
$el.find('.tui-full-calendar-dropdown-button').addClass('btn btn-secondary');
$el.find('.tui-full-calendar-popup-close').addClass('btn btn-cancel fa fa-times cp-calendar-close').empty();
$el.find('.tui-full-calendar-section-allday').attr('tabindex', 0);
$el.find('.cp-calendar-close').attr('tabindex',-1);
$el.find('.tui-full-calendar-section-allday').keydown(function (e) {
if (e.which === 13) {
$(this).click();
}
});
var $container = $el.closest('.tui-full-calendar-floating-layer');
$container.addClass('cp-calendar-popup-flex');
@ -2209,6 +2276,7 @@ APP.recurrenceRule = {
setFormat(allDay);
});
});
UI.addTabListener(el);
};
var onCalendarEditPopup = function (el) {
var $el = $(el);
@ -2217,7 +2285,7 @@ APP.recurrenceRule = {
$el.find('.tui-full-calendar-content').removeClass('tui-full-calendar-content');
var delButton = h('button.btn.btn-danger', [
h('i.fa.fa-trash'),
h('i.fa.fa-trash', {'aria-hidden': 'true'}),
h('span', Messages.kanban_delete)
]);
var $del = $el.find('.tui-full-calendar-popup-delete').hide();
@ -2244,12 +2312,13 @@ APP.recurrenceRule = {
var data = ev.schedule || {};
var id = data.id;
UI.addTabListener(el);
if (!id) { return; }
if (id.indexOf('|') === -1) { return; } // Original event ID doesn't contain |
// This is a recurring event, add button to stop recurrence now
var $b = $(h('button.btn.btn-default', [
h('i.fa.fa-times'),
h('i.fa.fa-times', {'aria-hidden': 'true'}),
h('span', Messages.calendar_rec_stop)
])).insertBefore($section);
UI.confirmButton($b[0], { classes: 'btn-default' }, function () {
@ -2322,7 +2391,9 @@ APP.recurrenceRule = {
});
var store = window.cryptpadStore;
APP.module.execCommand('SUBSCRIBE', null, function (obj) {
if (obj.empty && !privateData.calendarHash) {
let empty = !obj.length;
APP.numberCalendars = obj.length;
if (empty && !privateData.calendarHash) {
if (!privateData.loggedIn) {
return void UI.errorLoadingScreen(Messages.mustLogin, false, function () {
common.setLoginRedirect('login');

View File

@ -15,5 +15,5 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<div id="cp-progress"></div>
<iframe-placeholder>
<script type="text/javascript" src="/checkup/dependency-warning.js?ver=1.0.1"></script>
<script data-bootload="main.js" data-main="/common/boot.js" src="/components/requirejs/require.js"></script>
<script data-bootload="main.js" data-main="/common/boot.js" src="/components/requirejs/require.js?ver=2.3.7"></script>

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<html class="cp-app-noscroll cp-app-print">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/checkup/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/checkup/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body class="cp-app-checkup">
</body>

View File

@ -574,7 +574,7 @@ define([
});
assert(function (cb, msg) {
var support = ApiConfig.supportMailbox;
var support = ApiConfig.supportMailboxKey;
setWarningClass(msg);
msg.appendChild(h('span', [
"This instance's encrypted support ticket functionality has not been enabled. This can make it difficult for its users to safely report issues that concern sensitive information. ",

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script data-bootload="main.js" data-main="/common/boot.js" src="/components/requirejs/require.js"></script>
<script data-bootload="main.js" data-main="/common/boot.js" src="/components/requirejs/require.js?ver=2.3.7"></script>
</head>
<body>
<div id="cp-progress"></div>

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta name="referrer" content="no-referrer" />
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/common/sframe-app-outer.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/common/sframe-app-outer.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.7"></script>
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
<link id="favicon-ico" type="image/x-icon" rel="icon"
data-main-favicon="/customize/favicon/main-favicon-code.ico"

View File

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/code/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.5"></script>
<script async data-bootload="/code/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.7"></script>
<style>
.loading-hidden { display: none; }
#editor1 { display: none; }

View File

@ -106,7 +106,7 @@ define([
framework._.toolbar.$theme.append($showAuthorColors);
markers.setButton($showAuthorColors);
};
var mkPrintButton = function (framework, $content) {
var mkPrintButton = function (framework, $content, $print) {
var $printButton = framework._.sfCommon.createButton('print', true);
$printButton.click(function () {
$print.html($content.html());
@ -115,8 +115,8 @@ define([
framework.feedback('PRINT_CODE');
UI.clearTooltipsDelay();
});
var $print = UIElements.getEntryFromButton($printButton);
framework._.toolbar.$drawer.append($print);
var $dropdownEntry = UIElements.getEntryFromButton($printButton);
framework._.toolbar.$drawer.append($dropdownEntry);
};
var mkMarkdownTb = function (editor, framework) {
var $codeMirrorContainer = $('#cp-app-code-container');

View File

@ -132,7 +132,7 @@ define(function() {
// en: "Hello world",
// fr: "Bonjour le monde",
// de: "Hallo Welt",
// "pt-br": "Olá Mundo"<
// "pt-br": "Olá Mundo"
};
/* Cryptpad apps use a common API to display notifications to users

View File

@ -164,12 +164,12 @@ define([
dialog.okButton = function (content, classString) {
var sel = typeof(classString) === 'string'? 'button.ok.' + classString:'button.btn.ok.primary';
return h(sel, { tabindex: '2', }, content || Messages.okButton);
return h(sel, content || Messages.okButton);
};
dialog.cancelButton = function (content, classString) {
var sel = typeof(classString) === 'string'? 'button.' + classString:'button.btn.cancel';
return h(sel, { tabindex: '1'}, content || Messages.cancelButton);
return h(sel, content || Messages.cancelButton);
};
dialog.message = function (text) {
@ -577,7 +577,7 @@ define([
return frame;
};
let addTabListener = frame => {
let addTabListener = UI.addTabListener = frame => {
// find focusable elements
let modalElements = $(frame).find('a, button, input, [tabindex]:not([tabindex="-1"]), textarea').filter(':visible').filter(':not(:disabled)');
@ -670,6 +670,7 @@ define([
$modal: $blockContainer,
show: function () {
$blockContainer.css('display', 'flex');
addTabListener($blockContainer);
},
hide: hide
};
@ -719,6 +720,7 @@ define([
Notifier.notify();
});
addTabListener(frame);
return {
element: frame,
delete: close
@ -770,7 +772,7 @@ define([
document.body.appendChild(frame);
setTimeout(function () {
$(input).select().focus();
addTabListener(frame);
Notifier.notify();
});
};
@ -778,7 +780,6 @@ define([
UI.confirm = function (msg, cb, opt, force) {
cb = cb || function () {};
opt = opt || {};
var message;
if (typeof(msg) === 'string') {
if (!force) { msg = Util.fixHTML(msg); }
@ -811,19 +812,11 @@ define([
addTabListener(frame);
frame.addEventListener('keydown', function(e) {
if (e.keyCode === 13) {
if (document.activeElement === $ok[0]) {
$ok.click();
} else if (document.activeElement === $cancel[0]) {
$cancel.click();
}
} else if (e.keyCode === 27) {
$cancel.click();
}
});
listener = listenForKeys(function () {
// Only trigger OK if cancel is not focused
if (document.activeElement === $cancel[0]) {
return void $cancel.click();
}
$ok.click();
}, function () {
$cancel.click();

View File

@ -623,13 +623,22 @@ define([
var appType = (common.getMetadataMgr().getMetadata().type || 'pad').toUpperCase();
data = data || {};
if (!callback && data.callback) { callback = data.callback; }
let makeButton = function(iconClasses, buttonClasses, title, text) {
const ariaLabel = title || text || '';
return $(h('button', {
class: buttonClasses,
title: title,
'aria-label': ariaLabel
}, [
iconClasses ? h('i', { class: iconClasses }) : null,
text ? h('span', { class: 'cp-toolbar-drawer-element' }, text) : null
]));
};
switch (type) {
case 'export':
button = $('<button>', {
'class': 'fa fa-download cp-toolbar-icon-export',
title: Messages.exportButtonTitle,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.exportButton));
button = makeButton('fa fa-download', 'cp-toolbar-icon-export', Messages.exportButtonTitle, Messages.exportButton);
button
.click(common.prepareFeedback(type))
.click(UI.clearTooltipsDelay);
@ -638,34 +647,28 @@ define([
}
break;
case 'import':
button = $('<button>', {
'class': 'fa fa-upload cp-toolbar-icon-import',
title: Messages.importButtonTitle,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.importButton));
var importer = importContent((data && data.binary) ? 'application/octet-stream' : 'text/plain', callback, {
accept: data ? data.accept : undefined,
binary: data ? data.binary : undefined
});
button = makeButton('fa fa-upload', 'cp-toolbar-icon-import', Messages.importButtonTitle, Messages.importButton);
var importer = importContent((data && data.binary) ? 'application/octet-stream' : 'text/plain', callback, {
accept: data ? data.accept : undefined,
binary: data ? data.binary : undefined
});
var handler = data.first? function () {
data.first(function () {
importer(); // Make sure we don't pass arguments to importer
});
}: importer; //importContent;
button
.click(common.prepareFeedback(type))
.click(function () {
handler();
UI.clearTooltipsDelay();
var handler = data.first? function () {
data.first(function () {
importer(); // Make sure we don't pass arguments to importer
});
}: importer; //importContent;
button
.click(common.prepareFeedback(type))
.click(function () {
handler();
UI.clearTooltipsDelay();
});
//}
break;
case 'upload':
button = $('<button>', {
'class': 'btn btn-primary new',
title: Messages.uploadButtonTitle,
}).append($('<span>', {'class':'fa fa-upload'})).append(' '+Messages.uploadButton);
button = makeButton('fa fa-upload', 'btn btn-primary new', Messages.uploadButtonTitle, Messages.uploadButton);
if (!data.FM) { return; }
var $input = $('<input>', {
'type': 'file',
@ -697,9 +700,7 @@ define([
});
break;
case 'copy':
button = $('<button>', {
'class': 'fa fa-files-o cp-toolbar-icon-import',
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.makeACopy));
button = makeButton('fa fa-files-o', 'cp-toolbar-icon-import', '', Messages.makeACopy);
button
.click(common.prepareFeedback(type))
.click(function () {
@ -710,9 +711,7 @@ define([
case 'importtemplate':
if (!AppConfig.enableTemplates) { return; }
if (!common.isLoggedIn()) { return; }
button = $('<button>', {
'class': 'fa fa-upload cp-toolbar-icon-import',
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.template_import));
button = makeButton('fa fa-upload', 'cp-toolbar-icon-import', '', Messages.template_import);
button
.click(common.prepareFeedback(type))
.click(function () {
@ -724,9 +723,7 @@ define([
case 'template':
if (!AppConfig.enableTemplates) { return; }
if (!common.isLoggedIn()) { return; }
button = $('<button>', {
'class': 'cptools cptools-new-template cp-toolbar-icon-template',
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.saveTemplateButton));
button = makeButton('cptools cptools-new-template', 'cp-toolbar-icon-template', '', Messages.saveTemplateButton);
if (data.rt || data.callback) {
button
.click(function () {
@ -774,9 +771,7 @@ define([
}
break;
case 'forget':
button = $('<button>', {
'class': "fa fa-trash cp-toolbar-icon-forget"
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.fc_delete));
button = makeButton('fa fa-trash', 'cp-toolbar-icon-forget', '', Messages.fc_delete);
callback = typeof callback === "function" ? callback : function () {};
button
.click(common.prepareFeedback(type))
@ -837,25 +832,16 @@ define([
])).click(common.prepareFeedback(type));
break;
case 'print':
button = $('<button>', {
title: Messages.printButtonTitle2,
'class': "fa fa-print cp-toolbar-icon-print",
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.printText));
button = makeButton('fa fa-print', 'cp-toolbar-icon-print', Messages.printButtonTitle2, Messages.printText);
break;
case 'history':
if (!AppConfig.enableHistory) {
button = $('<span>');
break;
}
button = $('<button>', {
title: Messages.historyButton,
'aria-label': Messages.historyButton,
'class': "fa fa-history cp-toolbar-icon-history",
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.historyText));
button = makeButton('fa fa-history', 'cp-toolbar-icon-history', Messages.historyButton, Messages.historyText, Messages.historyButton);
if (data.histConfig) {
button
.click(common.prepareFeedback(type))
.on('click', function () {
button.click(common.prepareFeedback(type)).on('click', function () {
common.getHistory(data.histConfig);
UI.clearTooltipsDelay();
});
@ -879,9 +865,10 @@ define([
if (callback) { button.click(callback); }
break;
case 'storeindrive':
button = $(h('button.cp-toolbar-storeindrive.fa.fa-hdd-o', {
button = $(h('button.cp-toolbar-storeindrive', {
style: 'display:none;'
}, [
h('i.fa.fa-hdd-o'),
h('span.cp-toolbar-name.cp-toolbar-drawer-element', Messages.toolbar_storeInDrive)
])).click(common.prepareFeedback(type)).click(function () {
$(button).hide();
@ -902,10 +889,7 @@ define([
});
break;
case 'hashtag':
button = $('<button>', {
'class': 'fa fa-hashtag cp-toolbar-icon-hashtag',
title: Messages.tags_title,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.fc_hashtag));
button = makeButton('fa fa-hashtag', 'cp-toolbar-icon-hashtag', Messages.tags_title, Messages.fc_hashtag);
button.click(common.prepareFeedback(type))
.click(function () {
common.isPadStored(function (err, data) {
@ -950,11 +934,8 @@ define([
//updateIcon(data.element.is(':visible'));
break;
case 'properties':
button = $('<button>', {
'class': 'fa fa-info-circle cp-toolbar-icon-properties',
title: Messages.propertiesButtonTitle,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'})
.text(Messages.propertiesButton))
button = makeButton('fa fa-info-circle', 'cp-toolbar-icon-properties', Messages.propertiesButtonTitle, Messages.propertiesButton);
button
.click(common.prepareFeedback(type))
.click(function () {
var isTop;
@ -970,11 +951,8 @@ define([
});
break;
case 'save': // OnlyOffice save
button = $('<button>', {
'class': 'fa fa-save',
title: Messages.settings_save,
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'})
.text(Messages.settings_save))
button = makeButton('fa fa-save', '', Messages.settings_save, Messages.settings_save);
button
.click(function() {
common.prepareFeedback(type);
UI.clearTooltipsDelay();
@ -982,10 +960,7 @@ define([
if (callback) { button.click(callback); }
break;
case 'newpad':
button = $('<button>', {
title: Messages.newButtonTitle,
'class': 'fa fa-plus cp-toolbar-icon-newpad',
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.newButton));
button = makeButton('fa fa-plus', 'cp-toolbar-icon-newpad', Messages.newButtonTitle, Messages.newButton);
button
.click(common.prepareFeedback(type))
.click(function () {
@ -994,10 +969,7 @@ define([
});
break;
case 'snapshots':
button = $('<button>', {
title: Messages.snapshots_button,
'class': 'fa fa-camera cp-toolbar-icon-snapshots',
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.snapshots_button));
button = makeButton('fa fa-camera', 'cp-toolbar-icon-snapshots', Messages.snapshots_button,Messages.snapshots_button);
button
.click(common.prepareFeedback(type))
.click(function () {
@ -1280,6 +1252,7 @@ define([
kanban: 'kanban',
form: 'form',
whiteboard: 'whiteboard',
diagram: 'diagram',
};
var href = "https://docs.cryptpad.org/en/user_guide/applications.html";
@ -1607,10 +1580,10 @@ define([
]));
if (config.caretDown) {
$button.append(h('i.fa.fa-caret-down'));
$button.prepend(h('i.fa.fa-caret-down'));
}
if (config.angleDown) {
$button.append(h('i.fa.fa-angle-down'));
$button.prepend(h('i.fa.fa-angle-down'));
}
// Menu
@ -2510,10 +2483,21 @@ define([
});
var selected = -1;
var previous = function () {
selected = (selected === 0 ? types.length : selected) - 1;
$container.find('.cp-icons-element-selected').removeClass('cp-icons-element-selected');
let element = $container.find('#cp-newpad-icons-'+selected).addClass('cp-icons-element-selected');
if (element.hasClass('cp-app-disabled')) {
previous();
}
};
var next = function () {
selected = ++selected % types.length;
$container.find('.cp-icons-element-selected').removeClass('cp-icons-element-selected');
$container.find('#cp-newpad-icons-'+selected).addClass('cp-icons-element-selected');
let element = $container.find('#cp-newpad-icons-'+selected).addClass('cp-icons-element-selected');
if (element.hasClass('cp-app-disabled')) {
next();
}
};
$modal.off('keydown');
@ -2521,7 +2505,11 @@ define([
if (e.which === 9) {
e.preventDefault();
e.stopPropagation();
next();
if (e.shiftKey) {
previous();
} else {
next();
}
return;
}
if (e.which === 13) {
@ -2537,6 +2525,7 @@ define([
window.setTimeout(function () {
modal.show();
$modal.focus();
next();
});
};
@ -2782,7 +2771,11 @@ define([
]);
// Password
var password = h('div.cp-creation-password', [
let text;
if (type === 'form') {
text = h('div.cp-creation-password-warning.alert.alert-info.dismissable', h('span.cp-inline-alert-text', Messages.form_passwordWarning));
}
var password = h('div.cp-creation-password', [
UI.createCheckbox('cp-creation-password', Messages.properties_addPassword, false),
h('span.cp-creation-password-picker.cp-creation-slider', [
UI.passwordInput({id: 'cp-creation-password-val'})
@ -2820,6 +2813,7 @@ define([
expire,
password,
]),
text,
templates,
createDiv
])).appendTo($creation);
@ -3559,7 +3553,7 @@ define([
$(link).click(function (e) {
e.preventDefault();
e.stopPropagation();
var obj = { pw: msg.content.password || '' };
var obj = { pw: msg.content.password || '', f: 1 };
common.openURL(Hash.getNewPadURL(msg.content.href, obj));
});
@ -3615,7 +3609,7 @@ define([
// Add the pad to your drive
// This command will also add your mailbox to the metadata log
// The callback is called when the pad is stored, independantly of the metadata command
// The callback is called when the pad is stored, independently of the metadata command
if (data.calendar) {
var calendarModule = common.makeUniversal('calendar');
var calendarData = data.calendar;

View File

@ -928,13 +928,15 @@ define([
delete meta.cursor;
if (meta.type === "form") {
// Keep anonymous and makeAnonymous values from templates
// Keep anonymous, makeAnonymous and submit message values from templates
var anonymous = parsed.answers.anonymous || false;
var makeAnonymous = parsed.answers.makeAnonymous || false;
var msg = parsed.answers.msg || undefined;
delete parsed.answers;
parsed.answers = {
anonymous: anonymous,
makeAnonymous: makeAnonymous
makeAnonymous: makeAnonymous,
msg: msg
};
}
}
@ -2619,7 +2621,8 @@ define([
disableCache: localStorage['CRYPTPAD_STORE|disableCache'],
driveEvents: !rdyCfg.noDrive, //rdyCfg.driveEvents // Boolean
lastVisit: Number(localStorage.lastVisit) || undefined,
blockId: blockId
blockId: blockId,
blockHash: blockHash
};
common.userHash = userHash || LocalStore.getUserHash();
@ -2712,7 +2715,7 @@ define([
window.addEventListener('unload', function () {
postMsg('CLOSE');
});
// eslint-disable-next-line no-constant-condition
// eslint-disable-next-line no-constant-condition,no-constant-binary-expression
} else if (false && !noWorker && !noSharedWorker && 'serviceWorker' in navigator) {
var initializing = true;
var stopWaiting = waitFor2(); // Call this function when we're ready

View File

@ -314,20 +314,29 @@ define([
};
APP.selectedFiles = [];
var findElementId = function ($element) {
var isTrashed = $element.data("path")[0] === TRASH;
let elementId;
if (isTrashed) {
elementId = $element.data("path").join(',');
} else {
elementId = $element.data("path").slice(-1)[0];
}
return elementId;
};
var isElementSelected = function ($element) {
var elementId = $element.data("path").slice(-1)[0];
var elementId = findElementId($element);
return APP.selectedFiles.indexOf(elementId) !== -1;
};
var selectElement = function ($element) {
var elementId = $element.data("path").slice(-1)[0];
var elementId = findElementId($element);
if (APP.selectedFiles.indexOf(elementId) === -1) {
APP.selectedFiles.push(elementId);
}
$element.addClass("cp-app-drive-element-selected");
};
var unselectElement = function ($element) {
var elementId = $element.data("path").slice(-1)[0];
var elementId = findElementId($element);
var index = APP.selectedFiles.indexOf(elementId);
if (index !== -1) {
APP.selectedFiles.splice(index, 1);
@ -990,6 +999,9 @@ define([
// Arrow keys to modify the selection
var onWindowKeydown = function (e) {
if (!$content.is(':visible')) { return; }
if ($('.cp-modal').is(':visible')) {
return;
}
var $searchBar = $tree.find('#cp-app-drive-tree-search-input');
if (document.activeElement && document.activeElement.nodeName === 'INPUT') { return; }
if ($searchBar.is(':focus') && $searchBar.val()) { return; }
@ -1459,7 +1471,6 @@ define([
}
});
if (paths.length > 1) {
hide.push('restore');
hide.push('properties', 'access');
hide.push('rename');
hide.push('openparent');
@ -2968,6 +2979,16 @@ define([
});
UI.openCustomModal(m);
};
var triggerEnter = function (selector, $context) {
$context.find(selector).on('keypress', function (event) {
if (event.which === 13) { // enter
event.preventDefault();
$(this).trigger('click'); // the keypress event triggers the click event
}
});
};
var addNewPadHandlers = function ($block, isInRoot) {
// Handlers
if (isInRoot) {
@ -2983,56 +3004,51 @@ define([
refresh();
};
$block.find('a.cp-app-drive-new-folder, li.cp-app-drive-new-folder')
.on('click keypress', function (event) {
if (event.type === 'click' || (event.type === 'keypress' && event.which === 13)) {
event.preventDefault();
manager.addFolder(currentPath, null, onCreated);
}
.on('click', function (event) {
event.preventDefault();
manager.addFolder(currentPath, null, onCreated);
});
triggerEnter('a.cp-app-drive-new-folder, li.cp-app-drive-new-folder', $block);
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
$block.find('a.cp-app-drive-new-shared-folder, li.cp-app-drive-new-shared-folder')
.on('click keypress', function (event) {
if (event.type === 'click' || (event.type === 'keypress' && event.which === 13)) {
event.preventDefault();
addSharedFolderModal(function (obj) {
if (!obj) { return; }
manager.addSharedFolder(currentPath, obj, refresh);
});
}
.on('click', function (event) {
event.preventDefault();
addSharedFolderModal(function (obj) {
if (!obj) { return; }
manager.addSharedFolder(currentPath, obj, refresh);
});
});
triggerEnter('a.cp-app-drive-new-shared-folder, li.cp-app-drive-new-shared-folder', $block);
}
$block.find('a.cp-app-drive-new-fileupload, li.cp-app-drive-new-fileupload')
.on('click keypress', function (event) {
if (event.type === 'click' || (event.type === 'keypress' && event.which === 13)) {
event.preventDefault();
showUploadFilesModal();
}
.on('click', function (event) {
event.preventDefault();
showUploadFilesModal();
});
triggerEnter('a.cp-app-drive-new-fileupload, li.cp-app-drive-new-fileupload', $block);
$block.find('a.cp-app-drive-new-folderupload, li.cp-app-drive-new-folderupload')
.on('click keypress', function (event) {
if (event.type === 'click' || (event.type === 'keypress' && event.which === 13)) {
event.preventDefault();
showUploadFolderModal();
}
.on('click', function (event) {
event.preventDefault();
showUploadFolderModal();
});
triggerEnter('a.cp-app-drive-new-folderupload, li.cp-app-drive-new-folderupload', $block);
$block.find('a.cp-app-drive-new-link, li.cp-app-drive-new-link')
.on('click keypress', function (event) {
if (event.type === 'click' || (event.type === 'keypress' && event.which === 13)) {
event.preventDefault();
showLinkModal();
}
.on('click', function (event) {
event.preventDefault();
showLinkModal();
});
triggerEnter('a.cp-app-drive-new-link, li.cp-app-drive-new-link', $block);
}
$block.find('a.cp-app-drive-new-doc, li.cp-app-drive-new-doc')
.on('click auxclick keypress', function (event) {
if (event.type === 'click' || event.type === 'auxclick' || (event.type === 'keypress' && event.which === 13))
{
.on('click auxclick', function (event) {
if (event.type === 'click' || event.type === 'auxclick') {
event.preventDefault();
var type = $(this).attr('data-type') || 'pad';
var path = manager.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
openIn(type, path, APP.team);
}
});
triggerEnter('a.cp-app-drive-new-doc, li.cp-app-drive-new-doc', $block);
};
var getNewPadOptions = function (isInRoot) {
var options = [];
@ -3568,7 +3584,8 @@ define([
if (APP.$content.data('readOnlyFolder') || !APP.editable) { return; }
var isInRoot = currentPath[0] === ROOT;
var $element = $('<li>', {
'class': 'cp-app-drive-element-row cp-app-drive-new-ghost'
'class': 'cp-app-drive-element-row cp-app-drive-new-ghost',
'tabindex': 0
}).prepend($addIcon.clone()).appendTo($list);
$element.append($('<span>', {'class': 'cp-app-drive-element-name'})
.text(Messages.fm_newButton));
@ -3587,6 +3604,12 @@ define([
window.setTimeout(function () { modal.show(); });
addNewPadHandlers($modal, isInRoot);
});
$element.keydown(function(){
if (event.which === 13) {
event.stopPropagation();
$element.click();
}
});
};
// Drive content toolbar
@ -5174,24 +5197,45 @@ define([
return void deletePaths(paths);
}
else if ($this.hasClass("cp-app-drive-context-restore")) {
if (paths.length !== 1) { return; }
var restorePath = paths[0].path;
var restoreName = paths[0].path[paths[0].path.length - 1];
if (restorePath.length === 4) {
var rEl = manager.find(restorePath);
if (manager.isFile(rEl)) {
restoreName = manager.getTitle(rEl);
} else if (manager.isSharedFolder(rEl)) {
var sfData = manager.getSharedFolderData(rEl);
restoreName = sfData.title || sfData.lastTitle || Messages.fm_deletedFolder;
} else {
restoreName = restorePath[1];
let getRestoreProperties = (path) => {
let restorePath = path;
let restoreName = path.at(-1);
if (restorePath.length === 4) {
let rEl = manager.find(restorePath);
if (manager.isFile(rEl)) {
restoreName = manager.getTitle(rEl);
} else if (manager.isSharedFolder(rEl)) {
let sfData = manager.getSharedFolderData(rEl);
restoreName = sfData.title || sfData.lastTitle || Messages.fm_deletedFolder;
} else {
restoreName = restorePath[1];
}
}
return [restorePath, restoreName];
};
let restoreNumber = paths.length;
if (restoreNumber === 0) { return; }
if (restoreNumber === 1) { // single file restoration
let [restorePath, restoreName] = getRestoreProperties(paths[0].path);
UI.confirm(Messages._getKey("fm_restoreDialog", [restoreName]), res => {
if (!res) { return; }
manager.restore(restorePath, refresh);
});
} else { // multiple files restoration
UI.confirm(Messages._getKey("fm_restoreMultipleDialog", [restoreNumber]), res => {
if (!res) { return; }
nThen(waitFor => {
paths.forEach(path => {
if (!path) { // We met an error
console.error("Error while restoring files: no path");
return;
}
let restorePath = getRestoreProperties(path.path)[0];
setTimeout(manager.restore(restorePath, waitFor()), 10);
});
}).nThen(refresh);
});
}
UI.confirm(Messages._getKey("fm_restoreDialog", [restoreName]), function(res) {
if (!res) { return; }
manager.restore(restorePath, refresh);
});
}
else if ($this.hasClass("cp-app-drive-context-openparent")) {
if (paths.length !== 1) { return; }

View File

@ -10,9 +10,51 @@ define([
ext.getExtensions = id => {
let e = ext[id];
if (!Array.isArray(e)) { e = []; }
return e;
return e.map(_ext => {
return new Promise((resolve, reject) => {
// If there's no check function, resolve immediately
if (typeof _ext.check !== "function") {
return resolve(_ext);
}
const checkResult = _ext.check();
checkResult
.then((extPassed) => {
if (!extPassed) {
// Reject if the check didn't pass
undefined (resolve());
} else {
// Extension passed the check
resolve(_ext);
}
})
.catch((error) => {
reject(error);
});
});
});
};
ext.getExtensionsSync = id => {
let e = ext[id];
if (!Array.isArray(e)) { e = []; }
return e.map(_ext => {
if (typeof _ext.check !== "function") {
return _ext;
}
const extPassed = _ext.check();
if (extPassed) {
return _ext;
} else {
return null;
}
}).filter(ext => ext !== null);
};
if (!Array.isArray(Extensions) || !Extensions.length) { return ext; }
let all = Extensions.slice();

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