diff --git a/.dockerignore b/.dockerignore index f09aed879..ba2816f47 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,14 +3,18 @@ # SPDX-License-Identifier: AGPL-3.0-or-later .dockerignore +.editorconfig .git .gitignore .gitmodules .github +.reuse +.stylelintrc.js +.eslint.config.js docker-compose.yml -traefik2.yml Dockerfile* *.png +*.md /onlyoffice-builds.git/ /www/common/onlyoffice/dist/ blob @@ -18,3 +22,4 @@ block customize data datastore +docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d1e234d..60f502ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,71 @@ SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and cont SPDX-License-Identifier: AGPL-3.0-or-later --> + +# ❄️ Winter release (2024.12.0) + +## Goals + +This version delivers fixes and improvements across CryptPad. We are particularly happy to release a fix of our OnlyOffice integration that could address long-standing issues with documents becoming corrupted. If confirmed at scale, this fix could dramatically improve the use of OnlyOffice apps in CryptPad. + +## Improvements + +- OnlyOffice integration + - Fix bug resulting in corrupted documents [#1736](https://github.com/cryptpad/cryptpad/pull/1736) +- Drive + - Links included in Drive exports [#1695](https://github.com/cryptpad/cryptpad/pull/1695) +- Rich Text + - Formatted tables and strikethrough text in Pad .md exports [#1720](https://github.com/cryptpad/cryptpad/pull/1720) +- Forms + - Form password warning [#1690](https://github.com/cryptpad/cryptpad/pull/1690) +- Performance improvements (1/3) to example-advanced.nginx.conf [#1709](https://github.com/cryptpad/cryptpad/pull/1709) +- Enable toggle in and out of calendars on small screens [#1584](https://github.com/cryptpad/cryptpad/pull/1584) + +## Fixes + +- Accessibility + - Kanban Focus order fix [#1708](https://github.com/cryptpad/cryptpad/pull/1708) + - Change iframe title [#1706](https://github.com/cryptpad/cryptpad/pull/1706) + - Fix keyboard trap inside Form description [#1672](https://github.com/cryptpad/cryptpad/pull/1672) + - Disable arrow key navigation in the drive while modal is active [#1669](https://github.com/cryptpad/cryptpad/pull/1669) + - Simulate click action for keyboard users in Ctrl+E modal [#1726](https://github.com/cryptpad/cryptpad/pull/1726) +- Drive + - Prevent links in trash from disappearing after drive reload [#1697](https://github.com/cryptpad/cryptpad/pull/1697) + - Restore multiple files/directories [#1692](https://github.com/cryptpad/cryptpad/pull/1692) + - Stop selection of all other trashed files when restoring single file [#1681](https://github.com/cryptpad/cryptpad/pull/1681) +- Notifications + - Calendar reminders in notification panel [#1721](https://github.com/cryptpad/cryptpad/pull/1721) + - Add Notifications padding [#1688](https://github.com/cryptpad/cryptpad/pull/1688) + - Notification fixes [#1674](https://github.com/cryptpad/cryptpad/pull/1674) +- Forms + - Fix some form storage-related bugs [#1723](https://github.com/cryptpad/cryptpad/pull/1723) + - Fix spacing issues in Forms [#1682](https://github.com/cryptpad/cryptpad/pull/1682) + - Fix padding in form questions [#1670](https://github.com/cryptpad/cryptpad/pull/1670) +- Helpdesk + - Fix "Closed" support tickets remaining in Inbox [#1719](https://github.com/cryptpad/cryptpad/pull/1719) +- 'Request edit' button [#1680](https://github.com/cryptpad/cryptpad/pull/1680) +- Fix example-code-typo [#1703](https://github.com/cryptpad/cryptpad/pull/1703) + +## Upgrade notes + +If you are upgrading from a version older than `2024.9.1` please read the upgrade notes of all versions between yours and `2024.9.1` to avoid configuration issues. + +To upgrade: + +1. Stop your server +2. Get the latest code with git + +```bash +git fetch origin --tags +git checkout 2024.12.0 +npm ci +npm run install:components +``` + +3. Restart your server +4. Review your instance's checkup page to ensure that you are passing all tests + + # Autumn release (2024.9.0) ## Goals diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 2b4ee6234..469279baf 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -134,6 +134,24 @@ define(req, function(AppConfig, Default, Language) { return text; } }; + + // XXX + Messages.admin_cat_admins = "Administrators"; + Messages.admin_admin = "Admin"; + Messages.admin_listAdminsTitle = "Current administrators"; + Messages.admin_listAdminsHint = "View and remove administrators"; + Messages.admin_addAdminsTitle = "Add administrators"; + Messages.admin_addAdminsHint = "Add administrators from their public key or from your contacts list"; + Messages.admin_addAdminsAdd = "Promote a contact to admin"; + Messages.admin_addKeyLabel = "Add an admin using their public key"; + + Messages.admin_listName = "Admin name"; + Messages.admin_listKey = "Admin key"; + Messages.admin_listAction = "Remove admin rights"; + + Messages.admin_listHardcoded = "Admin added into config.js. Can only be removed by editing the config file."; + Messages.admin_listConfirm = "Are you sure you want to remove the admin rights of this user?"; + return Messages; }); diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 07117b0f9..1f5aee211 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -164,6 +164,7 @@ @cp_buttons-hover: @cryptpad_color_brand_fadest; @cp_buttons-default: @cryptpad_color_grey_700; @cp_buttons-default-color: @cryptpad_text_col; +@cp_buttons-default-alt-color: @cryptpad_color_grey_700; @cp_buttons-default-border: @cryptpad_text_col; @cp_buttons-red: #E55236; @cp_buttons-red-text: @cryptpad_color_light_red; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index faaca5285..ece9f6fe0 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -164,6 +164,7 @@ @cp_buttons-default: #CCC; @cp_buttons-default-color: @cryptpad_text_col; @cp_buttons-default-border: @cryptpad_text_col; +@cp_buttons-default-alt-color: @cryptpad_color_grey_50; @cp_buttons-red: #E55236; @cp_buttons-red-text: @cp_buttons-red; @cp_buttons-red-color: #FFF; diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index 3c0b206db..745d24ab0 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -199,6 +199,16 @@ background-color: @cp_buttons-default; } } + &.btn-default-alt { + border-color: @cp_buttons-default-alt-color; + color: @cp_buttons-default-alt-color; + background-color: @cp_buttons-default-color; + &:hover, &:not(:disabled):active, &:focus { + border-color: @cp_buttons-default-color; + color: @cp_buttons-default-color; + background-color: @cp_toolbar-fade3; + } + } &.danger, &.btn-danger { background-color: @cp_buttons-red; diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index 90eb873ef..33950434c 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -7,6 +7,7 @@ @import (reference) "/customize/src/less2/include/colortheme-all.less"; @import (reference) "/customize/src/less2/include/leftside-menu.less"; @import (reference) "/customize/src/less2/include/browser.less"; +@import (reference) "/customize/src/less2/include/variables.less"; @sidebar_block-width: 25rem;; @sidebar_base-margin: 0.5rem; @@ -121,6 +122,12 @@ flex-flow: column; align-items: baseline; } + .cp-usergrid-container { + margin-bottom: 0px !important; + .cp-usergrid-grid { + margin-bottom: -3px; + } + } label { margin-bottom: 0; } @@ -128,6 +135,9 @@ font-family: inherit; max-width: @sidebar_block-width; } + input:invalid { + border: 1px solid red; + } [type="color"] { width: @sidebar_block-width/5; padding: 3px; @@ -214,7 +224,7 @@ border-top-left-radius: 0px; border-bottom-left-radius: 0px; border-left: 0px; - height: 40px; + height: @variables_input-height; margin: 0 !important; } } @@ -236,6 +246,12 @@ margin-bottom: 0; } } + .cp-sidebarlayout-description-item { + display: block; + color: @cp_sidebar-hint; + margin-top: @sidebar_base-margin; + margin-bottom: 0; + } label.noTitle { display: inline-flex; .fa { diff --git a/docker-compose.yml b/docker-compose.yml index 49a81a25b..84ffc0e1b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ --- services: cryptpad: - image: "cryptpad/cryptpad:version-2024.12.0" + image: "cryptpad/cryptpad:version-2025.3.0" hostname: cryptpad environment: diff --git a/lib/api.js b/lib/api.js index df3206b1e..d613df78e 100644 --- a/lib/api.js +++ b/lib/api.js @@ -27,6 +27,31 @@ nThen(function (w) { console.error(err); } })); +}).nThen(function () { + if (Env.proofsMigrated) { return; } + const { Worker } = require('node:worker_threads'); + const Admin = require("./commands/admin-rpc"); + + const worker = new Worker('./scripts/migrations/migrate-blob-proofs.js'); + + worker.on('message', message => { + if (message === 'READY') { + log.info('BLOB_PROOFS_MIGRATION'); + return void worker.postMessage({ + start: 1, + }); + } + if (message === 'MIGRATED') { + return void log.info('BLOB_PROOFS_DELETION'); + } + if (message === 'CLEANED') { + log.info('BLOB_PROOFS_MIGRATED'); + Admin.sendDecree(Env, null, function (err) { + if (err) { return void log.error('BLOB_PROOF', err); } + Env.flushCache(); + }, ['PROOFS_MIGRATED', ['PROOFS_MIGRATED', 1]], 'server'); + } + }); }).nThen(function (w) { let admins = Env.admins || []; diff --git a/lib/archive-account.js b/lib/archive-account.js index 43ff1357e..44928f6be 100644 --- a/lib/archive-account.js +++ b/lib/archive-account.js @@ -13,6 +13,7 @@ const Metadata = require("./commands/metadata"); const Meta = require("./metadata"); const Logger = require("./log"); const plugins = require("./plugin-manager"); +const HK = require('./hk-util'); let SSOUtils = plugins.SSO && plugins.SSO.utils; @@ -53,7 +54,13 @@ const init = (cb) => { Env.computeMetadata = function (channel, cb) { const ref = {}; const lineHandler = Meta.createLineHandler(ref, (err) => { console.log(err); }); - return void Env.store.readChannelMetadata(channel, lineHandler, function (err) { + + let f = Env.store.readChannelMetadata; + if (channel.length === HK.BLOB_ID_LENGTH) { + f = Env.blobStore.readMetadata; + } + + return void f(channel, lineHandler, function (err) { if (err) { // stream errors? return void cb(err); @@ -132,9 +139,12 @@ COMMANDS.start = (edPublic, blockId, reason) => { n = n((w) => { // Blobs if (Env.blobStore.isFileId(chanId)) { - return void Env.blobStore.isOwnedBy(safeKey, chanId, w((err, owned) => { - if (err || !owned) { return; } - blobsToArchive.push(chanId); + return Env.computeMetadata(chanId, w((e, md) => { + if (e || !md) { return; } + if (md && md.owners + && md.owners.includes(edPublic)) { + blobsToArchive.push(chanId); + } })); } // Pads diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index 9030159f8..6bc35f5be 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -18,9 +18,11 @@ const MFA = require("../storage/mfa"); const ArchiveAccount = require('../archive-account'); const { Worker } = require('node:worker_threads'); const Fse = require("fs-extra"); - const Fs = require("fs"); +const config = require("../load-config"); +const Keys = require("../keys"); + var Admin = module.exports; var getFileDescriptorCount = function (Env, server, cb) { @@ -409,7 +411,7 @@ var getChannelMetadata = function (Env, Server, cb, data) { }; // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log) -var adminDecree = function (Env, Server, cb, data, unsafeKey) { +var adminDecree = Admin.sendDecree = function (Env, Server, cb, data, unsafeKey) { var value = data[1]; if (!Array.isArray(value)) { return void cb('INVALID_DECREE'); } @@ -468,7 +470,29 @@ var setLastEviction = function (Env, Server, cb, data, unsafeKey) { }; // CryptPad_AsyncStore.rpc.send('ADMIN', ['INSTANCE_STATUS], console.log) +const getAdminsData = (Env) => { + return Env.adminsData.map(str => { + // str is either a full public key or just the ed part + const edPublic = Keys.canonicalize(str); + const hardcoded = Array.isArray(config?.adminKeys) && + config.adminKeys.some(key => { + return Keys.canonicalize(key) === edPublic; + }); + if (str.length === 44) { + return { edPublic, first: true, hardcoded }; + } + let name; + try { + const parsed = Keys.parseUser(str); + name = parsed.user; + } catch (e) {} + return { + edPublic, hardcoded, name + }; + }); +}; var instanceStatus = function (Env, Server, cb) { + cb(void 0, { appsToDisable: Env.appsToDisable, @@ -514,6 +538,8 @@ var instanceStatus = function (Env, Server, cb) { instanceName: Env.instanceName, instanceNotice: Env.instanceNotice, enforceMFA: Env.enforceMFA, + + admins: getAdminsData(Env) }); }; @@ -1138,6 +1164,19 @@ Admin.command = function (Env, safeKey, data, _cb, Server) { var command = commands[data[0]]; + Object.keys(Env.plugins || {}).forEach(name => { + let plugin = Env.plugins[name]; + if (!plugin.addAdminCommands) { return; } + try { + let c = plugin.addAdminCommands(Env); + Object.keys(c || {}).forEach(cmd => { + if (typeof(c[cmd]) !== "function") { return; } + if (commands[cmd]) { return; } + commands[cmd] = c[cmd]; + }); + } catch (e) {} + }); + if (typeof(command) === 'function') { return void command(Env, Server, cb, data, unsafeKey); } diff --git a/lib/commands/metadata.js b/lib/commands/metadata.js index 8eeca31ce..a468f6f6e 100644 --- a/lib/commands/metadata.js +++ b/lib/commands/metadata.js @@ -13,7 +13,8 @@ Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) { const cb = Util.once(Util.mkAsync(_cb)); if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); } if (channel.length !== HK.STANDARD_CHANNEL_LENGTH && - channel.length !== HK.ADMIN_CHANNEL_LENGTH) { return cb("INVALID_CHAN_LENGTH"); } + channel.length !== HK.ADMIN_CHANNEL_LENGTH && + channel.length !== HK.BLOB_ID_LENGTH) { return cb("INVALID_CHAN_LENGTH"); } // return synthetic metadata for admin broadcast channels as a safety net // in case anybody manages to write metadata @@ -79,6 +80,7 @@ Data.setMetadata = function (Env, safeKey, data, cb, Server) { var channel = data.channel; var command = data.command; + // XXX BLOBMD allow blobs if (!channel || !Core.isValidId(channel)) { return void cb ('INVALID_CHAN'); } if (!command || typeof (command) !== 'string') { return void cb('INVALID_COMMAND'); } if (Meta.commands.indexOf(command) === -1) { return void cb('UNSUPPORTED_COMMAND'); } @@ -140,6 +142,7 @@ Data.setMetadata = function (Env, safeKey, data, cb, Server) { cb(void 0, metadata); return void next(); } + // XXX BLOBMD use correct store for blobs Env.msgStore.writeMetadata(channel, JSON.stringify(line), function (e) { if (e) { cb(e); @@ -155,6 +158,7 @@ Data.setMetadata = function (Env, safeKey, data, cb, Server) { // update the cached metadata metadata_cache[channel] = metadata; + Env.checkCache(channel); // XXX ??? // it's easy to check if the channel is restricted const isRestricted = metadata.restricted; diff --git a/lib/decrees-core.js b/lib/decrees-core.js new file mode 100644 index 000000000..3a40e5d6a --- /dev/null +++ b/lib/decrees-core.js @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +var Decrees = module.exports; +var Util = require("./common-util"); +var Fs = require("fs"); +var Path = require("path"); +var readFileBin = require("./stream-file").readFileBin; +var Schedule = require("./schedule"); +var Fse = require("fs-extra"); +var nThen = require("nthen"); + + +const Utils = Decrees.Utils = {}; +var isString = (str) => { + return typeof(str) === "string"; +}; +var isInteger = function (n) { + return !(typeof(n) !== 'number' || isNaN(n) || (n % 1) !== 0); +}; +Utils.args_isBoolean = function (args) { + return !(!Array.isArray(args) || typeof(args[0]) !== 'boolean'); +}; +Utils.args_isString = function (args) { + return !(!Array.isArray(args) || !isString(args[0])); +}; +Utils.args_isInteger = function (args) { + return !(!Array.isArray(args) || !isInteger(args[0])); +}; +Utils.args_isPositiveInteger = function (args) { + return Array.isArray(args) && isInteger(args[0]) && args[0] > 0; +}; + + +Decrees.create = (name, commands) => { + // [, , ,