mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-14 11:05:41 +05:00
Merge remote-tracking branch 'origin/staging' into insert-onlyoffice-api
This commit is contained in:
commit
c37dfb1eec
@ -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
|
||||
58
.eslintrc.js
58
.eslintrc.js
@ -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'],
|
||||
}
|
||||
};
|
||||
14
.github/ISSUE_TEMPLATE/bug_resolution.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_resolution.yml
vendored
@ -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
13
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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/*
|
||||
|
||||
@ -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,
|
||||
|
||||
144
CHANGELOG.md
144
CHANGELOG.md
@ -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 don’t 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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
});
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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%));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,3 +8,8 @@
|
||||
margin: 3cm;
|
||||
size: A4 portrait;
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
18
customize.dist/translations/messages.ar.js
Normal file
18
customize.dist/translations/messages.ar.js
Normal 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;
|
||||
});
|
||||
|
||||
18
customize.dist/translations/messages.es_CU.js
Normal file
18
customize.dist/translations/messages.es_CU.js
Normal 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;
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
---
|
||||
services:
|
||||
cryptpad:
|
||||
image: "cryptpad/cryptpad:version-2024.6.0"
|
||||
image: "cryptpad/cryptpad:version-2024.12.0"
|
||||
hostname: cryptpad
|
||||
|
||||
environment:
|
||||
|
||||
@ -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
10
docs/community/README.md
Normal 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
|
||||
245
docs/community/example-advanced.caddy.conf
Normal file
245
docs/community/example-advanced.caddy.conf
Normal 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
|
||||
}
|
||||
}
|
||||
64
docs/community/example.caddy.conf
Normal file
64
docs/community/example.caddy.conf
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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.
|
||||
|
||||
@ -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
93
eslint.config.js
Normal 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",
|
||||
},
|
||||
}];
|
||||
@ -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"
|
||||
|
||||
@ -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});
|
||||
}
|
||||
|
||||
@ -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});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -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
28
lib/crypto.js
Normal 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);
|
||||
});
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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
1389
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -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",
|
||||
|
||||
@ -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 don’t 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.
|
||||
|
||||

|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
65
scripts/testcrypto.js
Normal 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');
|
||||
});
|
||||
|
||||
@ -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]
|
||||
@ -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]
|
||||
@ -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]
|
||||
@ -0,0 +1 @@
|
||||
{"owners":["TestOwner"],"validateKey":"TestKey","channel":"0","created":1721035117462}
|
||||
@ -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]
|
||||
@ -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]
|
||||
@ -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]
|
||||
@ -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]
|
||||
64
scripts/tests/test-metadata.js
Normal file
64
scripts/tests/test-metadata.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
37
server.js
37
server.js
@ -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 () {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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. ",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user