From ce038b0b0f4d12926650181f6a001912ae9769ff Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 17 Jun 2026 15:18:23 +0100 Subject: [PATCH] fix(pad): keep token-less Delete pad reachable without pad-wide settings (#7959) (#7960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(pad): keep token-less Delete pad reachable without pad-wide settings (#7959) The token-less "Delete pad" button (#delete-pad) was nested inside the enablePadWideSettings-gated pad-settings section, so disabling pad-wide settings removed the only way to delete a pad without a recovery token. Combined with #7926 hiding the token disclosure when deletion needs no token (e.g. allowPadDeletionByAllUsers), a user who was allowed to delete could be left with no deletion UI at all. Pad deletion is unrelated to pad-wide settings, so: - Move #delete-pad out of the enablePadWideSettings block in pad.html; it is now always rendered and hidden by default. - Add a canDeletePad clientVar (isCreator || allowPadDeletionByAllUsers) and drive the button's visibility from it in pad_editor.ts, mirroring the existing canDeleteWithoutToken handling for the token disclosure. The two controls are now mutually coherent and neither depends on enablePadWideSettings: the plain button shows when this session can delete without a token, the recovery-token disclosure shows otherwise. Tests: - backend padDeletionUiPlacement.ts: #delete-pad is rendered with enablePadWideSettings both on and off (fails without the template move). - backend socketio.ts: canDeletePad reflects the creator/allow-all matrix, including a non-creator who only gains it under allowPadDeletionByAllUsers. - frontend pad_settings.spec.ts: asserts #delete-pad is no longer a descendant of #pad-settings-section. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(pad): never let readonly sessions delete via token-less paths (#7959) Qodo review of #7960: `canDeletePad` was `isCreator || allowPadDeletionByAllUsers`, so under allowPadDeletionByAllUsers a readonly viewer received canDeletePad=true and the relocated #delete-pad button unhid for them. Worse, the server-side handlePadDelete `flagOk`/`creatorOk` branches never checked session.readonly either, so a readonly-link holder could actually delete the pad without a token — a data-loss hole that the new always-rendered button would expose. Exclude readonly sessions from both the clientVar and the server's token-less authorization paths. A valid recovery token (tokenOk) stays a sufficient credential regardless of session mode. Test: socketio.ts asserts a readonly viewer gets canDeletePad=false and that a token-less PAD_DELETE from a readonly session leaves the pad intact (red before this change on the clientVar assertion). Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- src/node/handler/PadMessageHandler.ts | 20 ++++- src/static/js/pad_editor.ts | 7 ++ src/templates/pad.html | 14 ++-- .../backend/specs/padDeletionUiPlacement.ts | 44 +++++++++++ src/tests/backend/specs/socketio.ts | 74 +++++++++++++++++++ .../frontend-new/specs/pad_settings.spec.ts | 5 +- 6 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/tests/backend/specs/padDeletionUiPlacement.ts diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index 4f18693cc..7ede7d391 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -305,8 +305,13 @@ const handlePadDelete = async (socket: any, padDeleteMessage: PadDeleteMessage) // back to the creator-cookie path, otherwise a creator pasting a wrong // recovery token into the disclosure field would still succeed — masking a // typo and contradicting the UI. - const creatorOk = !tokenSupplied && isCreator; - const flagOk = !tokenSupplied && !isCreator && settings.allowPadDeletionByAllUsers; + // Readonly sessions can never delete via the token-less paths: they cannot + // edit the pad, so they must not be able to destroy it just because + // allowPadDeletionByAllUsers is on (issue #7959). A valid recovery token + // (tokenOk) remains a sufficient credential regardless of session mode. + const writable = !session.readonly; + const creatorOk = !tokenSupplied && isCreator && writable; + const flagOk = !tokenSupplied && !isCreator && settings.allowPadDeletionByAllUsers && writable; if (creatorOk || tokenOk || flagOk) { await retrievedPad.remove(); @@ -1321,6 +1326,16 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { const hasGetAuthorIdHook = (plugins.hooks.getAuthorId || []).length > 0; const hasDurableIdentity = hasGetAuthorIdHook && !!(user && user.username); const canDeleteWithoutToken = settings.allowPadDeletionByAllUsers || hasDurableIdentity; + // Whether this session may delete the pad with no token at all: the creator + // on this device (creator-cookie still present), or any user when the + // instance opted everyone in. Drives the plain "Delete pad" button, which is + // independent of enablePadWideSettings (issue #7959) — deletion is not a + // pad-wide setting and must stay reachable when that section is disabled. + // Readonly viewers are excluded: they cannot edit, let alone delete, so + // allowPadDeletionByAllUsers must not hand them a delete button (the server + // enforces the same in handlePadDelete). + const canDeletePad = + !sessionInfo.readonly && (isCreator || settings.allowPadDeletionByAllUsers); const padDeletionToken = isCreator && !canDeleteWithoutToken ? await padDeletionManager.createDeletionTokenIfAbsent(sessionInfo.padId) @@ -1346,6 +1361,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { // redundant, so the client labels the action "Delete Pad" instead of // "Delete with token" (issue #7926). See showDeletionTokenModalIfPresent. canDeleteWithoutToken, + canDeletePad, // Allow-listed copy — settings.privacyBanner could carry extra nested // keys from a hand-edited settings.json; sending those by reference // would leak them to every browser. See getPublicPrivacyBanner(). diff --git a/src/static/js/pad_editor.ts b/src/static/js/pad_editor.ts index 0382a813a..bd721e62c 100644 --- a/src/static/js/pad_editor.ts +++ b/src/static/js/pad_editor.ts @@ -159,6 +159,13 @@ const padeditor = (() => { $('#delete-pad-with-token').prop( 'hidden', !!(window as any).clientVars?.canDeleteWithoutToken); + // The plain "Delete pad" button is shown whenever this session can delete + // without a token (creator on this device, or allowPadDeletionByAllUsers). + // It is independent of pad-wide settings so it stays reachable when that + // section is disabled (issue #7959). + $('#delete-pad').prop( + 'hidden', !(window as any).clientVars?.canDeletePad); + // delete pad using a recovery token (second device / no creator cookie) $('#delete-pad-token-submit').on('click', () => { const token = String($('#delete-pad-token-input').val() || '').trim(); diff --git a/src/templates/pad.html b/src/templates/pad.html index 27f21948c..1a1efe19b 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -391,13 +391,17 @@

<% e.end_block(); %> - <% } %> - + +