Skip to content

Commit da00ad7

Browse files
author
Etherpad Release Bot
committed
Merge branch 'develop'
2 parents fa1c6b2 + 0b392e1 commit da00ad7

101 files changed

Lines changed: 8421 additions & 1727 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ If applicable, add screenshots to help explain your problem.
3030
- OS: [e.g., Ubuntu 20.04]
3131
- Node.js version (`node --version`):
3232
- npm version (`npm --version`):
33-
- Is the server free of plugins:
33+
- Is the server free of plugins:
34+
- Are you using any abstraction IE docker?
3435

3536
**Desktop (please complete the following information):**
3637
- OS: [e.g. iOS]

.github/workflows/backend-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ jobs:
146146
ep_hash_auth
147147
ep_headings2
148148
ep_markdown
149-
ep_readonly_guest
149+
ep_guest
150150
ep_set_title_on_pad
151151
ep_spellcheck
152152
ep_subscript_and_superscript
@@ -289,7 +289,7 @@ jobs:
289289
ep_hash_auth
290290
ep_headings2
291291
ep_markdown
292-
ep_readonly_guest
292+
ep_guest
293293
ep_set_title_on_pad
294294
ep_spellcheck
295295
ep_subscript_and_superscript

.github/workflows/docker.yml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,13 @@ jobs:
6666
name: Test
6767
working-directory: etherpad
6868
run: |
69-
docker run --rm -d -p 9001:9001 --name test ${{ env.TEST_TAG }}
69+
# ADMIN_PASSWORD provisions settings.json.docker's admin user so
70+
# the adminSettings_7819 container spec can authenticate against
71+
# /admin and the /settings socket. pad.js doesn't touch /admin
72+
# so the existing API specs are unaffected.
73+
docker run --rm -d -p 9001:9001 \
74+
-e ADMIN_PASSWORD=changeme1 \
75+
--name test ${{ env.TEST_TAG }}
7076
./bin/installDeps.sh
7177
docker logs -f test &
7278
while true; do
@@ -79,6 +85,34 @@ jobs:
7985
esac
8086
done
8187
(cd src && pnpm run test-container)
88+
# Regression for #7819. adminSettings_7819.ts saves a marker via
89+
# the admin /settings socket and intentionally leaves it on
90+
# disk. We assert here that the save actually hit the file
91+
# (mocha only sees the next `load` reply — this catches a
92+
# scenario where the load is served from cache and the file
93+
# never actually changed).
94+
docker exec test grep -q persist-marker-7819 /opt/etherpad-lite/settings.json || {
95+
echo "[#7819] socket-driven save did NOT reach /opt/etherpad-lite/settings.json on disk"
96+
docker exec test cat /opt/etherpad-lite/settings.json | head -50
97+
exit 1
98+
}
99+
# Now prove the on-disk file survives an in-place container
100+
# restart. This is the scenario a docker-compose user with
101+
# `restart: always` hits on every host reboot.
102+
docker restart test >/dev/null
103+
for i in $(seq 1 60); do
104+
status=$(docker container inspect -f '{{.State.Health.Status}}' test 2>/dev/null) || { docker logs test; exit 1; }
105+
case ${status} in
106+
healthy) break;;
107+
starting) sleep 2;;
108+
*) docker logs test; exit 1;;
109+
esac
110+
done
111+
docker exec test grep -q persist-marker-7819 /opt/etherpad-lite/settings.json || {
112+
echo "[#7819 REGRESSION] settings.json was reset on docker restart — ep_oauth block vanished"
113+
docker logs test
114+
exit 1
115+
}
82116
docker rm -f test
83117
git clean -dxf .
84118
-

.github/workflows/frontend-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ jobs:
219219
ep_hash_auth
220220
ep_headings2
221221
ep_markdown
222-
ep_readonly_guest
222+
ep_guest
223223
ep_set_title_on_pad
224224
ep_spellcheck
225225
ep_subscript_and_superscript
@@ -308,7 +308,7 @@ jobs:
308308
ep_hash_auth
309309
ep_headings2
310310
ep_markdown
311-
ep_readonly_guest
311+
ep_guest
312312
ep_set_title_on_pad
313313
ep_spellcheck
314314
ep_subscript_and_superscript

.github/workflows/load-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
ep_hash_auth
9494
ep_headings2
9595
ep_markdown
96-
ep_readonly_guest
96+
ep_guest
9797
ep_set_title_on_pad
9898
ep_spellcheck
9999
ep_subscript_and_superscript

.github/workflows/upgrade-from-latest-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676
ep_hash_auth
7777
ep_headings2
7878
ep_markdown
79-
ep_readonly_guest
79+
ep_guest
8080
ep_set_title_on_pad
8181
ep_spellcheck
8282
ep_subscript_and_superscript

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,42 @@
1+
# 3.2.0
2+
3+
3.2 adds first-class reverse-proxy / ingress support — `X-Forwarded-Prefix` and `X-Ingress-Path` are now honoured under `trustProxy`, so Etherpad can live under a subpath (Traefik, Nginx, Kubernetes Ingress) without breaking the PWA manifest, social-meta URLs, or any of the bootstrap asset links. The admin settings page learns to show *resolved* runtime values next to `${VAR:default}` placeholders, the v3.1.0 admin pad-list filter chips now apply server-side (so "show empty pads" no longer returns 0–12 of hundreds), and the v3.1.0 redesigned outdated-version gritter actually fires in production now (the session-based author lookup it shipped with always returned null for pad visitors).
4+
5+
### Notable enhancements
6+
7+
- **HTTP — accept `X-Forwarded-Prefix` and `X-Ingress-Path` under `trustProxy` (#7802 / #7806).** With `trustProxy: true`, Etherpad now honours `X-Forwarded-Prefix` (de-facto Traefik / Spring) and `X-Ingress-Path` (Kubernetes Ingress) in addition to the prefix it already inferred from the request path. The shared `sanitizeProxyPath` helper added in 3.1.0 (defence-in-depth: `[A-Za-z0-9_./-]` only, `//+` collapsed, `..` traversal rejected) is extended to the new headers and applied consistently across `/manifest.json`, `socialMeta` `og:url` / `og:image`, and the `index.html` / `pad.html` / `timeslider.html` / `export_html.html` templates (manifest links, jslicense links, reconnect URLs). A pre-existing `..` segment-count miscalculation in `pad.html` / `timeslider.html` that broke the manifest link when served from a deep subpath is also fixed in passing. New end-to-end suite covers the prefix-applied / prefix-ignored matrix under `trustProxy=true|false` for both header names. `settings.json.template` documents the new headers alongside the existing `trustProxy` notes.
8+
- **Admin settings — resolved runtime values surface on env-pill chips (#7803 / #7807).** The `/admin/settings` socket payload now carries a new `resolved` field alongside the existing raw-file `results` blob, carrying the actual in-memory settings module run through a new redactor (`AdminSettingsRedact`) that replaces known-sensitive paths (`users.*.password`, `dbSettings.password`, `sso.clients[*].client_secret`, `sessionKey`, …) with `[REDACTED]`. The admin SPA's `EnvPill` renders a `→ active value` chip when the path is resolved, or `→ ••••••` with a redacted tooltip when the server returned the sentinel — so `port: ${PORT:9001}` now shows `→ 9001` (or whatever the live value is) instead of silently falling back to the template default. Old admin SPAs that don't read `resolved` continue to work; the save round-trip is unchanged so `${VAR:default}` literals are still preserved verbatim on disk. The admin test script glob picks up `.test.tsx` alongside `.test.ts` so the new `EnvPill` and `resolveByPath` tests run under `tsx --test`.
9+
10+
### Notable fixes
11+
12+
- **Admin pads — filter chip now applies server-side, before pagination (#7798).** The 3.1.0 admin pad-list filter chips (`active` / `recent` / `empty` / `stale`) ran on the client *after* the 12-row page slice had already arrived. On a deployment with hundreds of pads, clicking "empty pads" on page 1 only matched the 0–12 empties that happened to land in the current page, with the pagination footer reporting nonsense totals (reported on a 3.1.0 deployment). The filter is now part of the `padLoad` socket query — pattern filter on names runs first (cheap), metadata hydration for the matching pad universe is gated on a non-`all` filter or a non-`padName` sort and runs under a 16-way concurrency cap (was unbounded `Promise.all`, which fanned out to thousands of in-flight `padManager.getPad()` reads on busy deployments), then the filter chip, then sort + slice. `total` reflects the filtered universe so the footer makes sense. Older admin clients that don't send `filter` keep working — the server defaults to `all`. The `if/else if` ladder that duplicated the hydrate-and-sort loop per `sortBy` is folded into one pipeline with a single comparator switch.
13+
- **Pad outdated notice — author now resolved from token cookie, not session (Qodo #7804 / #7805).** The 3.1.0 redesigned outdated-version gritter never fired in production. `resolveRequestAuthor()` looked for an `authorID` on `req.session.user`, which Etherpad does not populate for pad visitors (express-session only carries the admin-login user), so `computeOutdated()` always returned EMPTY. The lookup now mirrors how the socket.io handshake resolves pad-visitor identity — read the HttpOnly `token` (or `<prefix>token`) cookie and call `authorManager.getAuthorId(token, user)` via a dynamic import (same circular-init guard pattern the file already uses for `PadManager`). The admin OpenAPI document gains a `description` note clarifying that `/api/version-status` is a public pad-side endpoint that lives in the admin doc only because it shares the same internal route registration.
14+
- **Localisation — silence spurious "could not translate element content" warning (#7797).** `<select data-l10n-id="…">` with `<option>` element children — the pattern used by `ep_headings2`, `ep_align`, `ep_font_size`, `ep_font_family`, … — used to drop into the textContent branch of `html10n.translateNode`, hunt for a text-node child to overwrite, find none, and emit `Unexpected error: could not translate element content for key …` on every pad load. The `SELECT` / `INPUT` / `TEXTAREA` aria-label fallback already lived inside the same else-branch *after* the warning, so the accessible name landed correctly but the noisy console line still fired. Form-control elements now short-circuit into the aria-label path *before* the text-node hunt — aria-label is the only sensible localization target for these elements (a `<select>`'s text is its `<option>` labels, not its own name). Closes the console warning reported on Etherpad 3.1.0.
15+
16+
### Internal / contributor-facing
17+
18+
- **CI — swap archived `ep_readonly_guest` for `ep_guest` in the plugin matrix (#7795 / #7808).** `ep_readonly_guest` is archived (read-only on GitHub) and its `authenticate` hook unconditionally swapped `req.session.user` with a read-only guest, *even when the request carried an HTTP Authorization header*. That silently demoted admin login attempts and stalled the `anonymizeAuthorSocket` tests for 14 min/run on every with-plugins CI matrix. The pre-fix theory from 3.1.0 (#7796) blamed `ep_hash_auth.handleMessage`; that was a red herring — `handleMessage` only fires on the `/pad` namespace, never on `/settings`. `ep_guest` is the maintained successor (same authors, same purpose); 1.0.72 on npm already defers to basic auth / admin paths. Swapping the matrix unblocks the `anonymizeAuthorSocket` suite on Linux, Windows, and the upgrade-from-latest-release workflow. The runtime probe added in #7796 stays — it still catches any other authenticate-hook plugin that rejects the test's plain-text credentials (e.g. a future hashed-only plugin).
19+
- **Tests — admin `saveSettings` round-trip + cross-restart persistence (#7819 / #7820 / #7821).** The admin `saveSettings` socket had zero direct backend coverage and the existing e2e "restart works" test only checked that the page renders after a restart, neither of which catches a deployment that resets `settings.json` on restart, nor the user-visible workflow that triggered #7819 (add a top-level plugin block via Raw, save, watch it disappear). Three new backend specs (`adminSettingsSave.ts`) verify byte-for-byte write, top-level-block augmentation round-tripping through the next `load`, and `/* */` comments surviving the write path. A new e2e spec mirrors the #7819 user workflow — open Raw, prepend an `ep_oauth`-shaped top-level block, save, `restartEtherpad()`, re-login, confirm the block is still in Raw and surfaces as its own Form-view section (`Ep oauth` from `humanize()`). A separate `docker.yml` job (`adminSettings_7819.ts`) authenticates via `POST /admin-auth/` (always-requireAdmin, regardless of `settings.requireAuthentication`), saves a hand-built minimal-but-viable settings document containing a marker block, `docker exec test grep`s for it, `docker restart`s the container, waits for the health probe, and re-greps. Both checks must pass.
20+
- **Bug report template** now asks contributors whether the abstraction in their proposed fix matches the rest of the codebase, to head off premature-generalisation fixes earlier in review.
21+
22+
### Dependencies
23+
24+
- `ueberdb2` 6.0.3 → 6.1.2 (two patch releases of cleanup on top of the 6.1.0 `findKeysPaged` API that the 3.1.0 sessionstorage OOM fix relies on).
25+
- `semver` 7.8.0 → 7.8.1, `lru-cache` 11.3.6 → 11.5.0, `@elastic/elasticsearch` 9.4.0 → 9.4.1, `pg` 8.20.0 → 8.21.0, `openapi-backend` 5.16.1 → 5.17.0, `tsx` 4.22.0 → 4.22.3, `@tanstack/react-query` 5.100.10 → 5.100.11 + `@tanstack/react-query-devtools`, `js-cookie` 3.0.6 → 3.0.7, plus two dev-dependency group bumps.
26+
27+
### Localisation
28+
29+
- Multiple updates from translatewiki.net.
30+
131
# 3.1.0
232

333
3.1 ships the self-update programme's **Tier 4 — autonomous in a maintenance window** for real (the v3.0.0 notes documented the design; this is the release the code actually lands in), adds first-class SMTP delivery so update failures email the admin, and bundles a defence-in-depth pass across the HTTP/API entry points. Two new admin-facing escape hatches arrive: a preflight check that aborts an update *before* it mutates the working tree when the target tag's `engines.node` doesn't match the running runtime, and email notifications for every auto-rollback / preflight outcome (not just the terminal `rollback-failed` state).
434

535
### Notable enhancements
636

37+
- **pad: Outdated-version notice redesigned (#7799).** The persistent "severely outdated" banner is replaced by a dismissable gritter notification (auto-fades after 8 seconds), shown only to a pad's first author and only when the server is at least one minor version behind the latest released version. Patch-only deltas no longer fire the notice. The `vulnerable-below` directive scraping, the `severe` and `vulnerable` enum values, and the `vulnerableBelow` state field have been removed.
38+
- **API: `GET /api/version-status` updated (#7799).** Now accepts an optional `?padId=<id>` query parameter and returns `{outdated: "minor" | null, isFirstAuthor: boolean}`. The `severe` and `vulnerable` enum values are gone. Results are cached per `(padId, authorId)` for 60 seconds.
39+
740
- **Self-update — Tier 4 (autonomous in a maintenance window).** Set `updates.tier: "autonomous"` together with `updates.maintenanceWindow: {"start":"HH:MM","end":"HH:MM","tz":"local"|"utc"}` to constrain autonomous updates to a nightly window. The scheduler snaps `scheduledFor` forward to the next window opening when grace would otherwise land outside the window, and defers the fire when the window has closed by the timer callback. Cross-midnight windows (`end < start`) are supported; DST transitions are absorbed by host wall-clock arithmetic. A missing or malformed window degrades the policy to Tier 3 with an explicit `policy.reason` of `maintenance-window-missing` / `maintenance-window-invalid`; an admin banner surfaces the misconfiguration so autonomous behaviour is not silently disabled. The admin update page shows a "Maintenance window" section with the parsed window summary, the next opening, and a "deferred until <iso>" subtitle on the scheduled panel when the timer has been snapped forward. Closes #7607 (#7753).
841
- **Updater — real SMTP via nodemailer (new top-level `mail.*` block).** Replaces the "(would send email)" stub. New settings: `mail.host`, `mail.port`, `mail.secure`, `mail.from`, `mail.auth.{user,pass}`. `mail.host=null` keeps the legacy log-only behaviour. The `nodemailer` dependency is lazy-imported on first send so installs that don't configure mail pay no runtime cost; the transport is cached on the full SMTP options tuple so a `reloadSettings()` change to host/port/credentials invalidates the cache. `settings.json.docker` reads `MAIL_HOST` / `MAIL_FROM` / `MAIL_PORT` / `MAIL_SECURE` from env. Send errors are logged warn and swallowed so a transient SMTP failure can never poison the updater state machine.
942
- **Updater — preflight against the target tag's `engines.node`.** Before mutating the working tree, `runPreflight` now runs `git show <tag>:package.json` and verifies `process.versions.node` satisfies the target's `engines.node`. A mismatch fails cleanly at `preflight-failed` with the detail `target requires Node >=X, running Y` — no drain, no restart, no rollback. The check runs *after* signature verification so we only trust signed `package.json`. New `PreflightReason: 'node-engine-mismatch'`.
@@ -14,6 +47,7 @@
1447

1548
- **Export HTML — ordered-list counter no longer poisoned by a sibling unordered list.** When an ordered-list level was the only consumer of `olItemCounts`, closing *any* list at that depth (including a `<ul>` that happened to share the level) reset the counter to 0. A subsequent unrelated `<ol>` at the same depth then took the "counter exists but is 0" branch and emitted `<ol class="...">` without the `start=` attribute. The reset is now gated on `line.listTypeName === 'number'` so closing an unordered list never touches the ol bookkeeping. Fixes #7786 / #7787 (#7791).
1649
- **Export — bad `:rev` returns a meaningful 500 body, not Express's HTML error page.** A non-numeric `:rev` (e.g. `/p/foo/test1/export/txt`) reached `checkValidRev` which throws `CustomError('rev is not a number', 'apierror')`; the message fell through `.catch(next)` and Express's default renderer returned an HTML 500 page. The route handler now catches the apierror and emits `err.message` as a deterministic `text/plain` 500. As a follow-up, `checkValidRev` runs *before* `res.attachment()` so an invalid rev no longer leaves a `Content-Disposition` header in place (browsers were offering to save the error message as a file), and unrelated export failures (conversion, fs, soffice) are surfaced as text/plain rather than the HTML stack page. Fixes #7788 (#7792).
50+
- **Session cleanup no longer OOMs on huge sessionstorage tables.** Pre-2.7.3 `SessionStore._cleanup()` issued a single unbounded `findKeys('sessionstorage:*', null)` that materialised every key into one JS array; on a decade-old MariaDB install with millions of stale sessions the mysql2 driver retained the rows on the pool connection while the JS array dominated heap, OOMing the process within ~15 minutes of boot. Cleanup now pages the keyspace in 500-key batches via the new `findKeysPaged` API on ueberdb2 6.1.0 (DB-side ranged query on mysql/postgres, JS-side fallback elsewhere), yielding to the event loop between pages. A single run is capped at 10 minutes; the next scheduled run continues. The defensive cursor-stall guard now logs an error rather than silently aborting, and `DB.init()` fails fast if any required wrapper method is missing (a misconfigured ueberdb2 pin surfaces at boot instead of an hour later). Fixes #7830 (#7831).
1751

1852
### Security hardening
1953

0 commit comments

Comments
 (0)