* docs: PR1 GDPR deletion-controls design spec
First of five GDPR PRs tracked in #6701. PR1 covers deletion controls:
one-time deletion token, allowPadDeletionByAllUsers flag, authorisation
matrix for handlePadDelete and the REST deletePad endpoint, a single
token-display modal for browser pad creators, and test coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: PR1 GDPR deletion-controls implementation plan
13 TDD-structured tasks covering PadDeletionManager unit tests, socket
+ REST three-way auth, clientVars wiring, one-time token modal,
delete-with-token UI, Playwright coverage, and PR handoff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gdpr): scaffolding for pad deletion tokens
PadDeletionManager stores a sha256-hashed per-pad deletion token and
verifies it with timing-safe comparison. createPad / createGroupPad
return the plaintext token once on first creation, and Pad.remove()
cleans it up. Gated behind the new allowPadDeletionByAllUsers flag
which defaults to false to preserve existing behaviour.
Part of #6701 (GDPR PR1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix+test(gdpr): lazy DB access in PadDeletionManager + unit tests
Capturing DB.db at module-load time was null until DB.init() ran, which
broke importing the module outside a live server (including from the
test runner). Switch to DB.db.* at call time and add unit tests
exercising create/verify/remove plus timing-safe comparison.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gdpr): three-way auth for socket PAD_DELETE
Creator cookie → valid deletion token → allowPadDeletionByAllUsers flag.
Anyone else still gets the existing refusal shout.
* feat(gdpr): optional deletionToken on programmatic deletePad
* feat(gdpr): advertise optional deletionToken on REST deletePad
* test(gdpr): cover deletePad authorisation matrix via REST
* feat(gdpr): surface padDeletionToken in clientVars for creators only
Revision-0 author on their first CLIENT_READY visit receives the
plaintext token; all subsequent CLIENT_READYs receive null because
createDeletionTokenIfAbsent is idempotent. Readonly sessions and any
other user never see the token.
* i18n(gdpr): strings for deletion-token modal and delete-with-token flow
* feat(gdpr): token modal + delete-with-token disclosure markup
* feat(gdpr): show deletion token once, allow delete via recovery token
* style(gdpr): modal + delete-with-token layout
* test(gdpr): Playwright coverage for deletion-token modal + delete-with-token
* fix(test): auto-dismiss deletion-token modal in goToNewPad helper
The token modal introduced in PR1 blocks clicks for every Playwright
test that creates a new pad via the shared helper. Add a one-line
dismissal so unrelated tests keep passing, and have the deletion-token
spec navigate inline via newPadKeepingModal() when it needs the modal
open to capture the token.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): dismiss deletion-token modal without focus transfer
Clicking the ack button transferred focus out of the pad iframe, which
made subsequent keyboard-driven tests (Tab / Enter) silently miss the
editor. Swap the click for a page.evaluate() that hides the modal and
nulls clientVars.padDeletionToken directly, leaving focus where it was.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gdpr): PadDeletionManager race + document createPad/deletePad
Qodo review:
- createDeletionTokenIfAbsent() was a non-atomic read-then-write. Two
concurrent callers for the same pad could both return different
plaintext tokens while only the later hash was stored, leaving the
first caller with an unusable recovery token. Serialise per-pad via a
Promise chain and add a regression test that fires 8 concurrent
calls and asserts exactly one plaintext is emitted and validates.
- doc/api/http_api.md now documents createPad returning deletionToken
and deletePad accepting the optional deletionToken parameter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gdpr): always render delete-with-token in settings popup
The rebase onto develop placed the delete-pad-with-token details inside
the pad-settings-section conditional, which is only rendered when
enablePadWideSettings is true AND the section is toggled visible.
Second-device recovery (typing the captured token on a fresh browser)
must work without pad-wide settings enabled, so move the details out
to sit alongside the existing pad_deletion_token.spec.ts expectations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gdpr): require valid token when supplied, gate on auth, harden a11y/i18n
- PadMessageHandler: a supplied deletion token must validate; do not fall
back to the creator-cookie path when the token is wrong (was deleting
the pad anyway when the creator pasted a wrong token into the field).
- Skip token issuance + UI when requireAuthentication is on (creator
identity is stable, recovery token is redundant noise).
- Server emits messageKey instead of hardcoded English; both shout
handlers (inline alert and global gritter) localize via html10n.
- Suppress the global "Admin message" gritter for pad.deletionToken.*
shouts to avoid the "Admin message: undefined" duplicate.
- Token-modal a11y: role=dialog, aria-modal, aria-labelledby/describedby,
visually-hidden label on the token input, aria-live on Copy, focus to
the token input on open and restore on dismiss.
- Style the "Delete Pad with Token" disclosure to match the Delete pad
button; align the Copy/value row; pad the disclosure label.
Tests: Playwright now covers the creator-with-wrong-token path, asserts
no "Admin message" / "undefined" gritter on denial; backend API test
covers requireAuthentication suppressing the token.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): add IDE-style line ops (duplicate / delete)
Addresses #6433 — the issue asked for VS-Code-style multi-line editing
for collaborative markdown editing. Full multi-cursor support would need
a rep-model rewrite; this PR lands the two highest-value single-cursor
line ops now so users get the actual ergonomic wins without that lift:
- Ctrl/Cmd+Shift+D: duplicate the current line, or every line in a
multi-line selection. Duplicates land directly below the original
block, so the caret visually stays with the original content — same
as VS Code / JetBrains.
- Ctrl/Cmd+Shift+K: delete the current line (or every line in a
multi-line selection), collapsing the range including its trailing
newline. Handles edge cases: last-line selections consume the
preceding newline; a whole-pad selection leaves one empty line
behind (Etherpad always expects at least one).
Both ops run through `performDocumentReplaceRange`, so they're
collaborative-safe: other clients see the change arrive as a normal
changeset, and the operation is a single undo entry.
Wire-up:
- `src/node/utils/Settings.ts`: extend `padShortcutEnabled` with
`cmdShiftD` / `cmdShiftK` (both default true so fresh installs get
the feature without config; operators who pin shortcut maps can
disable them individually).
- `src/static/js/ace2_inner.ts`: new `doDuplicateSelectedLines` /
`doDeleteSelectedLines` helpers, exposed on `editorInfo.ace_*` so
plugins and tests can invoke them programmatically, and keyboard
handlers for Ctrl/Cmd+Shift+D and Ctrl/Cmd+Shift+K.
Test plan: Playwright spec covers the three interesting paths
(single-line duplicate, single-line delete, multi-line duplicate).
Closes#6433
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(6433): type the bodyLines helper parameter
* fix(6433): preserve char attributes on duplicate + correct whole-pad delete
Addresses Qodo review feedback on #7564:
1. `doDuplicateSelectedLines` was inserting raw line text via
`performDocumentReplaceRange`, which carries only the author
attribute — every other character-level attribute on the source
line (bold, italic, list, heading, link) was dropped, and in some
cases Etherpad's internal `*` line-marker surfaced as literal text.
Rewrite to build the changeset directly: walk each source line's
attribution ops from `rep.alines[i]`, split the line text at op
boundaries, and call `builder.insert(segment, op.attribs)` once per
op. Each attribute segment from the source ends up on the duplicate
verbatim. Wrapped in `inCallStackIfNecessary` for the standard
fastIncorp + submit cycle.
2. `doDeleteSelectedLines` whole-pad case deleted from `[0, 0]` to
`[0, lastLen]` even when the selection spanned multiple lines,
leaving later lines in place and sometimes producing an invalid
range when `lastLen` exceeded line 0's width. Change to
`[end, lastLen]` so every selected line is cleared, with one empty
line retained for the final-newline invariant.
3. Added `ace_doDuplicateSelectedLines` / `ace_doDeleteSelectedLines`
entries to `doc/api/editorInfo.md` so plugin authors can discover
the new surface.
4. New Playwright spec asserting `<b>` tags survive duplication.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert(6433): drop the attributed-duplicate changeset, keep whole-pad delete fix
The attributed-changeset rewrite for doDuplicateSelectedLines tripped
over the insertion-past-final-newline edge case — CI caught the basic
single-line duplicate regressing (gamma → [alpha, beta, gamma] with no
new gamma appearing because the hand-rolled changeset ended up invalid
at the end-of-pad boundary). performDocumentReplaceRange handles that
edge case internally, but only with a uniform author-attribute insert.
Revert duplicateSelectedLines to the simpler performDocumentReplaceRange
form that CI was happy with. Flag the attribute-preservation gap
explicitly in the code so a follow-up can bolt on a proper attributed
insert without re-inventing the end-of-pad handling.
Whole-pad delete fix and editorInfo.md docs stay. Attribute-preservation
test in line_ops.spec.ts is removed along with the broken code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: Rename some occurences of etherpad-lite to etherpad
* chore: Adjust etherpad git urls
* chore: Rename more occurences from etherpad-lite to etherpad
* chore: Adjust default text
* Add docs for aceRegisterLineAttributes hook
Documents the new hook in both .md and .adoc client-side hook references.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix: correct source file path from .js to .ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This puts global state change logic with the rest of the global state
management logic. This also makes it possible to create temporary Pad
objects without triggering plugin actions.
These files cause problems with Docker images and read-only
directories/mounts, and they have dubious value (any install-time
setup should instead be done at startup).