Merge branch '2024.9-rc' into ooapi

This commit is contained in:
yflory 2024-09-26 18:35:08 +02:00
commit 1b04232b55
224 changed files with 9624 additions and 3427 deletions

View File

@ -16,7 +16,7 @@ www/accounts
www/worker
www/todo
lib/plugins/
#lib/plugins/
www/common/hyperscript.js

View File

@ -35,7 +35,7 @@ module.exports = {
4
],
'linebreak-style': [
'error',
'off', // git handles linebreak conversion for us
'unix'
],
'quotes': [

View File

@ -89,6 +89,9 @@ body:
label: Version
description: What version of CryptPad are you running?
options:
- 2024.6.1
- 2024.6.0
- 2024.3.1
- 2024.3.0
- 5.7.0
- 5.6.0
@ -100,8 +103,6 @@ body:
- 5.2.0
- 5.1.0
- 5.0.0
- 4.14.1
- 4.14.0
- Other
validations:
required: true

11
.gitignore vendored
View File

@ -17,12 +17,11 @@ customize
messages.log
www/scratch
data
pins/
blob/
block/
blobstage/
block/
logs/
pins
blob
block
blobstage
logs
privileged.conf
config/config.js
config/sso.js

View File

@ -1,60 +0,0 @@
{
"fileExtensions": [".less"],
// These rules are almost certainly crap and will not catch bugs (Caleb)
"newlineAfterBlock": { "enabled": false }, // not just a newline but an entire empty line after each block
"spaceAroundOperator": { "enabled": false }, // disallow calc(10px+10px);
"hexLength": { "enabled": false }, // require long hex color codes or require short where possible
"hexNotation": { "enabled": false }, // require hex lowercase
"propertyOrdering": { "enabled": false }, // require attributes to be in alphabetical order D:
"stringQuotes": { "enabled": false }, // force quoting of strings with ' or " (silly)
"importPath": { "enabled": false }, // require imports to not have .less, ridiculous
"qualifyingElement": { "enabled": false }, // disallow div.xxx and require .xxx
"decimalZero": { "enabled": false }, // disallow .5em
"borderZero": { "enabled": false }, // disallow border: none;
"selectorNaming": { "enabled": false }, // this would be crap because classes are what they are.
"zeroUnit": { "enabled": false },
"singleLinePerProperty": { "enabled": false },
"_singleLinePerProperty": {
"enabled": true,
"allowSingleLineRules": true
},
"spaceAroundComma": { "enabled": false },
"importantRule": { "enabled": false },
"universalSelector": { "enabled": false },
"idSelector": { "enabled": false },
"singleLinePerSelector": { "enabled": false },
"spaceBetweenParens": { "enabled": false },
"maxCharPerLine": { "enabled": false }, // using lesshint flags can cause long lines
"comment": { "enabled": false }, // ban multi-line comments ?
// These rules should be discussed, if they're crap then they should be moved up.
"colorVariables": { "enabled": false }, // require all colors to be stored as variables first...
"variableValue": { "enabled": false }, // any attribute types which should always be variables ? color?
"spaceBeforeBrace": { "enabled": true },//{ "enabled": true, "style": "one_space" },
// Turn everything else on
"spaceAfterPropertyColon": { "enabled": true },
"finalNewline": { "enabled": true }, // require an empty line at the end of the file (enabled for now)
"attributeQuotes": { "enabled": true },
"depthLevel": {
"depth": 1 // TODO(cjd) This is obviously not triggering, even with 1
},
"duplicateProperty": { "enabled": false },
"emptyRule": { "enabled": true },
"hexValidation": { "enabled": true }, // disallow actual garbage color hex codes (e.g. #ab)
"propertyUnits": {
"valid": ["rem", "vw", "em", "px", "ch"], // These units are allowed for all properties
"invalid": ["pt"], // The 'pt' unit is not allowed under any circumstances
"properties": {
//"line-height": [] // No units are allowed for line-height
}
},
"spaceAfterPropertyName": { "enabled": true, "style": "no_space" },
"spaceAfterPropertyValue": { "enabled": true, "style": "no_space" },
"spaceAroundBang": { "enabled": true, "style": "before" },
"trailingSemicolon": { "enabled": true },
"trailingWhitespace": { "enabled": true },
"urlFormat": { "enabled": true, "style": "relative" },
"urlQuotes": { "enabled": true }
}

View File

@ -25,10 +25,14 @@ Files: .jshintrc
Copyright: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
License: AGPL-3.0-or-later
Files: .lesshintrc
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/*
@ -156,4 +160,4 @@ License: AGPL-3.0-or-later
Files: www/common/onlyoffice/x2t/*
Copyright: Ascensio System Limited 2010-2022
License: AGPL-3.0-or-later
License: AGPL-3.0-or-later

43
.stylelintrc.js Normal file
View File

@ -0,0 +1,43 @@
module.exports = {
"extends": "stylelint-config-standard-less",
"rules": {
"no-descending-specificity": null,
"length-zero-no-unit": null,
"no-duplicate-selectors": null,
"declaration-block-no-duplicate-properties": null,
"comment-empty-line-before": null,
"rule-empty-line-before": null,
"declaration-empty-line-before": null,
"at-rule-empty-line-before": null,
"custom-property-empty-line-before": null,
"font-family-name-quotes": null,
"font-family-no-missing-generic-family-keyword": null,
"declaration-block-no-redundant-longhand-properties": null,
"shorthand-property-no-redundant-values": null,
"declaration-block-no-shorthand-property-overrides": null,
"comment-whitespace-inside": null,
"property-no-vendor-prefix": null,
"selector-no-vendor-prefix": null,
"function-name-case": null,
"selector-class-pattern": null,
"custom-property-pattern": null,
"selector-id-pattern": null,
"selector-pseudo-element-colon-notation": null,
"media-feature-range-notation": null,
"selector-not-notation": null,
"color-function-notation": null,
"alpha-value-notation": null,
"number-max-precision": null,
"at-rule-no-unknown": null, // FIXME
"less/no-duplicate-variables": null,
"less/color-no-invalid-hex": null
}
};

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ COPY . /cryptpad
RUN sed -i "s@//httpAddress: 'localhost'@httpAddress: '0.0.0.0'@" /cryptpad/config/config.example.js
RUN sed -i "s@installMethod: 'unspecified'@installMethod: 'docker'@" /cryptpad/config/config.example.js
# Install dependencies
RUN npm install --production \
&& npm run install:components
@ -60,7 +60,7 @@ ENTRYPOINT ["/bin/bash", "/cryptpad/docker-entrypoint.sh"]
HEALTHCHECK --interval=1m CMD curl -f http://localhost:3000/ || exit 1
# Ports
EXPOSE 3000 3001 3003
EXPOSE 3000 3003
# Run cryptpad on startup
CMD ["npm", "start"]

View File

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

View File

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

View File

@ -191,7 +191,7 @@ module.exports = {
* This archived data still takes up space and so you'll probably still want to
* remove these files after a brief period.
*
* cryptpad/scripts/evict-inactive.js is intended to be run daily
* cryptpad/scripts/evict-archived.js is intended to be run daily
* from a crontab or similar scheduling service.
*
* The intent with this feature is to provide a safety net in case of accidental

View File

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

View File

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

View File

@ -9,12 +9,14 @@
* If you want to check all the configurable values, you can open the internal configuration file
but you should not change it directly (/common/application_config_internal.js)
*/
define(['/common/application_config_internal.js'], function (AppConfig) {
// Example: If you want to remove the survey link in the menu:
// AppConfig.surveyURL = "";
// To inform users of the support ticket panel which languages your admins speak:
//AppConfig.supportLanguages = [ 'en', 'fr' ];
return AppConfig;
});

View File

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

View File

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

View File

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

View File

@ -22,11 +22,13 @@ define([
'/common/outer/login-block.js',
'/common/common-hash.js',
'/common/outer/http-command.js',
'/api/config',
'/components/tweetnacl/nacl-fast.min.js',
'/components/scrypt-async/scrypt-async.min.js', // better load speed
], function ($, Listmap, Crypto, Util, NetConfig, Login, Cred, ChainPad, Realtime, Constants, UI,
Feedback, h, LocalStore, Messages, nThen, Block, Hash, ServerCommand) {
Feedback, h, LocalStore, Messages, nThen, Block, Hash, ServerCommand,
ApiConfig) {
var Exports = {
Cred: Cred,
Block: Block,
@ -218,6 +220,11 @@ define([
proxy.edPublic = result.edPublic;
}
if (ApiConfig && Array.isArray(ApiConfig.adminKeys) &&
ApiConfig.adminKeys.includes(proxy.edPublic)) {
localStorage.CP_admin = "1";
}
setTimeout(function () {
Realtime.whenRealtimeSyncs(result.realtime, function () {
proceed(result);

View File

@ -5,15 +5,18 @@
(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',
//'hi': 'हिन्दी',
'id': 'Bahasa Indonesia',
'it': 'Italiano',
'ja': '日本語',
'nb': 'Norwegian Bokmål',
@ -21,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': '中文(簡體)',
@ -44,6 +47,7 @@ var getLanguage = Messages._getLanguage = function () {
(map[l.split('_')[0]] ? l.split('_')[0] : 'en'));
};
var language = getLanguage();
window.cryptpadLanguage = language;
// Translations files were migrated from requirejs modules to json.
// To avoid asking every administrator to update their customized translation files,
@ -87,6 +91,9 @@ define(req, function(AppConfig, Default, Language) {
});
}
let html = typeof(document) !== "undefined" && document.documentElement;
if (html) { html.setAttribute('lang', language); }
var extend = function (a, b) {
for (var k in b) {
if (Array.isArray(b[k])) {
@ -128,7 +135,6 @@ define(req, function(AppConfig, Default, Language) {
}
};
return Messages;
});

View File

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

View File

@ -15,7 +15,8 @@ define([
'/common/outer/local-store.js',
'/customize/pages.js',
'/common/pad-types.js',
], function ($, Config, h, Hash, Constants, Util, TextFit, Msg, AppConfig, LocalStore, Pages, PadTypes) {
'/common/extensions.js'
], function ($, Config, h, Hash, Constants, Util, TextFit, Msg, AppConfig, LocalStore, Pages, PadTypes, Extensions) {
var urlArgs = Config.requireConf.urlArgs;
var checkEarlyAccess = function (x) {
@ -152,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 {
@ -164,9 +165,19 @@ 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));
});
});
return [
h('div#cp-main', [
Pages.infopageTopbar(),
popup,
notice,
h('div.container.cp-container', [
h('div.row.cp-home-hero', [

View File

@ -18,8 +18,6 @@ define([
return;
}
Msg.install_token = "Install token";
document.title = Msg.install_header;
var frame = function (content) {
@ -27,8 +25,7 @@ Msg.install_token = "Install token";
h('div#cp-main', [
//Pages.infopageTopbar(),
h('div.container.cp-container', [
//h('div.row.cp-page-title', h('h1', Msg.install_header)),
h('div.row.cp-page-title', h('h1', Msg.register_header)),
h('div.row.cp-page-title', h('h1', Msg.install_header)),
].concat(content)),
Pages.infopageFooter(),
]),
@ -39,17 +36,12 @@ Msg.install_token = "Install token";
h('div.row.cp-register-det', [
h('div#data.hidden.col-md-6', [
h('h2', Msg.register_notes_title),
//Pages.setHTML(h('div.cp-register-notes'), Msg.install_notes)
Pages.setHTML(h('div.cp-register-notes'), Msg.register_notes)
Pages.setHTML(h('div.cp-register-notes'), Msg.install_notes)
]),
h('div.cp-reg-form.col-md-6', [
h('div#userForm.form-group.hidden', [
h('div.cp-register-instance', [
Msg._getKey('register_instance', [ Pages.Instance.name ]),
/*h('br'),
h('a', {
href: '/features.html'
}, Msg.register_whyRegister)*/
Msg.install_instance,
]),
h('input.form-control#installtoken', {
type: 'text',
@ -75,7 +67,7 @@ Msg.install_token = "Install token";
/*h('div.checkbox-container', [
UI.createCheckbox('import-recent', Msg.register_importRecent, true)
]),*/
h('button#register', Msg.login_register)
h('button#register', Msg.install_launch)
])
]),
])

View File

@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@import (reference) "./colortheme-all.less";
@import (reference) "./forms.less";
@import (reference) './icon-colors.less';
.admin_main() {
--LessLoader_require: LessLoader_currentFile();
}
& {
// Instance accent color presets
@palette-colors:
#0087FF,
#de0064,
#8c52bc,
#3d7672;
div.cp-palette-container {
.cp-palette-nocolor {
display: none;
}
.instance-colors(@palette-colors; @index) when (@index > 0) {
// loop through the @colors
.instance-colors(@palette-colors; (@index - 1));
@color: extract(@palette-colors, @index);
// make a numbered class selector for each color
.cp-palette-color@{index} {
background-color: @color !important;
color: contrast(@color, @cryptpad_color_grey_800, @cryptpad_color_grey_200) !important;
}
}
.instance-colors(@palette-colors; length(@palette-colors));
}
.cp-admin-customize-apps-grid, .cp-admin-customize-options-grid {
display: grid;
gap: 0.5rem;
}
.cp-admin-customize-apps-grid {
grid-template-columns: 1fr 1fr 1fr;
.cp-appblock {
padding: 0.5rem;
border-radius: @variables_radius;
font-size: 1.2em;
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
.iconColors_main();
&:hover {
cursor: pointer;
}
i.cp-icon {
font-size: 2.8rem;
}
.cp-app-name {
flex-grow: 1;
}
}
.cp-inactive-app {
background-color: transparent;
opacity: 0.75;
.cp-on-enabled {
visibility: hidden;
}
}
.cp-active-app {
background-color: fade(@cryptpad_text_col, 10%);
.cp-on-enabled {
visibility: visible;
}
}
}
.cp-admin-customize-options-grid {
grid-template-columns: 1fr 1fr;
.cp-optionblock {
padding: 0.5rem;
border-radius: @variables_radius;
background-color: fade(@cryptpad_text_col, 10%);
align-self: start;
.cp-checkmark-label {
font-weight: bold;
}
.cp-option-hint {
margin-left: 30px;
display: inline-block;
}
}
}
}

View File

@ -198,6 +198,12 @@
text-decoration: none;
}
}
.cp-usergrid-user, textarea, a, .fa-times {
outline: none;
&:focus-visible {
outline: @variables_focus_style;
}
}
}
.cp-alertify-type-container {
overflow: visible !important;
@ -237,6 +243,10 @@
}
}
}
outline: none;
&:focus-visible {
outline: @variables_focus_style;
}
}
span.alertify-tabs-active {
background-color: @cp_alertify-fg !important;
@ -263,6 +273,10 @@
input {
.tools_placeholder-color();
outline: none;
&:focus-visible {
outline: @variables_focus_style;
}
}
span.cp-password-container {

View File

@ -128,9 +128,9 @@
position: absolute;
box-sizing: border-box;
}
&:focus {
box-shadow: 0px 0px 5px @cp_checkmark-back1;
outline: none;
outline: none;
&:focus-visible {
outline: @variables_focus_style;
}
}
@ -216,9 +216,9 @@
height: @checkmark-dim1;
height: var(--checkmark-dim1);
}
&:focus {
box-shadow: 0px 0px 5px @cp_checkmark-back1;
outline: none;
outline: none;
&:focus-visible {
outline: @variables_focus_style;
}
}

View File

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

View File

@ -28,6 +28,9 @@
}
&.tui-full-calendar-content {
font-size: @colortheme_app-font-size;
&:focus-visible {
outline: @variables_focus_style;
}
}
&[readonly] {
//margin-top:1rem;
@ -119,13 +122,37 @@
}
}
// The following palette container is just for the UI components
// The specific colors you want to show have to be defined in your app
// using the classes .cp-palette-nocolor .cp-palette-color1 .cp-palette-color2 etc.
div.cp-palette-container {
display: flex;
justify-content: space-between;
.cp-palette-color {
display: inline-block;
border-radius: 50%;
height: 30px;
width: 30px;
text-align: center;
line-height: 30px;
color: @cp_kanban-fg;
border: 1px solid fade(@cp_kanban-fg, 40%);
&.fa-check { // tick on selected color
color: @cryptpad_text_col;
}
outline: none;
&:focus-visible {
outline: @variables_focus_style;
}
}
}
button.btn {
background-color: @cp_buttons-cancel;
box-sizing: border-box;
outline: 0;
align-items: center;
padding: 0 6px;
line-height: 36px;
line-height: 20px;
padding: 8px 6px;
white-space: nowrap;
text-align: center;
text-transform: uppercase;
@ -232,11 +259,9 @@
}
&:focus {
//border: 1px dotted @alertify-base;
box-shadow: 0px 0px 5px @cp_buttons-primary !important;
outline: none;
outline: none;
&:focus-visible {
outline: @variables_focus_style;
}
&::-moz-focus-inner {
border: 0;

View File

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

View File

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

View File

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

View File

@ -80,6 +80,10 @@
text-overflow: ellipsis;
padding-left: 4px;
vertical-align: middle;
outline: none;
&:focus-visible {
outline: @variables_focus_style;
}
}
.close {
opacity: 1;

View File

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

View File

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

View File

@ -115,7 +115,8 @@
}
}
.fa-times {
padding-left: 5px;
border-radius: @variables_radius;
margin-left: 5px;
cursor: pointer;
height: 100%;
line-height: 25px;

View File

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

View File

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

View File

@ -255,9 +255,10 @@
* {
display: block;
}
button {
background-color: @cryptpad_color_link;
color: @cryptpad_text_col_inv;
.subscribe-btn {
text-align: center;
background-color: @cp_buttons-primary;
color: @cryptpad_text_col;
font-size: 1.4rem;
margin: 10px 0;
border-radius: @infopages-radius-L;
@ -267,8 +268,7 @@
margin-right: 10px;
}
&:hover, &:focus {
background-color: @cp_static-card-bg;
color: @cryptpad_text_col;
background-color: contrast(@cp_buttons-primary-text, darken(@cp_buttons-primary, 10%), lighten(@cp_buttons-primary, 10%));
}
}
}
@ -297,6 +297,29 @@
}
}
.cp-extensions-popups {
width: 100%;
.cp-extensions-popup {
background-color: @cp_alertify-bg;
border-radius: @infopages-radius-L;
padding: 10px;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
width: 400px;
max-width: 100%;
color: @cryptpad_text_col;
margin-left: 40px;
}
}
@media (max-width: 700px) {
.cp-extensions-popups {
max-width: 90%;
.cp-extensions-popup {
margin-left: 0;
}
}
}
@media (min-width: 576px) and (max-width: 767px) {
.container {
padding-left: 0;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.id.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

View File

@ -3,11 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
---
version: '3.8'
services:
cryptpad:
image: "cryptpad/cryptpad:version-2024.3.0"
image: "cryptpad/cryptpad:version-2024.6.1"
hostname: cryptpad
environment:
@ -30,7 +28,6 @@ services:
ports:
- "3000:3000"
- "3001:3001"
- "3003:3003"
ulimits:

View File

@ -22,16 +22,16 @@ 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
cd $CPAD_HOME
if [ "$CPAD_INSTALL_ONLYOFFICE" == "yes" ]; then
./install-onlyoffice.sh --accept-license
./install-onlyoffice.sh --accept-license --trust-repository
fi
npm run build

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

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
# This file is included strictly as an example of how Apache httpd can be
# configured to work with CryptPad. If you are using CryptPad in production
# and require professional support please contact sales@cryptpad.fr
# and require professional support please contact sales@cryptpad.org
# This configuration requires mod_ssl, mod_socache_shmcb, mod_proxy,
# mod_proxy_http and mod_headers
@ -33,8 +33,14 @@ SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
nokeepalive ssl-unclean-shutdown \
downgrade-1.0 force-response-1.0
Protocols h2 http/1.1
LimitRequestBody 157286400
AddType application/javascript mjs
ProxyPass / http://localhost:3000/ upgrade=websocket
ProxyPassReverse / http://localhost:3000/
<Location "/">
LimitRequestBody 157286400
ProxyPass http://localhost:3000/ upgrade=websocket
ProxyPassReverse http://localhost:3000/
</Location>
<Location "/cryptpad_websocket">
ProxyPass http://localhost:3003/ upgrade=websocket
ProxyPassReverse http://localhost:3003/
</Location>
</VirtualHost>

View File

@ -8,6 +8,21 @@
# installation (http server by the Nodejs process). If you are using CryptPad
# in production and require professional support please contact sales@cryptpad.fr
server {
listen 80;
listen [::]:80;
server_name your-main-domain.com your-sandbox-domain.com;
access_log /dev/null;
error_log /dev/null emerg;
# Let's Encrypt webroot
include letsencrypt-webroot;
# Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
@ -234,6 +249,20 @@ server {
add_header Cross-Origin-Embedder-Policy require-corp;
}
location ~ ^/extensions.js {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# These settings prevent both NGINX and the API server
# from setting the same headers and creating duplicates
proxy_hide_header Cross-Origin-Resource-Policy;
add_header Cross-Origin-Resource-Policy cross-origin;
proxy_hide_header Cross-Origin-Embedder-Policy;
add_header Cross-Origin-Embedder-Policy require-corp;
}
# Requests for blobs and blocks are now proxied to the API server
# This simplifies NGINX 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

View File

@ -8,6 +8,21 @@
# installation (http server by the Nodejs process). If you are using CryptPad
# in production and require professional support please contact sales@cryptpad.fr
server {
listen 80;
listen [::]:80;
server_name your-main-domain.com your-sandbox-domain.com;
access_log /dev/null;
error_log /dev/null emerg;
# Let's Encrypt webroot
include letsencrypt-webroot;
# Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
@ -71,4 +86,15 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
}
location ^~ /cryptpad_websocket {
proxy_pass http://localhost:3003;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
}
}

View File

@ -33,7 +33,7 @@ main() {
install_version v4 6ebc6938
install_version v5 88a356f0
install_version v6 abd8a309
install_version v7 ba82142f
install_version v7 e1267803
install_x2t v7.3+1 ab0c05b0e4c81071acea83f0c6a8e75f5870c360ec4abc4af09105dd9b52264af9711ec0b7020e87095193ac9b6e20305e446f2321a541f743626a598e5318c1
rm -rf "$BUILDS_DIR"
@ -69,6 +69,10 @@ parse_arguments() {
ACCEPT_LICENSE="1"
shift
;;
-t | --trust-repository)
TRUST_REPOSITORY="1"
shift
;;
*)
show_help
shift
@ -110,17 +114,26 @@ OPTIONS:
Accept the license of OnlyOffice and do not ask when running this
script. Read and accept this before using this option:
https://github.com/ONLYOFFICE/web-apps/blob/master/LICENSE.txt
-t, --trust-repository
Automatically configure the cloned onlyoffice-builds repository
as a safe.directory.
https://git-scm.com/docs/git-config/#Documentation/git-config.txt-safedirectory
EOF
exit 1
}
ensure_oo_is_downloaded() {
ensure_command_available git
ensure_command_available git
if ! [ -d "$BUILDS_DIR" ]; then
echo "Downloading OnlyOffice..."
git clone --bare https://github.com/cryptpad/onlyoffice-builds.git "$BUILDS_DIR"
fi
if ! [ -d "$BUILDS_DIR" ]; then
echo "Downloading OnlyOffice..."
git clone --bare https://github.com/cryptpad/onlyoffice-builds.git "$BUILDS_DIR"
fi
if [ ${TRUST_REPOSITORY+x} ] || [ "${PROPS[trust_repository]:-no}" == yes ]; then
git config --global --add safe.directory /cryptpad/onlyoffice-conf/onlyoffice-builds.git
fi
}
install_version() {

View File

@ -8,8 +8,10 @@ const Decrees = require("./decrees");
const nThen = require("nthen");
const Fs = require("fs");
const Fse = require("fs-extra");
const Path = require("path");
const Nacl = require("tweetnacl/nacl-fast");
const Hash = require('./common-hash');
module.exports.create = function (Env) {
var log = Env.Log;
@ -25,6 +27,41 @@ nThen(function (w) {
console.error(err);
}
}));
}).nThen(function (w) {
let admins = Env.admins || [];
// If we don't have any admin on this instance, print an onboarding link
if (Array.isArray(admins) && admins.length) { return; }
let token = Env.installToken;
let printLink = () => {
let url = `${Env.httpUnsafeOrigin}/install/#${token}`;
console.log('=============================');
console.log('Create your first admin account and customize your instance by visiting');
console.log(url);
console.log('=============================');
};
// If we already have a token, print it
if (token) { return void printLink(); }
// Otherwise create a new token
let decreeName = Path.join(Env.paths.decree, 'decree.ndjson');
token = Hash.createChannelId() + Hash.createChannelId();
let decree = ["ADD_INSTALL_TOKEN",[token],"",+new Date()];
Fs.appendFile(decreeName, JSON.stringify(decree) + '\n', w(function (err) {
if (err) { console.log(err); return; }
Env.installToken = token;
Env.envUpdated.fire();
printLink();
}));
}).nThen(function () {
if (!Env.admins.length) {
Env.Log.info('NO_ADMIN_CONFIGURED', {
message: `Your instance is not correctly configured for production usage. Review its checkup page for more information.`,
details: new URL('/checkup/', Env.httpUnsafeOrigin).href,
});
}
}).nThen(function (w) {
// we assume the server has generated a secret used to validate JWT tokens
if (typeof(Env.bearerSecret) === 'string') { return; }
@ -40,9 +77,19 @@ nThen(function (w) {
], w(function (err) {
if (err) { throw err; }
}));
}).nThen(function (w) {
Fse.mkdirp(Env.paths.block, w(function (err) {
if (err) {
log.error("BLOCK_FOLDER_CREATE_FAILED", err);
}
}));
}).nThen(function (w) {
var fullPath = Path.join(Env.paths.block, 'placeholder.txt');
Fs.writeFile(fullPath, 'PLACEHOLDER\n', w());
Fs.writeFile(fullPath, 'PLACEHOLDER\n', w(function (err) {
if (err) {
log.error('BLOCK_PLACEHOLDER_CREATE_FAILED', err);
}
}));
}).nThen(function () {
// asynchronously create a historyKeeper and RPC together
require('./historyKeeper.js').create(Env, function (err, historyKeeper) {
@ -61,7 +108,7 @@ nThen(function (w) {
};
// spawn ws server and attach netflux event handlers
NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer}))
let Server = NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer}))
.on('channelClose', historyKeeper.channelClose)
.on('channelMessage', historyKeeper.channelMessage)
.on('channelOpen', historyKeeper.channelOpen)
@ -90,6 +137,68 @@ nThen(function (w) {
});
})
.register(historyKeeper.id, historyKeeper.directMessage);
// Store max active WS during the last day (reset when sending ping if enabled)
setInterval(() => {
try {
// Concurrent usage data
let oldWs = Env.maxConcurrentWs || 0;
let oldUniqueWs = Env.maxConcurrentUniqueWs || 0;
let oldChans = Env.maxActiveChannels || 0;
let oldUsers = Env.maxConcurrentRegUsers || 0;
let stats = Server.getSessionStats();
let chans = Server.getActiveChannelCount();
let reg = 0;
let regKeys = [];
Object.keys(Env.netfluxUsers).forEach(id => {
let keys = Env.netfluxUsers[id];
let key = Object.keys(keys || {})[0];
if (!key) { return; }
if (regKeys.includes(key)) { return; }
reg++;
regKeys.push(key);
});
Env.maxConcurrentWs = Math.max(oldWs, stats.total);
Env.maxConcurrentUniqueWs = Math.max(oldUniqueWs, stats.unique);
Env.maxConcurrentRegUsers = Math.max(oldUsers, reg);
Env.maxActiveChannels = Math.max(oldChans, chans);
} catch (e) {}
}, 10000);
// Clean up active registered users and channels (possible memory leak)
setInterval(() => {
try {
let users = Env.netfluxUsers || {};
let online = Server.getOnlineUsers() || [];
let removed = 0;
Object.keys(users).forEach(id => {
if (!online.includes(id)) {
delete users[id];
removed++;
}
});
if (removed) {
Env.Log.info("CLEANED_ACTIVE_USERS_MAP", {removed});
}
} catch (e) {}
try {
let HK = require('./hk-utils');
let chans = Env.channel_cache || {};
let active = Server.getActiveChannels() || [];
let removed = 0;
Object.keys(chans).forEach(id => {
if (!active.includes(id)) {
HK.dropChannel(Env, id);
removed++;
}
});
if (Env.store) {
Env.store.closeInactiveChannels(active);
}
if (removed) {
Env.Log.info("CLEANED_ACTIVE_CHANNELS_MAP", {removed});
}
} catch (e) {}
}, 30000);
});
});

View File

@ -101,7 +101,7 @@ var shutdown = function (Env, Server, cb) {
// and allow system functionality to restart the server
};
var getRegisteredUsers = function (Env, Server, cb) {
var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) {
Env.batchRegisteredUsers('', cb, function (done) {
var dir = Env.paths.pin;
var folders;
@ -119,11 +119,15 @@ var getRegisteredUsers = function (Env, Server, cb) {
var dir = Env.paths.pin + '/' + f;
Fs.readdir(dir, waitFor(function (err, list) {
if (err) { return; }
// Don't count placeholders
list = list.filter(name => {
return !/\.placeholder$/.test(name);
});
users += list.length;
}));
});
}).nThen(function () {
done(void 0, users);
done(void 0, {users});
});
});
};
@ -466,6 +470,8 @@ var setLastEviction = function (Env, Server, cb, data, unsafeKey) {
// CryptPad_AsyncStore.rpc.send('ADMIN', ['INSTANCE_STATUS], console.log)
var instanceStatus = function (Env, Server, cb) {
cb(void 0, {
appsToDisable: Env.appsToDisable,
restrictRegistration: Env.restrictRegistration,
restrictSsoRegistration: Env.restrictSsoRegistration,
dontStoreSSOUsers: Env.dontStoreSSOUsers,

View File

@ -10,6 +10,8 @@ const Https = require("https");
const Http = require("http");
const Util = require("../common-util");
const Stats = require("../stats");
const Admin = require("./admin-rpc.js");
const nThen = require('nthen');
var validLimitFields = ['limit', 'plan', 'note', 'users', 'origin'];
@ -111,45 +113,83 @@ var queryAccountServer = function (Env, cb) {
var done = Util.once(Util.mkAsync(cb));
var rawBody = Stats.instanceData(Env);
Env.Log.info("SERVER_TELEMETRY", rawBody);
var body = JSON.stringify(rawBody);
var options = {
host: 'accounts.cryptpad.fr',
path: '/api/getauthorized',
method: 'POST',
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
}
let send = () => {
Env.Log.info("SERVER_TELEMETRY", rawBody);
var body = JSON.stringify(rawBody);
var options = {
host: 'accounts.cryptpad.fr',
path: '/api/getauthorized',
method: 'POST',
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
}
};
var req = Https.request(options, function (response) {
if (!('' + response.statusCode).match(/^2\d\d$/)) {
return void cb('SERVER ERROR ' + response.statusCode);
}
var str = '';
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
try {
var json = JSON.parse(str);
checkUpdateAvailability(Env, json);
done(void 0, json);
} catch (e) {
done(e);
}
});
});
req.on('error', function () {
done();
});
req.end(body);
};
var req = Https.request(options, function (response) {
if (!('' + response.statusCode).match(/^2\d\d$/)) {
return void cb('SERVER ERROR ' + response.statusCode);
}
var str = '';
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
try {
var json = JSON.parse(str);
checkUpdateAvailability(Env, json);
done(void 0, json);
} catch (e) {
done(e);
if (Env.provideAggregateStatistics) {
let stats = {};
nThen(waitFor => {
Admin.getRegisteredUsers(Env, null, waitFor((err, data) => {
if (err) { return; }
stats.registered = data.users;
if (Env.lastPingRegisteredUsers) {
stats.usersDiff = stats.registered - Env.lastPingRegisteredUsers;
}
Env.lastPingRegisteredUsers = stats.registered;
}));
}).nThen(() => {
if (Env.maxConcurrentWs) {
stats.maxConcurrentWs = Env.maxConcurrentWs;
Env.maxConcurrentWs = 0;
}
if (Env.maxConcurrentUniqueWs) {
stats.maxConcurrentUniqueIPs = Env.maxConcurrentUniqueWs;
Env.maxConcurrentUniqueWs = 0;
}
if (Env.maxConcurrentRegUsers) {
stats.maxConcurrentRegUsers = Env.maxConcurrentRegUsers;
Env.maxConcurrentRegUsers = 0;
}
if (Env.maxActiveChannels) {
stats.maxConcurrentChannels = Env.maxActiveChannels;
Env.maxActiveChannels = 0;
}
rawBody.statistics = stats;
send();
});
});
req.on('error', function () {
done();
});
req.end(body);
return;
}
send();
};
Quota.shouldContactServer = function (Env) {
return !(Env.blockDailyCheck === true ||

6
lib/common-hash.js Normal file
View File

@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
module.exports = require("../www/common/common-hash");

View File

@ -220,6 +220,15 @@ commands.SET_SUPPORT_MAILBOX = makeGenericSetter('supportMailbox', function (arg
return args_isString(args) && Core.isValidPublicKey(args[0]);
});
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SUPPORT_KEYS', ["Tdz6+fE9N9XXBY93rW5qeNa/k27yd40c0vq7EJyt7jA=", "Tdz6+fE9N9XXBY93rW5qeNa/k27yd40c0vq7EJyt7jA="]]], console.log)
commands.DISABLE_APPS = function (Env, args) {
if (!Array.isArray(args)) { throw new Error("INVALID_ARGS"); }
if (JSON.stringify(args) === JSON.stringify(Env.appsToDisable)) { return false; }
Env.appsToDisable = args;
return true;
};
commands.SET_SUPPORT_KEYS = function (Env, args) {
const curvePublic = args[0]; // Support mailbox key
const edPublic = args[1]; // Support pin log
@ -232,7 +241,7 @@ commands.SET_SUPPORT_KEYS = function (Env, args) {
Env.supportMailboxKey = curvePublic;
Env.supportPinKey = edPublic;
return true;
};
};
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_PURPOSE', ["development"]]], console.log)
commands.SET_INSTANCE_PURPOSE = makeGenericSetter('instancePurpose', args_isString);
@ -290,7 +299,7 @@ var args_isMaintenance = function (args) {
// whenever that happens we can relax validation a bit to support more formats
var makeBroadcastSetter = function (attr, validation) {
return function (Env, args) {
if ((validation && !validation(args)) && !args_isString(args)) {
if ((validation && !validation(args)) && !args_isString(args)) {
throw new Error('INVALID_ARGS');
}
var str = args[0];

View File

@ -146,6 +146,7 @@ const dropChannel = HK.dropChannel = function (Env, chanName) {
expireChannel(Env, chanName);
}, TEMPORARY_CHANNEL_LIFETIME);
}
if (Env.store) { Env.store.closeChannel(chanName, function () {}); }
};
/* checkExpired

View File

@ -595,6 +595,7 @@ var serveConfig = makeRouteCache(function () {
maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize,
restrictRegistration: Env.restrictRegistration,
appsToDisable: Env.appsToDisable,
restrictSsoRegistration: Env.restrictSsoRegistration,
httpSafeOrigin: Env.httpSafeOrigin,
enableEmbedding: Env.enableEmbedding,
@ -630,6 +631,35 @@ var serveBroadcast = makeRouteCache(function () {
app.get('/api/config', serveConfig);
app.get('/api/broadcast', serveBroadcast);
(function () {
let extensions = plugins._extensions;
let styles = plugins._styles;
let str = JSON.stringify(extensions);
let str2 = JSON.stringify(styles);
let js = `let extensions = ${str};
let styles = ${str2};
let lang = window.cryptpadLanguage;
let paths = [];
extensions.forEach(name => {
paths.push(\`optional!/\${name}/extensions.js\`);
paths.push(\`optional!json!/\${name}/translations/messages.json\`);
paths.push(\`optional!json!/\${name}/translations/messages.\${lang}.json\`);
});
styles.forEach(name => {
paths.push(\`optional!less!/\${name}/style.less\`);
});
define(paths, function () {
let args = Array.prototype.slice.apply(arguments);
return args;
}, function () {
// ignore missing files
});`;
app.get('/extensions.js', (req, res) => {
res.setHeader('Content-Type', 'text/javascript');
res.send(js);
});
})();
var Define = function (obj) {
return `define(function (){
return ${JSON.stringify(obj, null, '\t')};
@ -723,7 +753,7 @@ app.post('/api/auth', function (req, res, next) {
});
app.use(function (req, res /*, next */) {
if (/^(\/favicon\.ico\/|.*\.js\.map)$/.test(req.url)) {
if (/^(\/favicon\.ico\/|.*\.js\.map|.*\/translations\/.*\.json)/.test(req.url)) {
// ignore common 404s
} else {
Log.info('HTTP_404', req.url);

View File

@ -4,6 +4,8 @@
const fs = require('node:fs');
const plugins = {};
const extensions = plugins._extensions = [];
const styles = plugins._styles = [];
try {
let pluginsDir = fs.readdirSync(__dirname + '/plugins');
@ -12,6 +14,18 @@ try {
try {
let plugin = require(`./plugins/${name}/index`);
plugins[plugin.name] = plugin.modules;
try {
let hasExt = fs.existsSync(`lib/plugins/${name}/client/extensions.js`);
if (hasExt) {
extensions.push(plugin.name.toLowerCase());
}
} catch (e) {}
try {
let hasStyle = fs.existsSync(`lib/plugins/${name}/client/style.less`);
if (hasStyle) {
styles.push(plugin.name.toLowerCase());
}
} catch (e) {}
} catch (err) {
console.error(err);
}

View File

@ -81,8 +81,8 @@ Stats.instanceData = function (Env) {
if (Env.provideAggregateStatistics) {
// check how many instances provide stats before we put more work into it
data.providesAggregateStatistics = true;
data.statistics = {}; // Filled in lib/commands/quota.js because of async calls
}
return data;
};

View File

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

2814
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowledge server",
"version": "2024.3.0",
"description": "a collaborative office suite that is end-to-end encrypted and open-source",
"version": "2024.9.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
@ -20,9 +20,9 @@
"bootstrap-tokenfield": "^0.12.0",
"chainpad": "^5.2.6",
"chainpad-crypto": "^0.2.5",
"chainpad-listmap": "^1.0.0",
"chainpad-netflux": "^1.0.0",
"chainpad-server": "^5.2.0",
"chainpad-listmap": "^1.1.0",
"chainpad-netflux": "^1.2.0",
"chainpad-server": "^5.2.2",
"ckeditor": "npm:ckeditor4@~4.22.1",
"codemirror": "^5.19.0",
"components-font-awesome": "^4.6.3",
@ -30,7 +30,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.20.0",
"file-saver": "1.3.1",
"fs-extra": "^7.0.0",
"get-folder-size": "^2.0.1",
@ -44,7 +44,7 @@
"localforage": "^1.5.2",
"marked": "^4.3.0",
"mathjax": "3.0.5",
"netflux-websocket": "^1.0.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",
@ -63,19 +63,19 @@
"thirty-two": "^1.0.2",
"tweetnacl": "~0.12.2",
"ulimit": "0.0.2",
"ws": "^3.3.1",
"ws": "^8.17.1",
"x2js": "^3.4.4"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-plugin-compat": "^4.2.0",
"lesshint": "6.3.7"
"stylelint": "^16.6.1",
"stylelint-config-standard-less": "^3.0.1"
},
"overrides": {
"glob-parent": "5.1.2",
"set-value": "4.0.1",
"minimist": "~1.2.3",
"minimatch": "~3.1.2",
"ws": "^8.17.1",
"jquery": "3.6.0"
},
"scripts": {
@ -86,9 +86,9 @@
"offline": "FRESH=1 OFFLINE=1 node server.js",
"offlinedev": "DEV=1 OFFLINE=1 node server.js",
"package": "PACKAGE=1 node server.js",
"lint": "eslint . && ./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
"lint": "eslint . && stylelint \"./customize.dist/src/less2/**/*.less\"",
"lint:js": "eslint .",
"lint:less": "./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/",
"lint:less": "stylelint \"./customize.dist/src/less2/**/*.less\"",
"lint:translations": "node ./scripts/translations/lint-translations.js",
"unused-translations": "node ./scripts/translations/unused-translations.js",
"test": "node scripts/TestSelenium.js",
@ -98,5 +98,7 @@
"clear": "node scripts/clear.js",
"installtoken": "node scripts/install.js"
},
"browserslist": ["> 0.5%, last 2 versions, Firefox ESR, not dead, not op_mini all"]
"browserslist": [
"> 0.5%, last 2 versions, Firefox ESR, not dead, not op_mini all"
]
}

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
# CryptPad
CryptPad is a collaboration suite that is end-to-end-encrypted and open-source. It is built to enable collaboration, synchronizing changes to documents in real time. Because all data is encrypted, the service and its administrators have no way of seeing the content being edited and stored.
CryptPad is a collaboration suite that is end-to-end-encrypted and open-source. It is built to enable collaboration, synchronizing changes to documents in real time. Because all data are encrypted, in the eventuality of a breach, attackers have no way of seeing the stored content. Moreover, if the administrators dont alter the code, they and the service also cannot infer any piece of information about the users' content.
![Drive screenshot](screenshot.png "preview of the CryptDrive")
@ -24,11 +24,11 @@ Configuring CryptPad for production requires a little more work, but the process
## Current version
The most recent version and all past release notes can be found [here](https://github.com/cryptpad/cryptpad/releases/).
The most recent version and all past release notes can be found on the [releases page on GitHub](https://github.com/cryptpad/cryptpad/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.
@ -36,7 +36,7 @@ Previously, Docker images were community maintained, had their own repository an
CryptPad offers a variety of collaborative tools that encrypt your data in your browser
before it is sent to the server and your collaborators. In the event that the server is
compromized the database holds encrypted data that is not of much value to attackers.
compromized, the database holds encrypted data that is not of much value to attackers.
The code which performs the encryption is still loaded from the host server like any
other web page, so you still need to trust the administrator to keep their server secure
@ -44,23 +44,29 @@ and to send you the right code. An expert can download code from the server and
that it isn't doing anything malicious like leaking your encryption keys, which is why
this is considered an [active attack].
The platform is designed to minimize what data is exposed to its operators. User registration
and account access is based on a cryptographic key that is derived from your username
and password so the server never needs to see either and you don't need to worry about
whether they are being stored securely. It is impossible to verify whether a server's
operators are logging your IP or other activity, so if you consider this information
sensitive it is safest to assume it is being recorded and access your preferred instance
via [Tor browser].
The platform is designed to minimize what data is exposed to its operators. User
registration and account access are based on cryptographic keys that are derived from your
username and password. Hence, the server never needs to see either, and you don't need to
worry about whether they are being stored securely. It is impossible to verify whether a
server's operators are logging your IP or other activity, so if you consider this
information sensitive it is safest to assume it is being recorded and access your
preferred instance via [Tor browser].
A correctly configured instance has safeguards to prevent collaborators from doing some
nasty things like injecting scripts into collaborative documents or uploads. The project
is actively maintained and bugs that our safeguards don't catch tend to get fixed quickly.
For this reason it is best to only use instances that are running the most recent version,
which is currently on a three-week release cycle. It is difficult for a non-expert to
which is currently on a three-month release cycle. It is difficult for a non-expert to
determine whether an instance is otherwise configured correctly, so we are actively
working on allowing administrators to opt in to a public directory of servers that
working on allowing administrators to opt in to a [public directory of
servers](https://cryptpad.org/instances/) that
meet our strict criteria for safety.
For end users, a [guide](https://blog.cryptpad.org/2024/03/14/Most-Secure-CryptPad-Usage/)
is provided in our blog to help understand the security of CryptPad. This blog post
also explains and show the best practices when using CryptPad and clarify what end-to-end
encryption entails and not.
# Translations
CryptPad can be translated with nothing more than a web browser via our

View File

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

View File

@ -40,5 +40,4 @@ nThen(function (w) {
console.log(token);
var url = config.httpUnsafeOrigin + '/install/';
console.log(`Please visit ${url} to create your first admin user`);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -68,13 +68,6 @@ nThen(function (w) {
Env.Log.info("WEBSERVER_LISTENING", {
origin: url,
});
if (!Env.admins.length) {
Env.Log.info('NO_ADMIN_CONFIGURED', {
message: `Your instance is not correctly configured for production usage. Review its checkup page for more information.`,
details: new URL('/checkup/', Env.httpUnsafeOrigin).href,
});
}
} catch (err) {
Env.Log.error("INVALID_ORIGIN", {
httpUnsafeOrigin: Env.httpUnsafeOrigin,

View File

@ -9,29 +9,77 @@
@import (reference) "../../customize/src/less2/include/creation.less";
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/export.less';
@import (reference) '../../customize/src/less2/include/admin.less';
&.cp-app-admin {
.framework_min_main();
.sidebar-layout_main();
.limit-bar_main();
.creation_main();
.admin_main();
display: flex;
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;
border-radius: 5px;
background-color: @cryptpad_color_brand;
}
input.cp-admin-color-picker {
vertical-align: middle;
}
.cp-palette-container {
display: inline-flex;
width: ~"calc(100% - 5rem)";
padding-left: 0.5rem;
vertical-align: middle;
}
.cp-admin-color-preview {
& > div {
margin-top: @sidebar_base-margin;

View File

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

View File

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

View File

@ -5,6 +5,7 @@
define([
'jquery',
'/common/toolbar.js',
'/common/pad-types.js',
'/components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-interface.js',
@ -17,11 +18,11 @@ define([
'/common/hyperscript.js',
'/common/clipboard.js',
'json.sortify',
'/customize/application_config.js',
'/api/config',
'/api/instance',
'/lib/datepicker/flatpickr.js',
'/common/hyperscript.js',
'/install/onboardscreen.js',
'css!/lib/datepicker/flatpickr.min.css',
'css!/components/bootstrap/dist/css/bootstrap.min.css',
'css!/components/components-font-awesome/css/font-awesome.min.css',
@ -29,6 +30,7 @@ define([
], function(
$,
Toolbar,
PadTypes,
nThen,
SFCommon,
UI,
@ -41,11 +43,12 @@ define([
h,
Clipboard,
Sortify,
AppConfig,
ApiConfig,
Instance,
Flatpickr
Flatpickr,
Onboarding,
) {
var APP = window.APP = {};
var Nacl = window.nacl;
@ -90,6 +93,12 @@ define([
'forcemfa',
]
},
'apps': { // Msg.admin_cat_apps
icon: 'fa fa-wrench',
content: [
'apps',
]
},
'users' : { // Msg.admin_cat_users
icon : 'fa fa-address-card-o',
content : [
@ -162,6 +171,22 @@ define([
const blocks = sidebar.blocks;
// EXTENSION_POINT:ADMIN_CATEGORY
common.getExtensions('ADMIN_CATEGORY').forEach(ext => {
if (!ext || !ext.id || !ext.name || !ext.content) {
return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext);
}
if (categories[ext.id]) {
return console.error('Extension point ID already used', ext);
}
console.error(ext);
categories[ext.id] = {
icon: ext.icon,
name: ext.name,
content: ext.content
};
});
const flushCache = (cb) => {
cb = cb || function () {};
sFrameChan.query('Q_ADMIN_RPC', {
@ -553,23 +578,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);
@ -609,7 +649,6 @@ define([
UI.log(Messages._getKey('ui_saved', [Messages.admin_emailTitle]));
});
});
var nav = blocks.nav([button]);
var form = blocks.form([
@ -621,6 +660,35 @@ define([
cb(form);
});
sidebar.addItem('apps', function (cb) {
const appsToDisable = ApiConfig.appsToDisable || [];
const grid = Onboarding.createAppsGrid(appsToDisable);
var save = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['DISABLE_APPS', appsToDisable]
}, function (e, response) {
if (e || response.error) {
UI.warn(Messages.error);
console.error(e, response);
done(false);
return;
}
flushCache();
done(true);
UI.log(Messages._getKey('ui_saved', [Messages.admin_appSelection]));
});
});
let form = blocks.form([
grid
], blocks.nav([save]));
cb(form);
});
sidebar.addItem('instance-info-notice', function(cb){
var key = 'instance-info-notice';
var notice = blocks.alert('info', key, [Messages.admin_infoNotice1, ' ', Messages.admin_infoNotice2]);
@ -790,7 +858,7 @@ define([
var currentContainer = blocks.block([], 'cp-admin-customize-logo');
let redraw = () => {
var current = h('img', {src: '/api/logo?'+(+new Date())});
var current = h('img', {src: '/api/logo?'+(+new Date()),alt:'Custom logo'}); // XXX
$(currentContainer).empty().append(current);
};
redraw();
@ -905,7 +973,7 @@ define([
setColor(color, done);
});
let $input = $(input).on('change', () => {
let onColorPicked = () => {
require(['/lib/less.min.js'], (Less) => {
let color = $input.val();
let lColor = Less.color(color.slice(1));
@ -925,7 +993,8 @@ define([
$preview.find('.cp-admin-color-preview-dark a').attr('style', `color: ${lightColor} !important`);
$preview.find('.cp-admin-color-preview-light a').attr('style', `color: ${color} !important`);
});
});
};
let $input = $(input).on('change', onColorPicked).addClass('cp-admin-color-picker');
UI.confirmButton($remove, {
classes: 'btn-danger',
@ -935,9 +1004,18 @@ define([
setColor('', () => {});
});
var colors = UIElements.makePalette(4, (color, $color) => {
// onselect
let rgb = $color.css('background-color');
let hex = Util.rgbToHex(rgb);
$input.val(hex);
onColorPicked();
});
$(label).append(colors);
let form = blocks.form([
labelCurrent,
label
label,
], blocks.nav([btn, remove, btn.spinner]));
cb([form, labelPreview]);
@ -1038,6 +1116,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([
@ -1138,7 +1219,7 @@ define([
});
});
cb([form, list]);
cb([form, div]);
});
var getBlockId = (val) => {
@ -1350,6 +1431,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]);
@ -1518,7 +1602,7 @@ define([
});
});
cb([form, list]);
cb([form, div]);
});
// Msg.admin_defaultlimitHint, .admin_defaultlimitTitle
@ -1678,6 +1762,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 () {
@ -1735,7 +1822,7 @@ define([
});
};
APP.refreshLimits();
cb(table);
cb(div);
});
// Msg.admin_accountMetadataHint.admin_accountMetadataTitle
@ -2647,9 +2734,10 @@ define([
var onRefresh = function () {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'REGISTERED_USERS',
}, function (e, data) {
}, function (e, arr) {
pre.innerText = '';
pre.append(String(data));
let data = arr[0];
pre.append(String(data.users));
});
};
onRefresh();
@ -3224,10 +3312,6 @@ define([
$active.empty();
if (Broadcast && Broadcast.surveyURL) {
var a = blocks.link(Messages.admin_surveyActive, Broadcast.surveyURL);
$(a).click(function (e) {
e.preventDefault();
common.openUnsafeURL(Broadcast.surveyURL);
});
$active.append([a, removeButton]);
}
});
@ -3534,6 +3618,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', {
@ -3566,7 +3653,7 @@ define([
onRefresh();
onRefreshPerformance.reg(onRefresh);
cb(table);
cb(div);
});
@ -3835,6 +3922,32 @@ define([
cb(opts);
});
// EXTENSION_POINT:ADMIN_ITEM
let utils = {
h, Util, Hash
};
common.getExtensions('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);
});
}, {
noTitle: !ext.title,
noHint: !ext.description,
title: ext.title,
hint: ext.description
});
});
sidebar.makeLeftside(categories);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2063,6 +2063,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);
@ -2147,6 +2156,13 @@ APP.recurrenceRule = {
$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 +2225,7 @@ APP.recurrenceRule = {
setFormat(allDay);
});
});
UI.addTabListener(el);
};
var onCalendarEditPopup = function (el) {
var $el = $(el);
@ -2244,6 +2261,7 @@ 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 |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
@ -240,11 +240,15 @@ define([
if (!(tab.content || tab.disabled) || !tab.title) { return; }
var content = h('div.alertify-tabs-content', tab.content);
var title = h('span.alertify-tabs-title'+ (tab.disabled ? '.disabled' : ''), h('span.tab-title-text',{id: 'cp-tab-' + tab.title.toLowerCase(), 'aria-hidden':"true"}, tab.title));
$(title).attr('tabindex', '0');
if (tab.icon) {
var icon = h('i', {class: tab.icon, 'aria-labelledby': 'cp-tab-' + tab.title.toLowerCase()});
$(title).prepend(' ').prepend(icon);
}
$(title).click(function () {
Util.onClickEnter($(title), function (event) {
event.preventDefault();
event.stopPropagation();
if (tab.disabled) { return; }
var old = tabs[active];
if (old.onHide) { old.onHide(); }
@ -300,14 +304,17 @@ define([
var $root = $t.parent();
var $input = $root.find('.token-input');
$input.attr('tabindex', 0);
var $button = $(h('button.btn.btn-primary', [
h('i.fa.fa-plus'),
h('span', Messages.tag_add)
]));
$button.click(function () {
Util.onClickEnter($button, function (e) {
$t.tokenfield('createToken', $input.val());
e.stopPropagation();
});
var $container = $(h('span.cp-tokenfield-container'));
@ -325,27 +332,47 @@ define([
if (!$tokens.length) {
$container.prepend(h('span.tokenfield-empty', Messages.kanban_noTags));
}
$tokens.find('.close').attr('tabindex', 0).on('keydown', e => {
e.stopPropagation();
});
$tokens.find('.token-label').attr('tabindex', 0).on('keydown', function (e) {
if (e.which === 13 || e.which === 32) {
$(this).dblclick();
}
e.stopPropagation();
});
$form.append($input);
$form.append($button);
if (isEdit) { $button.find('span').text(Messages.tag_edit); }
else { $button.find('span').text(Messages.add); }
$container.append($form);
$input.focus();
isEdit = false;
called = false;
});
};
resetUI();
const focusInput = () => {
let active = document.activeElement;
if ($.contains($container[0], active)) {
setTimeout(() => {
$input.focus();
});
}
};
$t.on('tokenfield:removedtoken', function () {
resetUI();
focusInput();
});
$t.on('tokenfield:editedtoken', function () {
resetUI();
focusInput();
});
$t.on('tokenfield:createdtoken', function () {
$input.val('');
resetUI();
focusInput();
});
$t.on('tokenfield:edittoken', function () {
isEdit = true;
@ -486,7 +513,7 @@ define([
var navs = [];
buttons.forEach(function (b) {
if (!b.name || !b.onClick) { return; }
var button = h('button', { tabindex: '1', 'class': b.className || '' }, [
var button = h('button', { 'class': b.className || '' }, [
b.iconClass ? h('i' + b.iconClass) : undefined,
b.name
]);
@ -509,7 +536,8 @@ define([
divClasses: 'left'
}, todo);
} else {
$(button).click(function () {
Util.onClickEnter($(button), function (e) {
e.stopPropagation();
todo();
});
}
@ -548,6 +576,40 @@ define([
if (opt.forefront) { $(frame).addClass('forefront'); }
return 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)');
if (modalElements.length === 0) {
// there are no focusable elements -> nothing to do for us here
return;
}
// intialize with focus on first element
modalElements[0].focus();
$(frame).on('keydown', function (e) {
modalElements = $(frame).find('a, button, input, [tabindex]:not([tabindex="-1"]), textarea').filter(':visible').filter(':not(:disabled)'); // for modals with dynamic content
if (e.which === 9) { // Tab
if (e.shiftKey) {
// On the first element, shift+tab goes to last
if (document.activeElement === modalElements[0]) {
e.preventDefault();
modalElements[modalElements.length - 1].focus();
}
} else {
// On the last element, tab goes to first
if (document.activeElement === modalElements[modalElements.length - 1]) {
e.preventDefault();
modalElements[0].focus();
}
}
}
});
};
UI.openCustomModal = function (content, opt) {
var frame = dialog.frame([
content
@ -564,6 +626,9 @@ define([
setTimeout(function () {
Notifier.notify();
});
addTabListener(frame);
return frame;
};
@ -605,6 +670,7 @@ define([
$modal: $blockContainer,
show: function () {
$blockContainer.css('display', 'flex');
addTabListener($blockContainer);
},
hide: hide
};
@ -654,6 +720,7 @@ define([
Notifier.notify();
});
addTabListener(frame);
return {
element: frame,
delete: close
@ -705,7 +772,7 @@ define([
document.body.appendChild(frame);
setTimeout(function () {
$(input).select().focus();
addTabListener(frame);
Notifier.notify();
});
};
@ -713,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); }
@ -742,13 +808,20 @@ define([
var $ok = $(ok).click(function (ev) { close(true, ev); });
var $cancel = $(cancel).click(function (ev) { close(false, ev); });
document.body.appendChild(frame);
addTabListener(frame);
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();
}, frame);
document.body.appendChild(frame);
setTimeout(function () {
Notifier.notify();
$(frame).find('.ok').focus();
@ -815,15 +888,19 @@ define([
};
var newCls2 = config.new ? 'new' : '';
$(originalBtn).addClass('cp-button-confirm-placeholder').addClass(newCls2).click(function (e) {
e.stopPropagation();
// If we have a validation function, continue only if it's true
if (config.validate && !config.validate()) { return; }
i = 1;
to = setTimeout(todo, INTERVAL);
$(originalBtn).hide().after(content);
$(originalBtn).addClass('cp-button-confirm-placeholder').addClass(newCls2).on('click keydown', function (e) {
if (e.type === 'click' || (e.type === 'keydown' && e.key === 'Enter')) {
e.stopPropagation();
// If we have a validation function, continue only if it's true
if (config.validate && !config.validate()) { return; }
i = 1;
to = setTimeout(todo, INTERVAL);
$(originalBtn).hide().after(content);
$(button).focus();
}
});
return {
reset: function () {
done(false);
@ -877,12 +954,14 @@ define([
opts = opts || {};
var attributes = merge({
type: 'password',
tabindex: '1',
tabindex: '0',
autocomplete: 'one-time-code', // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values
}, opts);
var input = h('input.cp-password-input', attributes);
var eye = h('span.fa.fa-eye.cp-password-reveal');
var eye = h('span.fa.fa-eye.cp-password-reveal', {
tabindex: 0
});
var $eye = $(eye);
var $input = $(input);
@ -899,7 +978,8 @@ define([
$input.focus();
});
} else {
$eye.click(function () {
Util.onClickEnter($eye, function (e) {
e.stopPropagation();
if ($eye.hasClass('fa-eye')) {
$input.prop('type', 'text');
$input.focus();
@ -925,7 +1005,8 @@ define([
title: text,
href: href,
target: "_blank",
'data-tippy-placement': "right"
'data-tippy-placement': "right",
'aria-label': Messages.help_genericMore //TBC XXX
});
return q;
};
@ -1064,6 +1145,21 @@ define([
}
};
UI.getNewIcon = function (type) {
var icon = h('i.fa.fa-file-text-o');
if (AppConfig.applicationsIcon && AppConfig.applicationsIcon[type]) {
icon = AppConfig.applicationsIcon[type];
var font = icon.indexOf('cptools') === 0 ? 'cptools' : 'fa';
if (type === 'fileupload') { type = 'file'; }
if (type === 'folderupload') { type = 'file'; }
if (type === 'link') { type = 'drive'; }
var appClass = ' cp-icon cp-icon-color-'+type;
icon = h('i', {'class': font + ' ' + icon + appClass});
}
return icon;
};
var $defaultIcon = $('<span>', {"class": "fa fa-file-text-o"});
UI.getIcon = function (type) {
var $icon = $defaultIcon.clone();
@ -1205,18 +1301,18 @@ define([
if (labelOpts.class) { labelOpts.class += ' cp-checkmark'; }
// Mark properties
var markOpts = { tabindex: 0 };
var markOpts = { tabindex: 0, role: 'checkbox', 'aria-checked': checked, 'aria-labelledby': inputOpts.id + '-label' };
$.extend(markOpts, opts.mark || {});
var input = h('input', inputOpts);
var $input = $(input);
var mark = h('span.cp-checkmark-mark', markOpts);
var $mark = $(mark);
var label = h('span.cp-checkmark-label', labelTxt);
var label = h('span.cp-checkmark-label', {id: inputOpts.id + '-label'}, labelTxt);
$mark.keydown(function (e) {
if ($input.is(':disabled')) { return; }
if (e.which === 32) {
if (e.which === 32 || e.which === 13){
e.stopPropagation();
e.preventDefault();
$input.prop('checked', !$input.is(':checked'));
@ -1228,8 +1324,10 @@ define([
if (!opts.labelAlt) { return; }
if ($input.is(':checked') !== checked) {
$(label).text(opts.labelAlt);
$mark.attr('aria-checked', 'true');
} else {
$(label).text(labelTxt);
$mark.attr('aria-checked', 'false');
}
});
@ -1267,7 +1365,7 @@ define([
$(mark).keydown(function (e) {
if ($input.is(':disabled')) { return; }
if (e.which === 32) {
if (e.which === 13 || e.which === 32) {
e.stopPropagation();
e.preventDefault();
if ($input.is(':checked')) { return; }

View File

@ -173,8 +173,12 @@ define([
var removeBtn, el;
if (config.remove) {
removeBtn = h('span.fa.fa-times');
$(removeBtn).click(function () {
config.remove(el);
$(removeBtn).attr('tabindex', '0');
$(removeBtn).on('click keydown', function(event) {
if (event.type === 'click' || (event.type === 'keydown' && event.key === 'Enter')) {
event.preventDefault();
config.remove(el);
}
});
}
@ -184,6 +188,7 @@ define([
'data-curve': data.curvePublic || '',
'data-name': name.toLowerCase(),
'data-order': i,
'tabindex': config.noSelect ? '-1' : '0',
style: 'order:'+i+';'
},[
avatar,
@ -231,6 +236,13 @@ define([
}
onSelect();
});
$div.on('keydown', '.cp-usergrid-user', function (e) {
if (e.which === 13) {
e.preventDefault();
e.stopPropagation();
$(this).trigger('click');
}
});
}
return {
@ -611,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);
@ -626,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',
@ -685,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 () {
@ -698,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 () {
@ -712,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 () {
@ -762,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))
@ -825,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();
});
@ -867,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();
@ -890,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) {
@ -938,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;
@ -958,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();
@ -970,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 () {
@ -982,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 () {
@ -1268,6 +1252,7 @@ define([
kanban: 'kanban',
form: 'form',
whiteboard: 'whiteboard',
diagram: 'diagram',
};
var href = "https://docs.cryptpad.org/en/user_guide/applications.html";
@ -1595,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
@ -2498,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');
@ -2509,7 +2505,11 @@ define([
if (e.which === 9) {
e.preventDefault();
e.stopPropagation();
next();
if (e.shiftKey) {
previous();
} else {
next();
}
return;
}
if (e.which === 13) {
@ -2645,7 +2645,7 @@ define([
var urlArgs = (Config.requireConf && Config.requireConf.urlArgs) || '';
var logo = h('img', { src: '/customize/CryptPad_logo.svg?' + urlArgs });
var fill1 = h('div.cp-creation-fill.cp-creation-logo', logo);
var fill1 = h('div.cp-creation-fill.cp-creation-logo',{ role: 'presentation' }, logo);
var fill2 = h('div.cp-creation-fill');
var $creation = $('<div>', { id: 'cp-creation', tabindex:1 });
$creationContainer.append([fill1, $creation, fill2]);
@ -3547,7 +3547,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));
});
@ -3603,7 +3603,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;
@ -4285,5 +4285,76 @@ define([
return UI.errorLoadingScreen(msg, false, false);
};
UIElements.makePalette = (maxColors, onSelect) => {
let palette = [''];
for (var i=1; i<=maxColors; i++) { palette.push('color'+i); }
let offline = false;
let selectedColor = '';
let container = h('div.cp-palette-container');
let $container = $(container);
var all = [];
palette.forEach(function (color, i) {
var $color = $(h('button.cp-palette-color.fa'));
all.push($color);
$color.addClass('cp-palette-'+(color || 'nocolor'));
$color.keydown(function (e) {
if (e.which === 13) {
e.stopPropagation();
e.preventDefault();
$color.click();
}
});
$color.click(function () {
if (offline) { return; }
if (color === selectedColor) { return; }
selectedColor = color;
$container.find('.cp-palette-color').removeClass('fa-check');
$color.addClass('fa-check');
onSelect(color, $color);
}).appendTo($container);
$color.keydown(e => {
if (e.which === 37) {
e.preventDefault();
if (i === 0) {
all[all.length - 1].focus();
} else {
all[i - 1].focus();
}
}
if (e.which === 39) {
e.preventDefault();
if (i === (all.length - 1)) {
all[0].focus();
} else {
all[i + 1].focus();
}
}
if (e.which === 9) {
if (e.shiftKey) {
all[0].focus();
return;
}
all[all.length - 1].focus();
}
});
});
container.disable = state => {
offline = !!state;
};
container.getValue = () => {
return selectedColor;
};
container.setValue = color => {
$container.find('.cp-palette-color').removeClass('fa-check');
let $color = $container.find('.cp-palette-'+(color || 'nocolor'));
$color.addClass('fa-check');
selectedColor = color;
};
return container;
};
return UIElements;
});

View File

@ -606,6 +606,9 @@
parseInt(h.slice(4,6), 16),
];
};
Util.rgbToHex = function (rgb) {
return `#${rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/).slice(1).map(n => parseInt(n, 10).toString(16).padStart(2, '0')).join('')}`;
};
Util.isSmallScreen = function () {
return window.innerHeight < 800 || window.innerWidth < 800;

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