9687 Commits

Author SHA1 Message Date
John McLear
31e0a61126
fix: capture head revision atomically with atext to prevent mismatched apply (#7480)
* fix: capture head revision atomically with atext to prevent mismatched apply

When constructing CLIENT_VARS, pad.atext was captured at one point but
pad.getHeadRevisionNumber() was called later. If concurrent edits
advanced the revision between these two reads, the client received
initialAttributedText from rev N but rev=N+3, causing "mismatched apply"
errors when the next changeset arrived (expecting rev N+3 text).

Now captures headRev at the same time as atext and uses the captured
value consistently in CLIENT_VARS and sessionInfo.

Fixes #4040

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: flush missed revisions after socket joins pad room

During handleClientReady(), the server awaits the clientVars hook before
socket.join(). Any revisions appended during that await window are
broadcast to existing room members but the connecting socket misses them.
Call updatePadClients(pad) after joining to flush any such revisions.

Also adds a regression test that injects a slow clientVars hook and
verifies the connecting client receives catch-up changesets for edits
that occurred during the hook await window.

Fixes #4040

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: fix race condition in clientVars hook test

Listen for messages during handshake to avoid missing NEW_CHANGES that
arrive before the explicit waitForSocketEvent listener is attached.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: initialize sessionInfo.time before catch-up updatePadClients

The catch-up updatePadClients() call introduced in this PR could send
NEW_CHANGES with timeDelta=NaN because sessionInfo.time was never set
for new sessions. NaN poisons the client-side broadcast/timeslider
currentTime tracking.

Initialize sessionInfo.time to the timestamp of the snapshot revision
before the catch-up flush, with a fallback to Date.now() if the
revision date is unavailable.

Also strengthens the regression tests:
- Validate that initialAttributedText matches the pad AText at the
  EXACT advertised rev (not just the latest pad text), using
  pad.getInternalRevisionAText(rev).
- Add a load test that hammers the pad with concurrent edits while
  multiple clients connect, asserting CLIENT_VARS consistency under
  the exact race condition the fix is targeting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: replace open-ended load loop with bounded mid-handshake edit

The previous load test ran 'while (!stopLoad) await pad.setText(...)'
in the background while the test connected clients. This saturated
ueberDB's write queue and on shutdown the queued writes never drained,
hanging the mocha process for the full 6h GitHub Actions job timeout.

Replace it with a bounded approach: a clientVars hook lands 3 edits
mid-handshake (deterministic, no background loop, no shutdown hang).
Still exercises the exact race the fix targets — an edit advancing
the rev after the atext snapshot but before CLIENT_VARS is sent —
and asserts AText / rev consistency via getInternalRevisionAText.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: address remaining Qodo concerns on PR #7480

Addresses Qodo review items 1, 2, 5 from
https://github.com/ether/etherpad-lite/issues/comments/4194702740 :

- Concern 1 (no loadTesting reproduction test): the suite now toggles
  settings.loadTest = true in before(), restores in after(). The
  middle test also pre-populates the pad with 20 revisions before
  connecting so we genuinely exercise a busy/loaded pad rather than a
  fresh one.

- Concern 2 (no CLIENT_VARS / NEW_CHANGES delay test): the slow
  clientVars hook in the middle test now has explicit setTimeout
  delays before AND after the mid-handshake edits, so the race window
  between atext snapshot and CLIENT_VARS send is observably wide
  rather than relying on async scheduling alone. The test also
  collects post-handshake messages and asserts a NEW_CHANGES catch-up
  arrives when the pad advanced past the advertised rev.

- Concern 5 (test doesn't validate rev): both rev-consistency tests
  use pad.getInternalRevisionAText(advertisedRev) and assert text and
  attribs match, not just `pad.text() === clientVars.text`.

Concerns 3 (connect can miss revisions) and 4 (NaN timeDelta) were
already addressed in earlier commits on this branch via the catch-up
updatePadClients() call and the sessionInfo.time initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:30:08 +01:00
John McLear
7c3837891b
feat: migrate npm publish to OIDC trusted publishing (#7401) (#7490)
* feat: migrate npm publish to OIDC trusted publishing (#7401)

Replaces NPM_TOKEN-based publishing with npm Trusted Publishing over
OIDC for both etherpad-lite core and the shared plugin publish
template. Tokens no longer expire every 90 days; each publish
authenticates via a short-lived OIDC token issued to the GitHub
Actions runner.

Changes:

- bin/plugins/lib/npmpublish.yml: the reusable workflow propagated to
  every ether/ep_* plugin via the update-plugins cron. Now bumps Node
  to 22, upgrades npm to >=11.5.1, declares id-token: write, drops
  NODE_AUTH_TOKEN, and calls `npm publish --provenance --access public`
  directly (not via pnpm/gnpm wrappers, which obscure the npm CLI
  version requirement).

- bin/plugins/lib/test-and-release.yml: the parent workflow that calls
  npmpublish.yml as a reusable workflow. Top-level and release-job
  permissions now grant id-token: write so the OIDC token can flow
  into the called workflow.

- .github/workflows/releaseEtherpad.yml: core's own publish workflow
  for the ep_etherpad package. Same OIDC migration; keeps the gnpm
  install + rename steps but switches the final publish to npm.

- doc/npm-trusted-publishing.md: explains how trusted publishing
  works, the one-time per-package setup that has to happen on
  npmjs.com, requirements (Node 22.14+, npm 11.5.1+, cloud runners),
  and common errors.

The next update-plugins cron run will propagate the new template to
every plugin. Once that lands and the trusted publisher is configured
on npmjs.com per package, the NPM_TOKEN secret can be removed.

Closes #7401

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add bin/setup-trusted-publishers.sh for bulk OIDC config (#7401)

Adds a script that automates the per-package trusted-publisher setup
that previously had to be done by clicking through npmjs.com once for
each of the 80+ ep_* plugins. Uses the new `npm trust github` CLI
(npm >= 11.5.1) so the whole org can be configured in one shot:

  npm login
  bin/setup-trusted-publishers.sh

The script:
- Discovers every non-archived ether/ep_* repo via `gh repo list`
- Maps ep_etherpad to the etherpad-lite repo / releaseEtherpad.yml,
  and every plugin to its same-named repo / test-and-release.yml
- Runs `npm trust github <pkg> --repository <org>/<repo> --file
  <workflow> --yes` for each package
- Supports --dry-run, --packages <comma list>, and --skip-existing
- Verifies npm >= 11.5.1 and that the user is logged in before doing
  anything destructive

Doc updated to feature the script as the recommended setup path,
with manual web-UI steps kept as a fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: don't bump CI Node version to 22 for OIDC

npm 11.5.1 (the version that ships trusted publishing) actually
requires '^20.17.0 || >=22.9.0', not Node 22.14+. The npm docs
recommend Node 22 but only because that's what bundles a recent
enough npm — installing 'npm@latest' on top of Node 20.17+ works
just as well.

The repo already requires Node >= 20.0.0 in engines.node and the
setup-node@v6 'version: 20' input resolves to the latest 20.x
(currently 20.20+), which satisfies npm 11's range. Revert the CI
publish workflows from node-version: 22 back to 20 so this PR does
not raise the Node bar at all.

Doc updated to explain the actual constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:38:23 +01:00
dependabot[bot]
ef1a8d93ed
build(deps): bump jsdom from 29.0.1 to 29.0.2 (#7489)
Bumps [jsdom](https://github.com/jsdom/jsdom) from 29.0.1 to 29.0.2.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v29.0.1...v29.0.2)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 29.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 17:26:52 +01:00
dependabot[bot]
65c3fd3a00
build(deps): bump lru-cache from 11.3.0 to 11.3.2 (#7488)
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.3.0 to 11.3.2.
- [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-lru-cache/compare/v11.3.0...v11.3.2)

---
updated-dependencies:
- dependency-name: lru-cache
  dependency-version: 11.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 17:13:14 +01:00
dependabot[bot]
b00d9d680a
build(deps): bump oidc-provider from 9.7.1 to 9.8.0 (#7487)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.7.1 to 9.8.0.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.7.1...v9.8.0)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 17:13:12 +01:00
dependabot[bot]
d6ee322fa5
build(deps-dev): bump vitest in the dev-dependencies group (#7486)
Bumps the dev-dependencies group with 1 update: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vitest` from 4.1.2 to 4.1.3
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.3/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 17:13:08 +01:00
John McLear
301ae4df2c
feat: add one-line installer script (#7466) (#7485)
* feat: add one-line installer script (#7466)

Adds bin/installer.sh, a small POSIX shell script that:
- Verifies prerequisites (git, Node.js >= 18)
- Installs pnpm globally if missing (with sudo fallback)
- Clones etherpad-lite (configurable branch / dir)
- Runs `pnpm i` and `pnpm run build:etherpad`
- Optionally starts Etherpad if ETHERPAD_RUN=1

Users can now install Etherpad with a single command:

  curl -fsSL https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.sh | sh

README updated to feature the one-liner above the existing
Docker-Compose / manual install instructions.

Closes #7466

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add installer-test workflow + Windows PowerShell installer

- bin/installer.ps1: PowerShell port of installer.sh so the one-liner
  also works on Windows via 'irm ... | iex'.
- .github/workflows/installer-test.yml: end-to-end CI that runs each
  installer against the PR's own commit (via ETHERPAD_REPO/BRANCH env
  vars), verifies clone + node_modules + admin SPA artifacts, and
  smoke-tests by starting Etherpad and curling /api. Runs on
  ubuntu-latest, macos-latest, and windows-latest. Includes a
  shellcheck job for installer.sh.
- README: feature the Windows one-liner alongside the POSIX one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: fix windows smoke test - wrap pnpm in cmd /c

Start-Process can't run pnpm.cmd directly ("not a valid Win32 application").
Wrap it via cmd.exe /c instead, and bump the wait window to 90s for slower
Windows runners. Also dump stderr alongside stdout when the smoke test
fails for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Qodo review on installer (#7485)

Two correctness issues caught by Qodo:

1. Node version mismatch: installer required Node >= 18, but the repo's
   engines.node is >= 20. Bump REQUIRED_NODE_MAJOR to 20 in both shell
   and PowerShell installers, and update the README's quick-install
   prerequisite and Requirements section to match.

2. Branch ignored for existing checkouts: when ETHERPAD_DIR already
   existed, the script ran 'git pull --ff-only' on whatever branch
   happened to be checked out, ignoring ETHERPAD_BRANCH and never
   verifying ETHERPAD_REPO. The existing-dir path now:
   - validates the remote URL matches ETHERPAD_REPO
   - refuses to clobber uncommitted changes (excluding pnpm-lock.yaml,
     which pnpm i rewrites during install)
   - fetches with --tags --prune
   - checks out ETHERPAD_BRANCH as a branch or detaches at it as a tag
   - prints the resulting commit short SHA for clarity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:10:22 +01:00
dependabot[bot]
7011961423
build(deps-dev): bump the dev-dependencies group with 2 updates (#7482)
Bumps the dev-dependencies group with 2 updates: [eslint](https://github.com/eslint/eslint) and [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy).


Updates `eslint` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.1.0...v10.2.0)

Updates `vite-plugin-static-copy` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases)
- [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@4.0.0...vite-plugin-static-copy@4.0.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vite-plugin-static-copy
  dependency-version: 4.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-06 20:56:31 +01:00
dependabot[bot]
8764ae0838
build(deps): bump lru-cache from 11.2.7 to 11.3.0 (#7483)
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.2.7 to 11.3.0.
- [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-lru-cache/compare/v11.2.7...v11.3.0)

---
updated-dependencies:
- dependency-name: lru-cache
  dependency-version: 11.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-06 17:04:01 +01:00
dependabot[bot]
b6f2828ee9
build(deps): bump rate-limiter-flexible from 10.0.1 to 11.0.0 (#7484)
Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 10.0.1 to 11.0.0.
- [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases)
- [Commits](https://github.com/animir/node-rate-limiter-flexible/compare/v10.0.1...v11.0.0)

---
updated-dependencies:
- dependency-name: rate-limiter-flexible
  dependency-version: 11.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-06 17:03:34 +01:00
John McLear
8c1b8b0902
fix: add setters to CJS compatibility layer in Settings (#7481)
The CJS compatibility block added in fd97532 only defined getters,
making settings properties read-only for plugins using require().
Plugins like ep_webrtc need to mutate settings (e.g. requireAuthentication)
in tests. Add setters so CJS consumers can write properties too.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:19:23 +01:00
John McLear
29faec4a04
fix: increase max socket.io message size to 10MB for large pastes (#7474)
* fix: increase max socket.io message size to 10MB for large pastes

The default maxHttpBufferSize of 50KB caused socket.io to drop
connections when pasting >10,000 characters. Increased to 10MB which
safely accommodates large paste operations.

Fixes #4951

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: reduce default maxHttpBufferSize to 1MB

10MB was too generous and creates a DoS vector. 1MB (socket.io's own
default) is sufficient for large pastes while limiting memory abuse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 15:12:08 +01:00
John McLear
2814e5b913
fix: dev mode entrypoint paths respect x-proxy-path header (#7472)
* fix: prevent race condition in session cleanup timeout

When the cleanup timeout fires, check the in-memory exp.real before
reading from the DB. If touch() extended the expiry (but the old
timeout fires late, e.g. on slow CI), reschedule instead of reading
potentially stale cached data from the DB and destroying the session.

Also increased test expiry times so the "touch after eligible for
refresh" test isn't sensitive to event loop delays on slow machines.

Fixes flaky SessionStore test from #7448.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: dev mode entrypoint paths respect x-proxy-path header

In dev mode, the /watch/* script paths were hard-coded as absolute
paths without considering the x-proxy-path header used for subdirectory
reverse proxy setups. This caused 404s for the script tags when hosting
Etherpad on a subdirectory URL (e.g., /pad).

Now reads the x-proxy-path header from the request and prefixes the
entrypoint path, matching how admin.ts handles proxy paths.

Fixes #7137

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: make proxy path tests deterministic in production mode

Tests now verify entrypoint paths and x-proxy-path header handling
in production mode (where tests run) rather than conditionally
asserting only in dev mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* security: sanitize x-proxy-path header to prevent XSS

The header value was injected directly into <script src="...">
without sanitization. An attacker who can set request headers could
inject arbitrary HTML/JS. Now only allows path-safe characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:32:33 +01:00
John McLear
270e3c6576
fix: very old .etherpad imports could break import due to lack of aut… (#7473)
* fix: very old .etherpad imports could break import due to lack of author metadata, allow this now

* test: add regression tests for old .etherpad import without author

Tests that importing an old .etherpad export (circa 2014) where
revision records lack meta.author succeeds without error, and that
getRevisionAuthor returns '' for such revisions.

Covers the fix for #6785.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:31:57 +01:00
John McLear
e488a4338e
fix: use correct path for connection diagnostics POST (#7475)
* fix: use correct path for connection diagnostics POST

The relative path '../ep/pad/connection-diagnostic-info' resolved
incorrectly in subdirectory setups. Use absolute path from the
application root.

Fixes #4191

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: verify connection diagnostics endpoint is reachable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:31:15 +01:00
John McLear
7ce8b167ea
fix: numbered list wrapped lines now indent correctly (#7476)
* fix: numbered list wrapped lines now indent correctly

Changed text-indent to padding-left for ordered list indentation.
text-indent only affects the first line, so wrapped text didn't
align with the numbered content above it.

Fixes #2581

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: verify numbered list uses padding-left instead of text-indent

Regression test for #2581. Verifies that ordered list items use
padding-left (which indents all lines including wrapped ones) rather
than text-indent (which only indents the first line).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:31:09 +01:00
John McLear
6a3094b244
fix: sort language dropdown alphabetically by native name (#7477)
* fix: sort language dropdown alphabetically by native name

Languages in the settings dropdown were ordered by language code,
making it hard to find specific languages. Now sorted alphabetically
by their native display name.

Fixes #3263

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: verify language dropdown is sorted by native name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:28:57 +01:00
translatewiki.net
5320d56b80
Localisation updates from https://translatewiki.net. 2026-04-06 14:04:17 +02:00
John McLear
ac118cfde7
fix: preserve ordered list numbering across bullet interruptions in export (#7470)
* fix: preserve ordered list numbering across unordered list interruptions in export

When ordered lists were interrupted by unordered lists, each new <ol>
segment started at 1 instead of continuing the previous numbering.
Track running counts per indent level and emit start attributes.

Fixes #6471

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: respect explicit start attributes and reset counters per level

- line.start takes priority over counter-based continuation when present
- Counter is seeded from line.start to keep subsequent continuations aligned
- Counters for closed indent levels are cleared when list depth decreases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:24:01 +01:00
John McLear
ef0b257d3e
fix: RTL URL parameter rtl=false now correctly disables RTL mode (#7464)
* fix: RTL URL parameter rtl=false now correctly disables RTL mode

The rtl parameter callback only handled rtl=true (checkVal was 'true'),
so rtl=false was ignored and the layout stayed in RTL from the cookie.
Now accepts any value and sets rtlIsTrue = (val === 'true'). Also
always applies the RTL setting instead of only when true, so switching
from rtl=true to rtl=false takes effect.

Fixes #5559

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only override RTL when explicitly set via URL/server config

The unconditional changeViewOption('rtlIsTrue', false) overwrote
cookie-persisted RTL preferences and language-direction defaults.
Track explicit setting with rtlIsExplicit flag so we only override
when the user or server actually specified an rtl value.

Adds regression tests for rtl=true, rtl=false, and cookie persistence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move RTL override into postAceInit to fix race condition

The RTL changeViewOption call was racing with padeditor.init() — the
async setViewOptions(initialViewOptions) at the end of init overwrote
the URL-param-based RTL setting. Moving it into postAceInit ensures
padeditor is fully initialized. Also switched tests to use Playwright
auto-retrying assertions for robustness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve Playwright test failures for RTL URL parameter

Three issues fixed:
- setCheckbox used .attr('checked') instead of .prop('checked'), so the
  JS checked property was never set and Playwright saw unchecked state
- html10n localized event overwrote RTL setting from URL params and
  cookie preferences; now skips override when either is active
- Server default padOptions.rtl:false was treated as explicit, overwriting
  cookie-persisted RTL; added fromUrl flag to distinguish URL from server

All 94 Playwright tests and 740 backend tests pass locally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:22:57 +01:00
John McLear
24fd1b1fce
fix: correct readFileSync calls in LinkInstaller to fix plugin installation (#7467)
* fix: correct readFileSync calls in LinkInstaller to fix plugin installation

pathToFileURL() was incorrectly wrapping paths passed to readFileSync(),
causing ENOENT errors that were silently caught. Using plain paths with
'utf-8' encoding fixes plugin dependency resolution.

Fixes #6811

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add backend tests for LinkInstaller dependency resolution

Covers the readFileSync fix from the plugin installation bug where
pathToFileURL incorrectly wrapped file paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only track dependency in map after successful setup

Previously dependenciesMap.set() ran after the catch block, marking
dependencies as tracked even when linking or package.json reading
failed. This blocked later cleanup via removeSubDependency().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:22:05 +01:00
John McLear
220ae82086
fix: use pnpm instead of npm in updatePlugins.sh (#7468)
* fix: use pnpm instead of npm in updatePlugins.sh

The script used npm outdated which doesn't work with pnpm workspaces,
and pnpm install which doesn't update existing packages. Changed to
pnpm outdated and pnpm update respectively.

Fixes #6670

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope plugin updates to ep_etherpad-lite and exclude core package

- Use --filter ep_etherpad-lite so pnpm operates on the right workspace
- Exclude ep_etherpad-lite from the plugin list
- Handle pnpm outdated exit codes correctly (returns 1 when outdated)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:21:21 +01:00
John McLear
ad214095ac
fix: handle git submodule in Docker build (#7469)
* fix: handle git submodule in Docker build

When etherpad-lite is checked out as a submodule, .git is a file
(gitlink) instead of a directory, so COPY .git/HEAD fails.  The
previous glob-based workaround (HEA[D], ref[s]) does not work with
buildah (containers/buildah#5742).

Copy the whole .git entry instead and remove it in a RUN step when it
is a submodule gitlink file.  The .dockerignore already strips heavy
objects so the image size is unaffected for normal checkouts.

Fixes #6663

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: document BUILD_ENV=copy bypass for builds without .git

Adds a comment explaining how to build from source tarballs or other
contexts where .git metadata is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:20:58 +01:00
John McLear
605ad28068
fix: prevent race condition in session cleanup timeout (#7471)
When the cleanup timeout fires, check the in-memory exp.real before
reading from the DB. If touch() extended the expiry (but the old
timeout fires late, e.g. on slow CI), reschedule instead of reading
potentially stale cached data from the DB and destroying the session.

Also increased test expiry times so the "touch after eligible for
refresh" test isn't sensitive to event loop delays on slow machines.

Fixes flaky SessionStore test from #7448.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:19:34 +01:00
John McLear
da9f5ac4ee
fix: add periodic cleanup of expired/stale sessions from database (#7448)
* fix: add periodic cleanup of expired/stale sessions from database

SessionStore now runs a periodic cleanup (every hour, plus once on
startup) that removes:
- Sessions with expired cookies (expires date in the past)
- Sessions with no expiry that contain no data beyond the default
  cookie (the empty sessions that accumulate indefinitely per #5010)

Without this, sessions accumulated forever in the database because:
1. Sessions with no maxAge never got an expiry date
2. On server restart, in-memory expiration timeouts were lost
3. There was no mechanism to clean up sessions that were never
   accessed again

Fixes #5010

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve TypeScript error for sessionStore.startCleanup()

Use a local variable for the SessionStore instance to avoid type
narrowing issues with the module-level Store|null variable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Qodo review — chained timeouts, cleanup tests, docs

- Replace setInterval with chained setTimeout to prevent overlapping
  cleanup runs on large databases
- Store and clear startup timeout in shutdown() to prevent leaks
- Add .unref() on all timers so they don't delay process exit
- Fix misleading docstring — cleanup removes empty no-expiry sessions,
  not sessions older than STALE_SESSION_MAX_AGE_MS (removed unused const)
- Add 5 regression tests: expired sessions removed, empty sessions
  removed, sessions with data preserved, valid sessions preserved,
  shutdown cancels timer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add cookie.sessionCleanup setting to control session cleanup

Session cleanup is now gated behind cookie.sessionCleanup (default
true). Admins who want to keep stale sessions can set this to false
in settings.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:12:43 +01:00
John McLear
f8e6b20f43
chore: reduce CI matrix for PRs to prevent runner exhaustion (#7463)
PRs now run a minimal test matrix; full matrix runs on push to develop.

Changes:
- Backend tests: PRs test on Node 24 only (Linux). Windows tests only
  run on push to develop. Reduces from 12 to 2 jobs for PRs.
- Upgrade-from-latest-release: PRs test on Node 24 only (1 job vs 3).
- Frontend admin tests: PRs test on Node 24 only (1 job vs 3).

This reduces PR CI from ~25 jobs to ~10, preventing runner exhaustion
when multiple PRs are merged in succession. The full matrix (3 Node
versions × Linux + Windows) still runs on every push to develop.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 10:14:20 +01:00
John McLear
bd73785431
fix: preserve line attributes on neighboring lines during drag-and-drop (#7461)
* fix: preserve line attributes on neighboring lines during drag-and-drop

On Chrome and Safari, when dragging a line in a list, the browser's
contentEditable engine merges the removed line with its neighbor,
corrupting the neighbor's line attributes (e.g., changing its list
type).

The drop handler now captures line attributes of the lines adjacent
to the dragged content before the browser processes the drop. After
incorporateUserChanges runs, it checks if those attributes were
corrupted and restores them.

Note: this bug cannot be reproduced in Playwright's headless browsers
(DnD in contentEditable iframes isn't supported), so manual testing
with Chrome/Safari is required.

Fixes #3120

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: also save/restore lines with no list type during DnD

Lines with no list attribute can get corrupted to inherit the dragged
line's list type. Now saves all adjacent lines (including those with
no list type) and properly removes corrupted attributes when restoring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: null-safety for atKey in drop handler

Guard against atKey returning null for dynamically inserted nodes
that aren't in the rep.lines index.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:51 +01:00
John McLear
72dc94f1b9
fix: bold text retains formatting after copy-paste (#7460)
* fix: bold text retains formatting after copy-paste

When pasting bold (or italic, underline, etc.) text, the browser's
contentEditable engine normalized the pasted DOM before Etherpad's
content collector could extract the formatting. The pasted HTML
contained proper <b> tags, but the browser flattened the nested
ace-line divs and stripped the inline formatting in the process.

Now the paste handler checks clipboard HTML for formatting tags. If
found, it prevents default browser paste, parses the HTML in a
detached DOMParser document, and inserts the nodes directly into the
editor. This preserves the formatting tags for the content collector.

Fixes #5037

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* security: sanitize pasted HTML to prevent XSS via clipboard

Strip dangerous elements (script, style, iframe, object, embed, form,
link, meta) and event handler attributes (onclick, onerror, etc.) from
pasted HTML before inserting into the editor. Also removes javascript:
URLs from href attributes.

DOMParser doesn't execute scripts, but importNode copies all attributes
including event handlers that execute when inserted into the live
document.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:48 +01:00
John McLear
e55914d197
fix: dead key / compose key no longer eats preceding space (#7459)
On Firefox Linux, when typing accented characters with a dead key or
compose key, the space before the character was being deleted. This
happened because the keydown event for the dead key (keyCode 229)
fired before compositionstart, so inInternationalComposition wasn't
set yet and observeChangesAroundSelection() ran prematurely, capturing
a pre-composition DOM state.

Now treats keyCode 229 (the standard IME/composition keyCode) the same
as other half-character inputs: defers the idle timer and suppresses
normalization until the composition completes.

Fixes #5623

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:46 +01:00
John McLear
833561a1e7
fix: popup notification fits small screens (#7457)
Changed min-width and max-width on .popup-content to use min() with
viewport-relative units so the popup doesn't overflow on screens
narrower than 300px, keeping the close button accessible.

Fixes #7246

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:41 +01:00
John McLear
93c17918a8
fix: ignore errors from browser extensions in error handler and editor init (#7456)
Browser extensions (BitWarden, Dashlane, etc.) inject scripts that can
throw errors caught by Etherpad's global exception handler, showing a
scary error popup and sometimes blocking the editor from loading.

Two fixes:
- globalExceptionHandler (pad_utils.ts): Skip errors where the source
  URL matches moz-extension://, chrome-extension://, or
  safari-extension:// patterns.
- Ace2Editor.init (ace.ts): The eventFired() error callback now checks
  if the error event's target src is a browser extension and ignores
  it, preventing extension-injected script failures from killing
  editor initialization.

Fixes #6802

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:38 +01:00
John McLear
af03259555
fix: POST API requests with JSON body no longer time out (#7455)
* fix: POST API requests with JSON body no longer time out

When express.json() middleware parses the request body before the
OpenAPI handler runs, formidable's IncomingForm hangs forever waiting
for stream data that was already consumed. Now checks req.body first
and only falls back to formidable for multipart/form-data requests.

Also fixed case-insensitive method check (c.request.method may be
uppercase depending on openapi-backend version).

Fixes #7127

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: handle empty JSON body and missing method safely

- Remove Object.keys().length > 0 check on req.body so empty JSON
  objects ({}) don't fall through to formidable (which would hang)
- Guard c.request.method with fallback to empty string to prevent
  TypeError if method is undefined

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* security: prevent parameter pollution by excluding headers from field merge

Previously Object.assign merged headers, params, query, and formData
into a single fields object. This allowed POST body parameters to
override security-sensitive headers like Authorization, or headers to
pollute API parameter values.

Now only merges params, query, and formData. The Authorization header
is passed explicitly as a fallback for legacy API key authentication,
but cannot be overridden by body/query parameters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:35 +01:00
John McLear
36d61b2e29
fix: locale issues — custom strings caching, lang race, and window._() (#7454)
* fix: customLocaleStrings not applied due to aggressive locale caching

The admin panel's i18next backend used fetch with cache: "force-cache",
causing the browser to serve stale locale JSON even after the server
restarted with new customLocaleStrings in settings.json. The server
already sets appropriate Cache-Control headers (max-age based on
settings.maxAge), so the client-side force-cache was redundant and
prevented custom strings from appearing.

Fixes #6390

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: URL lang param now reliably overrides server default language

getParams() was processing server options first and URL params second,
both calling html10n.localize() for the lang setting. Since localize()
is async, the two calls raced and the result was nondeterministic.

Now processes each setting once: URL param wins if present, otherwise
falls back to server option. This eliminates the race condition.

Fixes #5510

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: window._() localization function always available for plugins

The html10n gettext shortcut window._ was only set if window._ was
undefined, but underscore.js was already setting it via the esbuild
bundle. Since internal code uses underscore via require() not window._,
it's safe to always set window._ to html10n.get so plugins can use
window._() for localization in hooks like documentReady.

Fixes #6627

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:33 +01:00
John McLear
474918a881
feat: make cookie names configurable with prefix setting (#7450)
* feat: make cookie names configurable with prefix setting

Add cookie.prefix setting (default "ep_") that gets prepended to all
cookie names set by Etherpad. This prevents conflicts with other
applications on the same domain that use generic cookie names like
"sessionID" or "token".

Affected cookies: token, sessionID, language, prefs/prefsHttp,
express_sid.

The prefix is passed to the client via clientVars.cookiePrefix in the
bootstrap templates so it's available before the handshake. Server-side
cookie reads fall back to unprefixed names for backward compatibility
during migration.

Fixes #664

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: default cookie prefix to empty string for backward compatibility

Changing the default to "ep_" would invalidate all existing sessions
on upgrade since express-session only looks for the configured cookie
name. Default to "" (no prefix) so upgrades are non-breaking — users
opt-in to prefixed names by setting cookie.prefix in settings.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Qodo review — cookie prefix migration and fallbacks

- l10n.ts: Read prefixed language cookie with fallback to unprefixed
- welcome.ts: Use cookiePrefix for token transfer reads
- timeslider.ts: Use prefix for sessionID in socket messages
- pad_cookie.ts: Fall back to unprefixed prefs cookie for migration
- indexBootstrap.js: Pass cookiePrefix via clientVars to welcome page
- specialpages.ts: Pass settings to indexBootstrap template

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: escape regex metacharacters in cookie prefix, document Vite hardcode

- l10n.ts: Escape special regex characters in cookiePrefix before using
  it in RegExp constructor to prevent runtime errors
- padViteBootstrap.js: Add comment noting the hardcoded prefix is
  dev-only and must match settings.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* security: validate cookie prefix to prevent header injection

Reject cookie.prefix values containing characters outside
[a-zA-Z0-9_-] to prevent HTTP header injection via crafted cookie
names (e.g., \r\n sequences). Falls back to empty prefix with an
error log.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:29 +01:00
John McLear
f0b84cc1d0
fix: list bugs — indent export, renumber performance, and batching (#7449)
* fix: list bugs — indent export, renumber performance, and batching

Addresses four list-related bugs:

#4426: Indented text exports as bulleted lists. Added list-style-type:none
to indent-type <ul> elements in ExportHtml.ts so exported indented content
doesn't show bullet markers.

#3504 / #5546: List operations (indent, outdent, toggle) on large lists
are O(n²) because renumberList() runs after each individual line change.
Added _skipRenumber batching flag to setLineListType() — bulk operations
in doInsertList() and doIndentOutdent() now set all line types first,
then renumber once at the end.

#6471: Ordered list numbering in exports — the start attribute is already
read from the pad's atext during export. The client-side renumberList()
correctly sets start attributes which are persisted. Added export test
to verify numbering is preserved across bullet interruptions.

Fixes #4426, #3504, #5546
Related: #6471

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Qodo review — exception safety, batch removal, renumber scope

- Wrap _skipRenumber in try/finally to prevent permanent disabling on error
- Move list removal (togglingOff) into the batched mods array instead of
  calling setLineListType directly (fixes O(n²) for list removal)
- Use firstLine instead of mods[0][0] for renumbering since the first
  mod may be an indent/removal that renumberList skips
- Rewrite indent export test to actually create indent lines via setHTML
  and unconditionally assert list-style-type:none is present

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rewrite export tests to use importHtml/exportHtml directly

The HTTP API approach (setHTML via supertest) was hanging when tests
ran standalone because the API endpoint waited for something in the
request pipeline. Using importHtml.setPadHTML and exportHtml.getPadHTML
directly is faster and more reliable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update importexport test to expect list-style-type on indent ul

The indent export fix adds style="list-style-type: none;" to indent
<ul> elements, which broke the golden test string comparison.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:58:27 +01:00
John McLear
502a3b93e0
fix: accessibility — keyboard trap, screen reader support, aria-live (#7451)
* fix: accessibility — keyboard trap, screen reader support, aria-live

Three accessibility fixes:

#6581 (WCAG 2.1.2 keyboard trap): Escape key now moves focus from the
editor to the first toolbar button, giving keyboard-only users an
escape route. Added a screen-reader-only hint about Escape and Alt+F9.

#7255 (screen reader access): Added role="textbox", aria-multiline="true",
and aria-label="Pad content" to the contenteditable body so screen
readers can identify and interact with the editor content. Fixed
non-standard aria-role="document" to role="document" in pad.html.

#5695 (aria-live character echo): Removed aria-live="assertive" from
every line div in domline.ts. This was causing screen readers to
announce every character typed, overriding users' keyboard echo
settings. The attribute was added in PR #5149 for JAWS compatibility
but aria-live on individual contenteditable lines is a misuse.

Also added .sr-only CSS utility class for visually hidden content.

Fixes #6581, #7255, #5695

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Escape closes gritters first, only exits editor if nothing to dismiss

If gritter popups are visible, Escape closes them and keeps focus in
the editor. Only when there are no popups does Escape move focus to
the toolbar for keyboard trap escape.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Qodo review — keyboard hint in iframe, aria-readonly

- Move keyboard hint (Escape/Alt+F9) inside the inner iframe with
  aria-describedby so screen readers announce it when focusing the
  editor. Previously it was on the outer editorcontainer which is a
  different document context.
- Set aria-readonly on the editor body when in readonly mode so screen
  readers correctly convey editability state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:57:59 +01:00
John McLear
2c0c4df90e
chore: show individual test names in CI Playwright output (#7462)
Add 'list' reporter alongside 'github' reporter in CI. The 'github'
reporter only shows failures as PR annotations. The 'list' reporter
shows each test with pass/fail status in the log output, making it
easy to see which tests ran and passed.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:57:49 +01:00
John McLear
66249b5d7e
fix: correct numConnectedUsers count for joining user (#7453)
numConnectedUsers in CLIENT_VARS was computed from roomSockets.length
before the new socket joined the room, so the joining user always saw
a count one less than the actual number. Added +1 to include the
joining user in the count.

Fixes #6145

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:27:16 +01:00
John McLear
4896b5286a
fix: add padId to padUpdate/padCreate hook context (#7452)
The pad object's toJSON() intentionally strips the id property (since
it's part of the database key), which caused confusion when plugins
serialized the hook context. Adding padId as a top-level property on
the hook context makes it directly accessible without relying on the
pad object's internal properties.

Fixes #5814

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 00:25:20 +01:00
John McLear
f7e4100aba
fix: appendText API now attributes text to the specified author (#7446)
* fix: appendText API now attributes text to the specified author

spliceText() was calling makeSplice() without passing author attributes,
so inserted text had no authorship attribution in the changeset — even
though the authorId was recorded in the revision metadata. Now passes
[['author', authorId]] and the pool to makeSplice() so the changeset
ops carry the author attribute, making the text show the author's color
in the editor and appear in listAuthorsOfPad.

Also fixed the same issue in pad init (first changeset creation) and
updated PadType interface to include the authorId parameter.

Fixes #6873

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: assert API response code on createPad and anonymous appendText

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:28:09 +01:00
John McLear
928eef8978
fix: consecutive numbering fails after indented sub-bullets (#7447)
* fix: consecutive numbering fails after indented sub-bullets

The applyNumberList() function in renumberList() checked
listType[0] === 'indent' but after regex exec, listType[0] is the
full match (e.g., "indent1"), never just "indent". Changed to
listType[1] which is the capture group containing just the type name.

This caused indent-type lines to not be recognized during renumbering,
breaking the numbering sequence when numbered lists followed indented
content.

Fixes #5718

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: assert sub-bullet indent and remove redundant waitForTimeout

- Assert .list-bullet2 exists after Tab to verify indent precondition
- Remove waitForTimeout(500) since toHaveAttribute already waits 5s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:19:08 +01:00
John McLear
f186ea9d2c
fix: skip identity changesets during timeslider playback (#7438)
* fix: skip identity changesets during timeslider playback

When a pad's revision history contains an identity changeset (Z:N>0$,
representing no actual change), the timeslider playback would crash or
break because broadcast.ts tried to apply it via mutateAttributionLines
and mutateTextLines.

Now all three applyChangeset call sites in broadcast.ts check for
identity changesets using the existing isIdentity() helper and skip
them. This also prevents errors when compose() produces an identity
changeset from multiple revisions that cancel each other out.

Fixes: https://github.com/ether/etherpad-lite/issues/5214

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move identity changeset check inside applyChangeset

Move the isIdentity() guard from the call sites into applyChangeset()
itself, so that identity changesets still advance currentRevision,
currentTime, slider position, and author UI — just skipping the
mutation (mutateAttributionLines/mutateTextLines). This prevents the
timeslider from getting stuck on a stale revision when an identity
changeset is encountered.

Also removes unused `identity` import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: improve timeslider identity changeset test coverage

- Verify slider position advances during playback (confirms revisions
  including identity changesets are processed, not skipped)
- Scrub through every revision individually instead of just rev 0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: timeslider playback test starts from rev 0

The test was starting playback from the latest revision, so the slider
had nowhere to advance — causing the position assertion to fail in CI.
Now navigates to #0 first so playback progresses through all revisions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove stale identity-skip comment from goToRevision

The isIdentity() check was moved inside applyChangeset() but the old
comment remained at the call sites, creating a misleading code/comment
mismatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:08:45 +01:00
John McLear
8b7155b612
fix: createDiffHTML API fails with "Not a changeset: undefined" (#7445)
* fix: createDiffHTML API fails with "Not a changeset: undefined"

Root cause: An empty prototype override (`PadDiff.prototype._createDeletionChangeset = function() {}`)
silently replaced the real class method with a no-op returning undefined.
This caused `applyToAText(undefined, ...)` to throw "Not a changeset".

Also fixed a crash when `startRev === endRev`: the `self` property used
to access `_authors` was only initialized inside `_addAuthors()`, which
is never called when there are no changesets to process. Replaced all
`this.self!._authors` with direct `this._authors` access.

Fixes #6847

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Qodo review — random pad ID and assert cleanup

- Use random ID instead of Date.now() to avoid collisions in parallel runs
- Assert HTTP 200 on deletePad in after() hook

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:25:26 +01:00
John McLear
5199319cea
fix: secure-by-default error sanitization with dedup and regression tests (#7434)
* fix: suppress internal error details from users in production mode

In production mode (NODE_ENV=production), the client-side error handler
now shows a generic "reload the page" message with just the ErrorId
instead of leaking internal details like error messages, file paths,
line numbers, stack traces, and user agent strings.

In development mode, the full error details are still shown for
debugging.

The basic_error_handler (pre-initialization errors) now always shows a
generic message and logs details to the console instead of displaying
them in the DOM.

The server-side jserror endpoint still receives full error details for
server-side logging — only the user-facing display is suppressed.

Fixes: https://github.com/ether/etherpad-lite/issues/5765

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: secure-by-default error sanitization and dedup fixes

Address Qodo review concerns:
- Flip mode check to `!== 'development'` (secure by default) so errors
  before CLIENT_VARS handshake hide internal details
- Default server-side mode to 'development' when NODE_ENV is unset
- Replace DOM-based gritter dedup with in-memory Set so dedup works in
  production mode (where .error-msg element is absent)
- Add Playwright regression tests for error sanitization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: revert server-side mode default to preserve secure-by-default

Don't default mode to 'development' when NODE_ENV is unset — that would
defeat the client-side secure-by-default check. Let mode be undefined so
the client's `!== 'development'` check correctly hides error details.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: report early page load errors to server via sendBeacon

basic_error_handler now sends error details to ../jserror using
navigator.sendBeacon with FormData, matching the format expected by
the server's Formidable parser. Includes an errorId shown to the user
for correlation with server logs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* revert: restore original basic_error_handler with full error details

The basic_error_handler runs before the pad framework loads, so it
should show full details to help developers debug bootstrap failures.
Removes the sendBeacon reporting and error sanitization added in the
previous commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:27:44 +01:00
John McLear
fd6b3513c5
docs: add local test running guide to AGENTS.md (#7442)
* docs: add detailed local test running guide to AGENTS.md

Expand the Testing & Validation section with step-by-step instructions
for running backend (Mocha), frontend E2E (Playwright), and admin panel
tests locally, including prerequisites and single-file examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add Playwright browser prerequisite check to AGENTS.md

Agents and developers should verify Playwright browsers and system
dependencies are installed before running frontend/admin tests to
avoid silent failures and timeouts (especially webkit).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix plugin test path and browser list per review

- Plugin tests are at repo-root node_modules/, not src/node_modules/
- Playwright runs chromium and firefox only (webkit is disabled)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix install-deps working directory to run from src/

Playwright is a devDependency of the src workspace, so install-deps
must also run from src/ to match the correct Playwright version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:23:40 +01:00
dependabot[bot]
62002302a7
build(deps-dev): bump the dev-dependencies group with 4 updates (#7443)
Bumps the dev-dependencies group with 4 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon), [react-hook-form](https://github.com/react-hook-form/react-hook-form) and [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom).


Updates `@types/node` from 25.5.0 to 25.5.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/sinon` from 21.0.0 to 21.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sinon)

Updates `react-hook-form` from 7.72.0 to 7.72.1
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.72.0...v7.72.1)

Updates `react-router-dom` from 7.13.2 to 7.14.0
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.14.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/sinon"
  dependency-version: 21.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.72.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.14.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 20:10:05 +01:00
dependabot[bot]
bd60279b8b
build(deps): bump esbuild from 0.27.5 to 0.28.0 (#7444)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.27.5 to 0.28.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.5...v0.28.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 16:37:14 +01:00
John McLear
712bfe6ac4
fix: PageDown now advances caret by a full page of lines (#7437)
PageDown was broken — it moved the caret to the last visible line of
the current viewport instead of advancing by one page. This caused it
to get "stuck" at the bottom of the viewport.

The old code set the caret to oldVisibleLineRange[1] - 1 (the last
visible line), which was essentially a no-op for scrolling. The fix
mirrors the PageUp logic: advance/retreat by numberOfLinesInViewport.

Also simplified the clamping logic for both selStart and selEnd.

Fixes: https://github.com/ether/etherpad-lite/issues/6710

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 07:15:58 +01:00
dependabot[bot]
4ce2a1acd1
build(deps): bump esbuild from 0.27.4 to 0.27.5 (#7440)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.27.4 to 0.27.5.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.4...v0.27.5)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.27.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 07:15:31 +01:00
John McLear
d5acbb31ae
fix: filter already-deleted sessions when deleting a group (#7435)
* fix: filter already-deleted sessions when deleting a group

deleteSession uses setSub(..., undefined) to remove session references
from group2sessions and author2sessions, but this can leave null entries
in the sessionIDs object. When deleteGroup later iterates Object.keys
of sessionIDs and calls deleteSession on each, it throws "sessionID
does not exist" for the already-deleted sessions.

Now deleteGroup filters out null/falsy session entries before attempting
to delete them.

Fixes: https://github.com/ether/etherpad-lite/issues/5798

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add regression test for deleteGroup after deleteSession (#5798)

Creates a group, author, and session, then deletes the session first,
then deletes the group. Without the fix, deleteGroup would throw
"sessionID does not exist" when encountering the null session entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 07:13:46 +01:00