From dccbf2eb84a2b7f903f8c888c1cbe41c612c0cb3 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 9 Jun 2026 14:21:13 +0100 Subject: [PATCH] fix(pad): don't issue a deletion token (or show its modal) when allowPadDeletionByAllUsers is on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `allowPadDeletionByAllUsers` is true, anyone can delete a pad with no token at all (handlePadDelete's flagOk branch), so the one-time deletion token is pointless and the "Save your pad deletion token" modal only overwhelms users who will never need it. Gate token issuance on `!settings.allowPadDeletionByAllUsers` so no token reaches clientVars; the client's showDeletionTokenModalIfPresent() then returns early and the modal never appears. No new setting — it derives automatically from the existing one. Closes #7926. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/node/handler/PadMessageHandler.ts | 12 +++++-- src/tests/backend/specs/socketio.ts | 45 ++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index 592e5b9e857..0bf559dfdd8 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -1287,9 +1287,15 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { // once. Readonly sessions never see it. const isCreator = !sessionInfo.readonly && sessionInfo.author === await pad.getRevisionAuthor(0); - // Skip token issuance when requireAuthentication is on: every creator has a - // stable identity so the cookie/identity path is sufficient. - const padDeletionToken = isCreator && !settings.requireAuthentication + // Skip token issuance — and so the client never shows the "Save your pad + // deletion token" modal (issue #7926) — when the token cannot help: + // - requireAuthentication: every creator already has a stable identity, so + // the cookie/identity path is sufficient. + // - allowPadDeletionByAllUsers: anyone can delete the pad with no token at + // all (see handlePadDelete's flagOk branch), so a recovery token is noise + // and the modal only overwhelms users who will never need it. + const padDeletionToken = + isCreator && !settings.requireAuthentication && !settings.allowPadDeletionByAllUsers ? await padDeletionManager.createDeletionTokenIfAbsent(sessionInfo.padId) : null; diff --git a/src/tests/backend/specs/socketio.ts b/src/tests/backend/specs/socketio.ts index 43da20b15e2..84a306282ea 100644 --- a/src/tests/backend/specs/socketio.ts +++ b/src/tests/backend/specs/socketio.ts @@ -34,7 +34,7 @@ describe(__filename, function () { plugins.hooks[hookName] = []; } backups.settings = {}; - for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users', 'enablePadWideSettings']) { + for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users', 'enablePadWideSettings', 'allowPadDeletionByAllUsers']) { // @ts-ignore backups.settings[setting] = settings[setting]; } @@ -492,6 +492,49 @@ describe(__filename, function () { }); }); + describe('Pad deletion token issuance (#7926)', function () { + const removeIfExists = async (padId: string) => { + if (await padManager.doesPadExist(padId)) { + const p = await padManager.getPad(padId); + await p.remove(); + } + }; + + beforeEach(async function () { + // @ts-ignore - public setting toggled per test + settings.allowPadDeletionByAllUsers = false; + await removeIfExists('pad'); + }); + afterEach(async function () { + if (socket) socket.close(); + socket = null; + await removeIfExists('pad'); + }); + + it('anonymous creator receives a deletion token by default', async function () { + const res = await agent.get('/p/pad').expect(200); + socket = await common.connect(res); + const cv: any = await common.handshake(socket, 'pad'); + assert.equal(cv.type, 'CLIENT_VARS'); + assert.equal(typeof cv.data.padDeletionToken, 'string', + 'creator should get a token so the client can show the save-token modal'); + assert.ok(cv.data.padDeletionToken.length >= 32); + }); + + it('no token (and so no modal) when allowPadDeletionByAllUsers is true', async function () { + // @ts-ignore - public setting + settings.allowPadDeletionByAllUsers = true; + const res = await agent.get('/p/pad').expect(200); + socket = await common.connect(res); + const cv: any = await common.handshake(socket, 'pad'); + assert.equal(cv.type, 'CLIENT_VARS'); + // A null token means showDeletionTokenModalIfPresent() returns early on the + // client, so the "Save your pad deletion token" modal never appears. Anyone + // can already delete the pad without a token in this configuration. + assert.equal(cv.data.padDeletionToken, null); + }); + }); + describe('SocketIORouter.js', function () { const Module = class { setSocketIO(io:any) {}