* docs: refresh docs for 3.2.0 — correct stale content, document recent features
The hand-maintained VitePress docs under doc/ had drifted behind a lot of
recent work. They are authored prose (not generated from the OpenAPI spec),
so they need manual upkeep. This pass corrects content that was actively
wrong and documents features shipped since they were last touched.
Corrections (was wrong / misleading):
- cli.md: every command used `node bin/foo.js`, but the scripts are
TypeScript run via pnpm — copy-paste failed. Rewrote to
`pnpm run --filter bin <script>`, documented ~13 previously-undocumented
operator tools, and split running-vs-stopped requirements. Registered the
missing `compactStalePads` script in bin/package.json so the documented
invocation actually works.
- stats.md: described a pre-Prometheus world. Rewrote for the gated
`/stats` (JSON) and `/stats/prometheus` endpoints, the live metric set,
the opt-in `scalingDiveMetrics` instruments (#7756), and `measured-core`.
- admin/updates.md: removed three false "SMTP not yet wired" claims (it is,
via nodemailer + the `mail.*` block), documented the `node-engine-mismatch`
preflight check and the rollback/preflight failure emails, and stripped
obsolete "PR 1 / PR 2" staging language now that all tiers ship.
- api/http_api.md: added the undocumented `anonymizeAuthor` (GDPR Art. 17)
call, fixed copyPad/movePad version annotations (1.2.8 → 1.2.9), corrected
getPadID's param name (readOnlyID → roID), and dropped a reference to a
non-existent `getEtherpad` API call.
- skins.md: colibris is the current default, not an "experimental" skin for
a future 2.0.
- localization.md: bare `window._('key')` is unbound and returns undefined;
recommend `window.html10n.get(...)` / data-l10n-id instead.
- README.md: bumped the v2.2.5 upgrade example to v3.2.0; fixed a
docker.adoc link to docker.md.
- docker.md: added MAIL_*, ENABLE_METRICS, GDPR_AUTHOR_ERASURE_ENABLED,
PRIVACY_BANNER_*, PUBLIC_URL, AUTHENTICATION_METHOD, ENABLE_DARK_MODE,
ENABLE_PAD_WIDE_SETTINGS; fixed the SOCKETIO_MAX_HTTP_BUFFER_SIZE default
(50000 → 1000000).
New documentation:
- configuration.md (new): how settings + `${VAR:default}` substitution work,
trustProxy, and — the previously-undocumented feature — running under a
subpath/ingress via x-proxy-path / X-Forwarded-Prefix / X-Ingress-Path,
with the sanitizer rules and Traefik/NGINX examples. Wired into the
VitePress sidebar and the index hero.
- hooks_server-side.md: ccRegisterBlockElements (the server-side companion
plugin authors miss), exportConvert, exportHTMLSend, createServer,
restartServer, and clientReady (marked deprecated).
- hooks_client-side.md: aceDrop, acePaste, handleClientTimesliderMessage_<name>.
VitePress build passes. The legacy .adoc set was intentionally left in place
— it still feeds the per-version doc archives published to ether.github.com
at release time (bin/release.ts), so it is not dead and is out of scope here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: address Qodo review — drop .js invocations from configuration.md and CLI help
- configuration.md: the settings-override example referenced a nonexistent
`node src/node/server.js`. Use the supported launcher instead
(`bin/run.sh -s <file>`), and note the runtime is server.ts via tsx.
- compactStalePads.ts / compactPad.ts / compactAllPads.ts: their header
comments and runtime usage output still printed `node bin/*.js`, which
points at files that don't exist. Switched to the documented
`pnpm run --filter bin <script>` form so the --help text matches the docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two related operator-facing docs gaps, both surfaced by #7819:
1. settings.json on disk is a *template*; env-var substitution happens
at load time in memory only. Operators repeatedly mistake the
templated file for a stale config because the docs never spell out
that the on-disk file is intentionally unchanged by env vars.
2. The default docker-compose.yml puts settings.json in the container's
writable layer with no host mount, which means admin /settings edits
are silently lost on `docker compose down && up`, `pull`, or
watchtower — but preserved across plain `restart`. Operators don't
reliably know which compose verbs recreate the container.
Adds two prose sections to doc/docker.md (explaining both gotchas, with
a recreate-vs-restart table) and a commented-out `./settings.json:…`
bind mount in both docker-compose.yml and the README compose example.
Bind mount is opt-in so existing setups behave identically.
No runtime change.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Native DOCX export, PDF export, and DOCX import shipped in #7568
via pure-JS in-process converters -- LibreOffice/soffice is no
longer required for those formats. Stale comments in
settings.json.template and settings.json.docker still implied
otherwise ("will only allow plain text and HTML import/exports"),
and the docker docs told users to configure soffice for DOCX as
well. Update them to match what's actually in core:
- soffice present: handles all office formats (existing behavior)
- soffice null: docx export, pdf export, docx import work
natively; odt/doc/rtf export and pdf import still need soffice
Touches:
- settings.json.template (soffice + docxExport comments)
- settings.json.docker (same)
- doc/docker.md ("Office-format import/export" section)
- doc/docker.adoc (same section + the SOFFICE table row,
matching what doc/docker.md already says since #7568)
No code changes, no behavior change -- documentation only.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(export): native DOCX export via html-to-docx (opt-in)
Addresses #7538. The current DOCX export path shells out to LibreOffice,
which means every deployment that wants a Word download either installs
soffice (~500 MB) or loses that export. This PR adds a pure-JS
alternative: render the HTML via the existing exporthtml pipeline, then
feed it to the `html-to-docx` library in-process to produce a valid
.docx buffer — no soffice required, no subprocess spawn, no temp file
dance for the DOCX case.
Behavior:
- `settings.nativeDocxExport` (default `false`) gates the new path so
existing deployments see zero behavior change.
- When enabled, `type === 'docx'` requests skip the LibreOffice branch,
run `html-to-docx(html)`, and return the buffer with the
`application/vnd.openxmlformats-officedocument.wordprocessingml.document`
content-type.
- If the native converter throws, the handler falls through to the
existing LibreOffice path — so flipping the flag on is safe even on a
mixed-installation where soffice is still present as a backstop.
- Other export formats (pdf, odt, rtf, txt, html, etherpad) are
unchanged.
Files:
- `src/package.json`: `html-to-docx` dep (pure JS, no binary reqs)
- `src/node/handler/ExportHandler.ts`: new DOCX branch gated on the
setting, with fall-through on error
- `src/node/utils/Settings.ts`, `settings.json.template`,
`settings.json.docker`, `doc/docker.md`: wire up the new setting +
env var (`NATIVE_DOCX_EXPORT`)
- `src/tests/backend/specs/export.ts`: two new tests — asserts the
exported buffer is a valid ZIP (PK\x03\x04 signature) and the
response carries the correct content-type — both with
`settings.soffice = 'false'` to prove the path doesn't need soffice
at all.
Out of scope for this PR:
- Native PDF export (would need a PDF rendering step — separate
undertaking, and the issue acknowledges the `pdfkit`/puppeteer size
trade-off).
Closes#7538
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(7538): skip native DOCX test when html-to-docx isn't installed
The upgrade-from-latest-release CI job installs deps from the previous
release's package.json (before this PR adds html-to-docx) and then
git-checkouts this branch's code without re-running pnpm install.
Under that one workflow the new test can't find the module and fails
on the LibreOffice fallback, masking that the native path actually
works in every normal install.
Guard the describe block with require.resolve('html-to-docx'); Mocha's
this.skip() on before cascades to the sibling its. Regular backend
tests (pnpm install against this branch's lockfile) still exercise it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(7538): spec for soffice-free DOCX/PDF export and DOCX import
Captures the agreed scope expansion of PR #7568: replace the flag-gated
native DOCX path with a soffice-first selection cascade, add native PDF
export via pdfkit + a small htmlparser2-driven walker, and add native
DOCX import via mammoth. Also defines a shared HTML sanitizer
(stripRemoteImages) used by both export converters to close the
SSRF surface that Qodo flagged on the html-to-docx path.
The spec drops the nativeDocxExport setting and its env var; with
soffice configured, behavior is unchanged, and with soffice null,
docx/pdf export and docx import all work in-process. odt/doc/rtf
(and pdf import) keep needing soffice and are documented as such.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(7538): implementation plan for native DOCX/PDF and DOCX import
Bite-sized TDD task breakdown of the soffice-free export/import work:
rebase, deps, sanitizer, PDF walker, mammoth wrapper, ExportHandler
cascade, route guard, ImportHandler branch, UI fix, flag rollback,
verification + Qodo reply.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(7538): add pdfkit, htmlparser2, mammoth deps
Pure-JS, no native binaries:
- pdfkit ^0.18.0 (PDF rendering)
- htmlparser2 ^12 (SAX parser used by walker + sanitizer)
- mammoth ^1.12 (DOCX -> HTML for native import)
- @types/pdfkit ^0.17 (dev)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(7538): add stripRemoteImages HTML sanitizer
Drops <img src=> elements pointing at non-data, non-relative URLs to
prevent the DOCX/PDF converters from making outbound requests via
plugin-modified HTML. Closes Qodo finding #4 against the
html-to-docx path; will be wired into both export branches in
the cascade refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(7538): native PDF export via pdfkit + htmlparser2 walker
Renders pad HTML to a PDF Buffer in-process: headings, paragraphs,
lists, links, inline emphasis, data:-URI images. Remote images are
explicitly skipped at the walker (defense-in-depth on top of the
shared stripRemoteImages sanitizer).
PDFs are emitted with compress:false so accessibility/SEO indexers
that don't FlateDecode can still extract text. Pads are small enough
that the size cost is negligible.
Walker is 167 LOC, well under the spec's 500-LOC bail-out
threshold for switching to pdfmake+html-to-pdfmake+jsdom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(7538): native DOCX import via mammoth
Wraps mammoth.convertToHtml so a soffice-less Etherpad can ingest
.docx files. Images are coerced to data: URIs at the converter
boundary so the import pipeline never sees a remote src=.
Includes a tiny generated DOCX fixture (heading, paragraph, list)
under tests/backend/specs/fixtures/ for both this wrapper test and
the upcoming end-to-end import test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(7538): soffice-first cascade in ExportHandler
Replaces the flag-gated DOCX branch with a deterministic dispatch:
soffice if configured, native DOCX/PDF otherwise, 5xx on native
error. Both native paths run plugin-modified HTML through
stripRemoteImages first.
Test changes:
- existing native DOCX block now sets soffice=null (was 'false', a
truthy non-null string that sidestepped the route guard); fixes
Qodo finding #3.
- new native PDF integration tests assert %PDF- header and
application/pdf content-type with soffice=null.
- new negative test: with soffice=null, /export/odt still returns
the 'not enabled' message.
- the legacy 500-on-export-error test now uses /bin/false so it
exercises the soffice error path explicitly (the cascade dropped
the ad-hoc 'false' string; .doc has no native path so this still
works as a soffice error probe).
Integration tests for native DOCX/PDF currently fail because the
/export route guard still treats both formats as soffice-only;
the next commit fixes that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): allow docx/pdf through export guard without soffice
Tightens the no-soffice block to ['odt','doc'] only — formats with
no native path. docx and pdf are handed to ExportHandler, which
dispatches to the in-process converters. Closes Qodo finding #2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(7538): native DOCX import path in ImportHandler
When soffice is null and the upload is .docx, run mammoth and feed
the resulting HTML through setPadHTML. Other office formats
(pdf/odt/doc/rtf) are explicitly rejected with uploadFailed instead
of silently falling through to the ASCII-only path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): always show DOCX/PDF export links
Native paths (#7538) make DOCX and PDF available regardless of
soffice presence, so unconditionally render those links. ODT still
gates on exportAvailable. Closes Qodo finding #2 on the UI side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(7538): drop nativeDocxExport flag
Selection is now purely soffice-presence-driven (cascade in
ExportHandler). The opt-in setting and its NATIVE_DOCX_EXPORT env
var are no longer needed -- soffice configured means soffice path;
soffice null means native path (DOCX, PDF, and DOCX import).
Reverts the additive surface introduced earlier in this PR.
Also updates the SOFFICE doc row to reflect that null no longer
means 'plain text and HTML only' -- docx/pdf export and docx
import now work natively without soffice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(7538): tighten link annotation assertion
CodeQL flagged the loose 'raw.includes("etherpad.org")' as
'incomplete URL substring sanitization' (a false positive in test
context, but worth fixing). Match the full /URI (host) form
instead -- it's both more accurate (we're verifying the PDF link
annotation structure) and CodeQL-clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): cleaner DOCX/PDF output + round-trip test coverage
DOCX:
- New extractBody helper drops <head>/<style> and the leading
newline inside <body> so html-to-docx doesn't render CSS or
prefix paragraphs with empty space.
- New wrapLooseLines pre-processor wraps loose pad lines in <p>
before the converter sees them. html-to-docx renders <br>
outside <p> as a new <w:p> (full empty line in Word); inside
<p> it correctly emits <w:br/> (soft break). Etherpad's HTML
uses bare <br> for every line, so this was making single
Enters look like double Enters in the Word output.
PDF:
- Walker SKIP_TAGS rejects head/style/script/title/meta/link
content -- prior version dumped CSS into the rendered PDF.
- New breakLine() helper combines flushLine() with moveDown(1).
pdfkit's text('', false) closes the continued run but does
NOT advance the cursor, so consecutive runs were stacking at
the same y-coordinate. <br>, end-of-block, and list items
now use breakLine().
- ontext collapses runs of whitespace and drops pure-whitespace
text nodes so pretty-printed source HTML doesn't render its
formatting newlines.
Round-trip:
- New backend test: pad text -> DOCX export -> DOCX import ->
new pad. Asserts content survives the trip.
- New PDF sanity test: extracts visible text from the PDF stream
and asserts the source pad text appears verbatim.
- 6 new unit tests for extractBody and wrapLooseLines plus 1 for
PDF walker SKIP_TAGS coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): CodeQL ReDoS + import.ts type error
- BR_PARA_RE was /(?:\s*<br>\s*){2,}/ -- two adjacent \s* runs can
match the same chars, so on '<br>\t<br>\t<br>...' the regex
backtracks exponentially. Re-anchored to match a fixed first <br>
followed by one or more additional <br>s, so each whitespace run
has exactly one home.
- import.ts: fetchBuffer was typed Promise<Buffer> but call sites
chained .expect(200) on it, which only works on supertest's Test
object. Return the Test (typed any) so the chain is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): plugin-aware HTML cleanup, PDF text-align, monospace
ep_headings2/ep_align emit one heading-styled blank-line block after
every styled line in the pad ('<h1 style=text-align:right></h1>'),
which both html-to-docx and our pdfkit walker render as a full empty
paragraph. Plus the pdfkit walker had no support for text-align or
monospace, so right/center alignment and 'code' lines rendered the
same as plain body text.
- New dropEmptyBlocks helper strips empty h1-h6/p/code/pre/div/
blockquote wrappers in preprocessing. Iterates so nested empties
collapse too. Applied before both DOCX and PDF conversion.
- PDF walker now reads style='text-align:left|center|right|justify'
on block elements (h1-6, p, div) and passes it as pdfkit's align
option. align is applied once per continued run, then reset on
flushLine so the next block can pick up its own value.
- PDF walker handles <code>, <tt>, <kbd>, <samp> as inline monospace
(Courier) and <pre> as block monospace (Courier + breakLine on
open/close).
11 new unit tests:
- 4 for dropEmptyBlocks (heading wrappers, code, nesting,
pass-through)
- 1 for PDF text-align (compares the BT matrix x for left vs right)
- 2 for Courier in <code> and <pre>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): separate adjacent heading-style blocks on import
Round-trip bug: ep_headings2 emits <h1>/<h2>/<code> in pad HTML.
Mammoth round-trips them as adjacent <h1>A</h1><h2>B</h2><p>C</p>
on import. Etherpad's server-side content collector has a default
_blockElems set of just {div, p, pre, li}, and ep_headings2 only
registers the CLIENT-side aceRegisterBlockElements -- not the
server-side ccRegisterBlockElements. So h1/h2/code end up being
treated as inline by the importer, and adjacent blocks merge into
a single pad line.
Fix: insert <br> after </h1>...</h6>/</code> when followed by
another block. Server-side workaround keeps this PR self-contained
regardless of plugin version. The right long-term fix is to extend
ep_plugin_helpers' lineAttribute factory to register both hooks
(filed as a follow-up).
Tests:
- 5 unit tests for separateAdjacentHeadingBlocks
- New end-to-end round-trip test asserts H1+H2+P land on three
separate pad lines after the import path.
Plus the prior PDF text-align/Courier/code commit also included
here:
- code/tt/kbd/samp inherit text-align from style attribute
- pre inherits text-align too
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): blank-line round-trip + DOCX code monospace + a==c tests
DOCX round-trip dropping blank pad lines:
- wrapLooseLines now emits an explicit <p></p> marker for each blank
line in a <br> run, instead of collapsing all gaps into a single
paragraph break. (N consecutive <br>s -> 1 paragraph boundary +
N-2 empty <p></p> markers, mapping to N-1 blank pad lines.)
- mammoth's docxBufferToHtml now passes ignoreEmptyParagraphs:false
so the empty <w:p> entries survive the import side. mammoth's
default of true was silently dropping them.
- dropEmptyBlocks no longer strips <p></p> -- that's the meaningful
marker for the round-trip. Empty <h1>/<code>/<pre>/<div>/
<blockquote> are still stripped (plugin noise).
DOCX <code> rendering as monospace:
- New applyMonospaceToCode wraps code/tt/kbd/samp/pre content in a
<span style="font-family:'Courier New', monospace">. html-to-docx
honors that and emits <w:rFonts w:ascii="Courier New".../>, which
Word renders as Courier. The bare <code> tag is otherwise just
a no-op for html-to-docx.
- Applied only on the DOCX export path (PDF walker already handles
monospace via Courier font selection).
Round-trip tests:
- New a==c suite: txt, etherpad, html, docx -- export from src,
import to dst, re-export and compare against the meaningful
invariant (line text for binary formats; trimmed body for HTML).
- HTML test tolerates one trailing <br> per round-trip because
setPadHTML appends a final <p> on import; this is pre-existing
core behavior, not our bug.
- DOCX test normalizes trailing newline run (same reason).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): preserve <w:jc> alignment through mammoth round-trip
mammoth doesn't expose Word's paragraph alignment (`<w:jc>`) when it
converts a docx to HTML -- there's no equivalent in its style-mapping
machinery. To keep alignment through DOCX round-trips we walk the
docx's document.xml directly, pull the `w:val` from each `<w:p>`'s
`<w:jc>`, and inject `style="text-align:..."` onto the matching
block element in mammoth's output by document order.
Word's w:jc accepts more values than CSS text-align; we map left/
start, center, right/end, both/justify/distribute and skip the rest
(start/end take left/right because we don't track ltr/rtl from the
docx for now).
Combines with the upstream ep_align PR (ether/ep_align#183) for the
full round-trip: this PR makes the mammoth output carry the
alignment style; ep_align#183 makes the importer pick it up.
Closes the alignment side of #7538.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): preserve <a href> inside <code>/<pre> in DOCX export
html-to-docx silently drops <a href> children of <code>/<pre> tags
(and of styled <span>s, but the <code> wrapper is the active offender
here). The pad-export HTML produced by ep_headings2 + ep_align uses
<code style='text-align:right'>...<a href>...</a>...</code> for each
'Code'-style line, which lost its links on every DOCX export.
Workaround: applyMonospaceToCode now drops the code/pre/tt/kbd/samp
wrapper entirely. The non-anchor content gets wrapped in monospace
spans; anchors are emitted unstyled so they keep their hyperlink. For
block-level usage (<pre>, or <code> with an inline style attr) we
emit a wrapping <p> and forward the text-align style. Run BEFORE
wrapLooseLines so the <p> doesn't get double-wrapped.
Tests added:
- inline <code> -> just a styled span (no <code> wrapper)
- <code style='text-align:right'> -> <p style> wrap
- <pre> -> always block-wrapped
- <tt>/<kbd>/<samp> -> inline span only
- regression: <a href> inside <code> survives html-to-docx round-trip
with both the URL in word/_rels/document.xml.rels AND a <w:hyperlink>
in the document body
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7538): HTML import line-break drift between blocks
Etherpad's HTML export wraps each pad line in <p>...</p> (or <h1>,
<code>, etc.) and then appends a <br> between lines. The closing
block tag already ends the line for contentcollector, so the trailing
<br> is redundant -- and on import the server collector counts BOTH
as line breaks, doubling every blank line between paragraphs and
inserting an extra blank between adjacent headings.
Two fixes, both gated on the runtime block-element registry so they
don't double-trigger when the underlying plugin already handles
adjacency:
1. HTML import path now runs the new collapseRedundantBrAfterBlocks
helper before setPadHTML. Drops a single <br> immediately
following </p>/</h1-6>/</code>/</pre>/</div>/</blockquote>/</ul>/
</ol>/</li>/</table>/</tr>/</td>/</th>. Multiple consecutive
<br>s after a block keep all but the first (the rest still
represent intentional blank lines).
2. The DOCX-import separateAdjacentHeadingBlocks workaround now
checks whether 'h1' is in the runtime ccRegisterBlockElements
set before inserting <br>s. When ep_headings2 has the new server
hook (per ep_plugin_helpers#14 + the upcoming ep_headings2 PR),
the workaround correctly stays out of the way -- otherwise it
adds an extra blank line per heading transition.
Also fixed a subtle ts-check failure on the import.ts test changes
and a leftover implicit-any in ImportDocxNative's alignment
preserver.
Tests added:
- collapseRedundantBrAfterBlocks: 5 unit tests (each block tag,
whitespace tolerance, multiple <br> keeping intentional blanks)
- HTML import: 'does not introduce a blank line between H1 and H2',
'preserves blank-line count between H1 and H2 (realistic shape)'
reproduces the 5-blanks-where-2-expected bug from the user's
round-trip pad.
1054 backend tests pass locally (the 6 failures are the pre-existing
favicon/webaccess send@1.x dotfile-path issue from running under
.claude/, doesn't reach CI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(7538): skip plugin-dependent HTML import tests on no-plugin CI
The two new HTML-import-adjacency tests assume ep_headings2 (or
another plugin) has registered h1/h2 as server-side block elements
via ccRegisterBlockElements. Without that, contentcollector treats
<h1>/<h2> as inline and adjacent ones merge into a single pad line
-- making the assertions inapplicable.
CI's backend-tests job runs without plugins installed, so guard
the describe block with a runtime hooks.callAll() check and skip
when h1 isn't a registered block. Local dev with ep_headings2 (and
the local plugin patch wiring ccRegisterBlockElements) still
exercises both tests.
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(colors): clamp author backgrounds to WCAG 2.1 AA on render
Fixes#7377.
Authors can pick any color via the color picker, so a user who chooses
a dark red ends up with black text rendered on a background that fails
WCAG 2.1 AA (4.5:1) — unreadable, but there is no way for *viewers* to
remediate since they cannot change another author's color. Screenshot
in the issue shows exactly this.
This PR lands a viewer-side clamp. For each author background, if
neither black nor white text would satisfy the target contrast ratio,
the bg is iteratively blended toward white until black text does. The
author's stored color is untouched — turning off the new
padOptions.enforceReadableAuthorColors flag restores the raw colors
immediately.
New helpers in src/static/js/colorutils.ts:
- relativeLuminance(triple) — WCAG 2.1 relative-luminance formula
- contrastRatio(c1, c2) — in [1, 21]; >=4.5 = AA, >=7.0 = AAA
- ensureReadableBackground(hex, minContrast = 4.5)
— returns a hex that meets minContrast
against black text, preserving hue
Wire-up:
- src/static/js/ace2_inner.ts (setAuthorStyle): pass bgcolor through
ensureReadableBackground before picking text color. Gated on
padOptions.enforceReadableAuthorColors (default true). Guarded by
colorutils.isCssHex so the few non-hex values (CSS vars, etc.) skip
the clamp and pass through unchanged.
- Settings.ts / settings.json.template / settings.json.docker: new
padOptions.enforceReadableAuthorColors flag, default true, with a
matching PAD_OPTIONS_ENFORCE_READABLE_AUTHOR_COLORS env var in the
docker template.
- doc/docker.md: env-var row.
- src/tests/backend/specs/colorutils.ts: new unit coverage for the
three new helpers, including the exact #cc0000 failure case from
the issue screenshot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(7377): simplify — just pick higher-contrast text, drop bg clamp
First iteration added an iterative bg-lightening helper
(ensureReadableBackground) gated by a new padOptions flag. CI caught the
correct simpler framing: because WCAG contrast is symmetric in [1, 21],
at least one of black/white always clears AA (4.5:1) for any sRGB
colour. The real bug was that the pre-fix textColorFromBackgroundColor
used a plain-luminosity cutoff (< 0.5 → white), which produced
sub-AA combinations like white-on-red (#ff0000) at 4.0:1.
Reduce the PR to the minimal surface:
- colorutils.textColorFromBackgroundColor now picks whichever of
black/white has the higher WCAG contrast ratio against the bg.
- colorutils.relativeLuminance and colorutils.contrastRatio are kept
as reusable building blocks; ensureReadableBackground is dropped
(no caller needed it once text selection was fixed).
- ace2_inner.ts setAuthorStyle no longer needs the opt-in flag or the
isCssHex guard — the helper handles every input its caller already
passes.
- padOptions.enforceReadableAuthorColors setting reverted along with
settings.json.template, settings.json.docker, and doc/docker.md.
- Tests replaced: instead of asserting the bg gets lightened, assert
that the chosen text colour clears AA for every primary. Covers the
exact #ff0000 failure case from the issue screenshot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(7377): assert relative-contrast invariant, not absolute AA
Pure primaries like #ff0000 cannot clear WCAG AA (4.5:1) against either
#222 or #fff — the best either can do is ~4.0:1. No text-colour choice
alone fixes that; bg clamping would be a separate concern. The test
should therefore verify the *real* invariant: the chosen text colour
must produce the higher contrast of the two options, regardless of
whether that contrast clears any absolute threshold.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7377): compare against rendered #222/#fff, not pure black/white
First cut of textColorFromBackgroundColor computed contrast against
pure black (L=0) and pure white (L=1), then returned the concrete
#222/#fff the pad actually renders with. For some mid-saturation
backgrounds the two comparisons disagreed — e.g. #ff0000:
vs pure black = 5.25 → pick black → render #222 → actual 3.98
vs pure white = 4.00 → would-render #fff → actual 4.00
The helper picked the wrong option because it compared against the
wrong target. Compare against the actual rendered colours so the
returned text colour is genuinely the higher-contrast choice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(7377): pick unambiguous colibris test bgs
#ff0000 lives right at the boundary for the two text choices (4.00 vs
3.98), so the test for colibris-skin mapping was entangled with the
border-case selector pick. Use #ffeedd (clearly light → dark text
wins) and #111111 (clearly dark → light text wins) so the test
isolates the skin mapping from the tie-breaking logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7377): use rendered text colour + clamp bg to actually meet AA
Local repro of the issue exposed two real bugs in the previous fix:
1. textColorFromBackgroundColor compared bg against a hardcoded #222 —
but in the colibris skin --super-dark-color resolves to #485365.
For the issue's exact case (#9AB3FA author bg) the selector returned
var(--super-dark-color) thinking it was getting a 7.7:1 ratio, while
the browser actually rendered 3.78:1 — identical to what the issue
screenshot reported. This PR's previous behaviour on the issue's
inputs was unchanged from the pre-fix.
2. For mid-saturation pastels (#9AB3FA) and pure primaries (#ff0000)
neither rendered dark nor white text can clear AA. Text-colour
selection alone genuinely cannot fix this band; the ensureReadable
bg clamp dropped in ce0c5c283 was load-bearing.
Changes:
- colorutils.ts: per-skin SKIN_TEXT_COLORS table with darkRef/lightRef
matching what the browser actually paints (colibris #485365,
default #222). Re-introduces ensureReadableBackground, but skin-aware
and symmetric — blends bg toward white or black depending on which
text colour wins, so it works for both light and dark backgrounds.
- ace2_inner.ts: setAuthorStyle runs the bg through the clamp before
picking text colour. Gated on padOptions.enforceReadableAuthorColors
(default true).
- Settings.ts / settings.json.template / settings.json.docker /
doc/docker.md: padOption + PAD_OPTIONS_ENFORCE_READABLE_AUTHOR_COLORS
env var.
- tests: failing-then-green coverage for the issue's exact case
(#9AB3FA + colibris), the previously-impossible #ff0000, the
no-mutation case, non-hex pass-through, and a sweep over primaries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(7377): add e2e DOM-contrast spec + extra unit cases
The previous coverage was unit-only, which is what let the original wrong-
reference-colour bug ship — the algorithm tests were green but nothing
exercised what the browser actually paints. New coverage:
Playwright (src/tests/frontend-new/specs/wcag_author_color.spec.ts):
- Sets the user's colour to the issue's exact #9AB3FA, types text, reads
the rendered author span's computed bg + colour from the inner frame,
and asserts the WCAG ratio between the two is >= 4.5. Repeated for
#ff0000 (the other historically-failing case).
- Asserts #ffeedd (already AA-friendly) is rendered unchanged — guards
against the clamp mutating colours that don't need it.
Backend additions (src/tests/backend/specs/colorutils.ts):
- Symmetric-clamp test: dark mid-saturation bg where light text wins, the
clamp must darken (not lighten). Direction check via relativeLuminance.
- minContrast parameter: AAA (7.0) must produce more clamping than AA.
- Output shape: result must be a parseable hex string (round-trip safe).
- Short-hex (#abc) input is accepted and normalised.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new pad option `fadeInactiveAuthorColors` (default `true`) that controls whether each author's caret/background fades toward white as they go inactive. Configurable server-side (`settings.json` / `PAD_OPTIONS_FADE_INACTIVE_AUTHOR_COLORS`), per-pad in the Pad Settings panel, per-user in the My View panel, or via `?fadeInactiveAuthorColors=false`.
Disabling the fade is useful on busy pads where every faded author visually counts as a second on-screen color (a 30-author pad becomes a 60-color pad), or when inactivity tracking is undesirable for whatever reason.
Closes#7138.
* ci: publish Docker images to GHCR alongside Docker Hub
Adds ghcr.io/ether/etherpad as a second publish target on release tags,
reusing the existing docker/metadata-action step so the same SemVer tags
(e.g. 2.6.1, 2.6, 2, latest) are pushed to both registries.
Motivation: downstream consumers (Helm charts in particular) hit Docker
Hub anonymous pull rate limits. GHCR has no such limits and the
workflow already runs with GITHUB_TOKEN, so this is additive with no
new secrets required.
Docker Hub remains the primary/canonical source; GHCR is a mirror.
Note: this only affects future release tags. The 2.6.1 tag already on
Docker Hub will need to be mirrored separately (e.g. via skopeo) if
downstream needs it on GHCR before the next release.
* address qodo review: scope packages:write to publish job, document GHCR
Two fixes from the qodo code review on #7569:
1. Overprivileged PR token (security). The original change set
'packages: write' at workflow level, which meant pull_request runs
(whose Test step executes PR-controlled code) also inherited push
access to GHCR. Splits the workflow into two jobs:
- build-test: runs on pull_request and push with contents:read
only. Does the single-arch load+test as before.
- publish: needs build-test, runs only on push with
packages:write. Does the multi-arch build-and-push, Docker Hub
description update, and ether-charts bump.
Docker Hub login is also now gated by job-level 'if' (same effect
as the previous step-level 'if').
2. Docs miss GHCR option. Updates doc/docker.md and README.md to
document the GHCR mirror alongside Docker Hub with equivalent pull
examples, so downstream users discovering via docs can choose the
mirror to avoid Docker Hub rate limits.
* feat!: replace Abiword with LibreOffice and add DOCX export (#4805)
The Abiword converter is dropped. Abiword's DOCX export is weak and the
project is niche on modern platforms; LibreOffice (soffice) is the
common deployment path and now serves as the sole converter backend.
DOCX is added as an export format and becomes the new target for the
"Microsoft Word" UI button. The /export/doc URL still works for legacy
API consumers.
BREAKING CHANGE: The 'abiword' setting, the INSTALL_ABIWORD Dockerfile
build arg, the abiwordAvailable clientVar, and the
#importmessageabiword UI element (with locale key
pad.importExport.abiword.innerHTML) are removed. Deployments relying on
Abiword must configure 'soffice' instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add docxExport feature flag and abiword deprecation WARN
- Add `docxExport: true` setting to opt out of DOCX (use legacy DOC)
- Pass `docxExport` to client via clientVars
- Use `docxExport` flag in pad_impexp.ts for Word button format
- Emit a specific WARN when deprecated `abiword` config is detected
- Update settings.json.template and settings.json.docker with docxExport
- Add docxExport to ClientVarPayload type in SocketIOMessage.ts
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/9afc5291-73b2-4b66-b028-feed39e7056f
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* refactor: extract wordFormat variable and improve docxExport comment
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/9afc5291-73b2-4b66-b028-feed39e7056f
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* fix: restore import-limitation message when no converter is configured
The abiword removal dropped both the #importmessageabiword DOM element
and its locale key, but Copilot's refactor still expected the show()
call to surface a message when exportAvailable === 'no'. Result: users
with no soffice binary got silent failure instead of an explanation.
Add #importmessagenoconverter back with updated, LibreOffice-focused
copy (new locale key pad.importExport.noConverter.innerHTML) and flip
the hidden prop when the client knows no converter is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n: inline English fallback for noConverter import message
The original abiword message existed in ~70 locale files and was
removed from all of them by this PR. The replacement key was only
added to en.json, so non-English users had an empty div until
translators localize. Follow the project's usual pad.html pattern
(e.g. line 146's "Font type:") and include the English text inside
the div as the fallback content; html10n replaces it when a
translation is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "i18n: inline English fallback for noConverter import message"
This reverts commit f336f24d. Follow the project convention: add the
new locale key to en.json only and let translations catch up via the
translation system, rather than putting inline fallback in the template.
* i18n: leave non-English locale files untouched
The PR had removed pad.importExport.abiword.innerHTML from ~82 locale
files alongside its removal from en.json. The replacement message uses
a new key (pad.importExport.noConverter.innerHTML) in en.json only, so
churning every localisation file for a key that is no longer referenced
produces useless translation diffs. Restore every non-en locale file to
its pre-PR state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* SecretRotator: New class to coordinate key rotation
* express-session: Enable key rotation
* Added new entry in docker.adoc
* Move to own package.Removed fallback as Node 16 is now lowest node version.
* Updated package-lock.json
---------
Co-authored-by: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
These options are used as strings, so it doesn't make sense to default
them to a boolean value.
Note that this change has no effect due to a bug in how pad options
are processed; that bug will be fixed in a future commit.
It doesn't make sense to override the browser's language with `en-gb`
by default.
Note that this change has no effect due to a bug in how pad options
are processed; that bug will be fixed in a future commit.
This MR introduces a docker build variable `INSTALL_ABIWORD`. When set
to any value other than `0`, ABIWORD is installed in the resulting
docker container, enabling the possibility to configure ABIWORD in
settings.json.docker or via ENV VAR `ABIWORD` for exporting to
DOC/PDF/ODT.
Documentation is included inline and in the docker markdown file.
The settings commitRateLimiting.duration and commitRateLimiting.points
were not available in the settings.json.docker file, and therefore it
was not possible to override their values via environment variables.
Now, they can be overridden by setting the following env vars:
* commitRateLimiting.duration: COMMIT_RATE_LIMIT_DURATION
* commitRateLimiting.points: COMMIT_RATE_LIMIT_POINTS
This will be a breaking change for some people.
We removed all internal password control logic. If this affects you, you have two options:
1. Use a plugin for authentication and use session based pad access (recommended).
1. Use a plugin for password setting.
The reasoning for removing this feature is to reduce the overall security footprint of Etherpad. It is unnecessary and cumbersome to keep this feature and with the thousands of available authentication methods available in the world our focus should be on supporting those and allowing more granual access based on their implementations (instead of half assed baking our own).
In this way, we also gain an explicit place for the default setting (still not
filled in).
No functional changes.
This is in preparation of a future commit by Paul Tiedke.
The mechanism used for determining if the application is being served over SSL
is wrapped by the "express-session" library for "express_sid", and manual for
the "language" cookie, but it's very similar in both cases.
The "secure" flag is set if one of these is true:
1. we are directly serving Etherpad over SSL using the native nodejs
functionality, via the "ssl" options in settings.json
2. Etherpad is being served in plaintext by nodejs, but we are using a reverse
proxy for terminating the SSL for us;
In this case, the user has to be instructed to properly set trustProxy: true
in settings.json, and the information wheter the application is over SSL or
not will be extracted from the X-Forwarded-Proto HTTP header.
Please note that this will not be compatible with applications being served over
http and https at the same time.
The change on webaccess.js amends 009b61b338, which did not work when the SSL
termination was performed by a reverse proxy.
Reference for automatic "express_sid" configuration:
https://github.com/expressjs/session/blob/v1.17.0/README.md#cookiesecureCloses#3561.
With this change, the Dockerfile builds the Docker image from the code
checked out in the local filesystem, instead of downloading a revision
from git.
Implements #3657