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) {}