`Pad.normalizePadSettings()` was defaulting `lang` to the literal string
'en' when `rawPadSettings.lang` was not a string. That value flowed into
`clientVars.padOptions.lang` and then into `getParams()` in pad.ts,
which calls `html10n.localize([serverValue, 'en'])` as a callback for
the `lang` setting. The result: every pad forced English on load,
overriding the browser's Accept-Language and the existing auto-detect
chain in l10n.ts (cookie -> navigator.language -> 'en').
The regression was introduced in #7545 ("Add creator-owned pad settings
defaults", commit e0ccdb4d9). 2.6.1 did not have this default, so
auto-detect worked there. 2.7.0 broke it.
Fix: default `lang` to null. The client's existing flow already handles
null correctly — getParams() at pad.ts:172 has
`if (serverValue == null) continue;`, so the forced-localize callback
simply does not fire, and l10n.ts's browser-language auto-detect runs.
Pad-settings dropdown consumer at pad.ts:489 already uses
`padOptions.lang || 'en'` so null renders fine there too.
`PadSettings.lang` is now typed `string | null` to match.
Added three backend regression tests under `normalizePadSettings lang`:
* defaults to null when lang is absent (so client auto-detects)
* preserves an explicit string lang (creator override still works)
* drops non-string lang values to null rather than coercing to 'en'
Manual verification: with Firefox set to German, loading a fresh pad
now renders the UI in German. Index and timeslider continued to work
as before. Setting `?lang=de` or a language cookie continues to
override browser detection, as intended.
Fixes#7586
PadMessageHandler built the `pluginsSanitized` payload for clientVars by
aliasing `plugins.plugins` and then mutating each entry's `package` field
in place:
let pluginsSanitized: any = plugins.plugins;
Object.keys(plugins.plugins).forEach(function(element) {
const p: any = plugins.plugins[element].package;
pluginsSanitized[element].package = {name: p.name, version: p.version};
});
Because `pluginsSanitized` is a reference to `plugins.plugins`, the
assignment clobbered the server-side plugin registry. After the first
pad connection, every plugin's `package` object held only `{name,
version}` — `realPath`, `path`, and `location` were gone.
Minify.ts resolves `/static/plugins/ep_*/...` URLs via
`plugin.package.realPath`. Once the field disappeared, every subsequent
static asset request for a bundled plugin 500'd with:
TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of
type string. Received undefined
at Object.join (node:path:1354:7)
at _minify (src/node/utils/Minify.ts:181:23)
Symptoms on Chromium: plugin CSS/JS assets fail to load (e.g.
/static/plugins/ep_font_size/static/css/size.css returns 500), so
plugins partially render or don't work at all. Firefox swallows the
resulting console errors quietly.
Fix: extract the sanitization into a pure helper `sanitizePluginsForWire`
that returns a fresh object graph and never touches the input. The
helper is covered by a new backend spec that:
* verifies the sanitized output has only {name, version} in `package`
* asserts the input registry's realPath/path/location survive the call
* runs the call repeatedly and confirms non-destructiveness
* mutates the returned copy and asserts the input is independent
Verified live with the dev server: before the fix, `/static/plugins/
ep_font_size/static/css/size.css` 500'd after visiting any pad; after
the fix it returns 200 both before and after pad connections.
* fix(editor): preserve U+00A0 non-breaking space (#3037)
Non-breaking spaces were silently normalized to regular spaces at every
ingestion point, so typed/pasted/imported nbsps never reached the
changeset and users could not glue words against line-wrap in French or
other languages that require nbsp typography.
Removed the four strip sites that replaced U+00A0 with U+0020:
- src/node/db/Pad.ts cleanText
- src/static/js/contentcollector.ts textify
- src/static/js/ace2_inner.ts textify
- src/static/js/ace2_inner.ts importText raw-text guard
Updated both processSpaces functions (domline and ExportHtml) to tokenize
U+00A0 as a separate unit, emit it verbatim as , and treat it as
content (not whitespace) for the run-collapse bookkeeping so adjacent
regular-space runs aren't miscounted.
Added backend round-trip tests for spliceText and setText, and extended
the cleanText case table. Updated the existing contentcollector and
importexport specs whose expectations encoded the previous buggy
behavior; they now assert genuine nbsp preservation.
Verified manually in Firefox: clipboard U+00A0 → paste → pad → getText
returns c2 a0; getHTML emits `100 km`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(contentcollector): collapse display-artifact nbsp runs on DOM read-back
processSpaces is a lossy one-way display transform: leading/trailing
spaces and all-but-the-last of a run get rendered as so HTML
doesn't collapse them. When incorporateUserChanges reads text back from
the DOM, those display-artifact nbsps were being stored in the changeset
model instead of being normalized back to plain spaces.
This broke handleReturnIndentation, whose /^ *(?:)/ regex only matches
ASCII spaces: auto-indent after `foo:\n` produced 4 spaces instead of
the expected prev-indent (2) + THE_TAB (4) = 6, because the previous
line's model had nbsps where it used to have spaces.
Fix: in contentcollector.textify, collapse any [ ]+ run back to
plain spaces UNLESS the run is pure U+00A0 AND strictly interior to
word chars. That preserves user-intended typographic nbsps like
"100 km" while undoing the one-way display transform.
Updated 7 contentcollector tests and 7 importexport tests whose
assertions needed to reflect the new rule (boundary/mixed runs collapse;
pure-interior nbsp runs preserve).
Fixes the Playwright regression in indentation.spec.ts:117 that the
previous commit introduced.
* fix(contentcollector): canonicalize nbsp runs at line assembly, not per text node
Addresses Qodo code review feedback on PR #7585.
## Bug fix — nbsp lost at DOM text-node boundary
The previous approach ran the "collapse display-artifact nbsp" rule inside
textify(), which is called per individual DOM TEXT_NODE. A user-intended
nbsp sitting at a text-node boundary (e.g., <span>100</span><span> km
</span>) was incorrectly seen as non-interior (before === '' for the second
text node) and normalized back to a regular space.
Fix: move the canonicalization out of textify() and run it on each
fully assembled line string inside cc.finish(). The rule remains:
[ ]+ run -> plain spaces
UNLESS pure U+00A0 AND strictly interior to non-ws chars
It is length-preserving, so attribute offsets and line lengths are
unaffected.
Added a regression test (contentcollector.spec.ts) for the cross-span
case.
## Docs concern
Reverted the type-only addition of spliceText to PadType. spliceText
is an existing Pad runtime method; the backend test now uses a cast
(`(pad as any).spliceText`) so the PR does not expand the declared
public type surface, avoiding a separate documentation requirement.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(packaging): add Debian (.deb) build via nfpm with systemd unit
First-class Debian packaging for Etherpad, producing signed-ready
etherpad-lite_<version>_<arch>.deb artefacts for amd64 and arm64 from a
single nfpm manifest. Installing the package gives users:
- /opt/etherpad-lite with a prebuilt, self-contained node_modules/ — no
pnpm required at runtime, just `nodejs (>= 20)`.
- etherpad system user/group, created via `adduser` in preinst.
- /etc/etherpad-lite/settings.json seeded from the template on first
install, preserved across upgrades, removed on `purge`.
- /var/lib/etherpad-lite owned by etherpad:etherpad, with the default
dirty-DB retargeted there so ProtectSystem=strict works.
- /lib/systemd/system/etherpad-lite.service — hardened unit
(NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp,
RestrictAddressFamilies) with Restart=on-failure.
- /usr/bin/etherpad-lite CLI wrapper running `node --import tsx/esm`.
CI (.github/workflows/deb-package.yml) triggers on v* tags, builds both
arches via native runners (ubuntu-latest + ubuntu-24.04-arm), smoke-tests
the amd64 package end-to-end (install → systemctl start → curl /health
→ purge → confirm user removed), and attaches the artefacts to the
GitHub Release.
Publishing to an APT repo (Cloudsmith, Launchpad PPA, self-hosted
reprepro) is intentionally out of scope — needs a governance decision on
who holds the signing key. Recipes are documented in packaging/README.md.
Refs #7529
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(deb): fail smoke test on /health timeout, tighten default-file perms, 2-space indent
Addresses Qodo review feedback on #7559:
1. Smoke test false-positive: the `for` loop polling /health never failed
the job if the endpoint stayed down — `curl && break || sleep 2`
keeps returning 0 from the trailing `sleep`, so `set -e` never
trips. CI could attach a broken .deb to a release. Fix: track
success explicitly and exit 1 (plus dump journald logs for
diagnostics) when the service never becomes healthy.
2. /etc/default/etherpad-lite was world-readable (0644). systemd loads
it via `EnvironmentFile=…`, and Etherpad supports
${ENV_VAR}-substitution for secrets (DB_PASSWORD etc.), so any
local user could read anything admins drop there. Fix: install the
conffile as root:etherpad 0640 — only root and the service user can
read it.
3. Indentation: reflow maintainer scripts from 4-space to 2-space to
match the repo style rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: design spec for issue #7570 (ueberdb2 driver bundling)
Spec for the upstream ueberDB fix (move 10 drivers back from optional
peer deps to dependencies) plus downstream etherpad-lite safety net
(explicit driver list + build-test-db-drivers CI job covering all 10
via presence check and MySQL+Postgres smoke tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: implementation plan for issue #7570 ueberdb2 driver bundling
Covers upstream ueberDB PR (move drivers from optional peer deps back
to dependencies, publish 5.0.46) and downstream etherpad-lite PR
(bump ueberdb2, defensive driver list, build-test-db-drivers CI job
with presence + MySQL + Postgres stages gating publish).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#7570): bundle DB drivers, add regression CI
- Bump ueberdb2 to ^5.0.47 (upstream ueberDB PR #939 re-bundles drivers
as real dependencies instead of optional peer deps, fixing the class
of Docker-prod "Cannot find module" failures).
- Declare all 10 ueberdb2 DB drivers as direct src dependencies as a
defensive safety net against a future upstream drift.
- Add build-test-db-drivers CI job that blocks the publish job:
* all-10-drivers presence check in the built prod image
* end-to-end MySQL smoke (reproduces the #7570 repro)
* end-to-end Postgres smoke
Any stage failure blocks Docker Hub / GHCR publish.
Supersedes #7571.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ci): run driver presence test from src/ so node_modules resolves
The presence test ran node from the default cwd (/opt/etherpad-lite),
but the drivers are installed under /opt/etherpad-lite/src/node_modules
by the monorepo workspace. Adding `-w /opt/etherpad-lite/src` makes
Node resolve modules from src/node_modules where pnpm places them.
Matches how the production container itself runs: `pnpm run prod` is
invoked from src/ (cross-env + node --require tsx/cjs node/server.ts).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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.
Adds an explicit `permissions: contents: read` block to update-plugins.yml.
Cross-repo work (cloning ether/ep_* repos, pushing updates, merging
Dependabot PRs) is authenticated via secrets.PLUGINS_PAT, so the default
GITHUB_TOKEN only needs read access for actions/checkout.
Addresses CodeQL code-scanning alert #115 ("Workflow does not contain
permissions"). Matches the pattern already used by the other workflows
under .github/workflows/.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a pnpm.overrides block to force-upgrade vulnerable transitive
dependencies to their patched versions. All 33 open Dependabot alerts on
ether/etherpad are against pnpm-lock.yaml; none of these packages are
direct dependencies of the workspace.
Bumps (vulnerable range → pinned):
- basic-ftp ≤5.2.2 → ≥5.3.0 (GHSA-5rq4-664w-9x2c,
GHSA-6v7q-wjvx-w8wg,
GHSA-rp42-5vxx-qpwr)
- brace-expansion <2.0.3 → ≥2.0.3 (GHSA-f886-m6hf-6m8v)
- diff <8.0.3 → ≥8.0.3 (GHSA-73rr-hh4g-fpgx)
- flatted <3.4.2 → ≥3.4.2 (GHSA-25h7-pfq9-p65f,
GHSA-rf6f-7fwh-wjgh)
- follow-redirects ≤1.15.11 → ≥1.16.0 (GHSA-r4q5-vmmm-2653)
- glob (10.x CLI) <10.5.0 → ≥10.5.0 (GHSA-5j98-mcp5-4vw2)
- js-yaml <4.1.1 → ≥4.1.1 (GHSA-mh29-5h37-fv8m)
- lodash ≤4.17.23 → ≥4.18.0 (GHSA-f23m-r3pf-42rh,
GHSA-r5fr-rjxr-66jc)
- minimatch (9.x) <9.0.7 → ≥9.0.7 (GHSA-23c5-xmqv-rm74,
GHSA-3ppc-4f35-3m26,
GHSA-7r86-cg39-jmmj)
- path-to-regexp (8.x) <8.4.0 → ≥8.4.0 (GHSA-27v5-c462-wpq7,
GHSA-j3q9-mxjg-w52f)
- picomatch (4.x) <4.0.4 → ≥4.0.4 (GHSA-3v7f-55p6-f55p,
GHSA-c2c7-rcm5-vvqj)
- qs <6.14.2 → ≥6.14.2 (GHSA-6rw7-vpxm-498p,
GHSA-w7fw-mjwx-w883)
- serialize-javascript ≤7.0.2 → ≥7.0.5 (GHSA-5c6j-r48x-rmvq,
GHSA-qj8w-gfj5-8c6v)
- socket.io-parser <4.2.6 → ≥4.2.6 (GHSA-677m-j7p3-52f9)
- tar <7.5.11 → ≥7.5.11 (GHSA-8qq5-rm4j-mr97,
GHSA-34x7-hfp2-rc4v,
GHSA-r6q2-hw4h-h46w,
GHSA-83g3-92jg-28cx,
GHSA-qffp-2rhf-9h96,
GHSA-9ppj-qmqm-q256)
- vite (non-aliased) <7.3.2 → ≥7.3.2 (GHSA-p9ff-h696-f583,
GHSA-v2wj-q39q-566r,
GHSA-4w7w-66w2-5vf9)
Scoped overrides are used where the vulnerable range is a specific major
line — e.g. `minimatch@>=9.0.0 <9.0.7` — so that 3.x/10.x lines resolving
via unrelated dependency chains are not disturbed. Otherwise the override
targets the bare package name.
Note: admin/ui/doc packages alias `vite` to `rolldown-vite@7.2.10`; those
are a separate package on npm and the vite CVEs do not apply to them.
- `pnpm install` succeeds
- `pnpm run ts-check` clean
- No source code changes; `tar` and `glob` are not directly imported by
etherpad-lite sources, so the major-version bumps (tar 6→7, glob 10→13)
affect only transitive consumers that already declare compatibility.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: Rename some occurences of etherpad-lite to etherpad
* chore: Adjust etherpad git urls
* chore: Rename more occurences from etherpad-lite to etherpad
* chore: Adjust default text
#7421 fixed the ESM/CJS interop bug where plugins using
require('ep_etherpad-lite/node/utils/Settings') got an object whose
.toolbar (and every other top-level field) was undefined, crashing
ep_font_color/ep_font_size/ep_plugin_helpers with "Cannot read
properties of undefined (reading 'indexOf')" during pad.html rendering.
That fix landed without a regression test.
Pin the contract: top-level settings fields must be reachable via a
CJS require(), the toolbar must keep its {left, right, timeslider}
shape, and setters on the shim must propagate to the underlying
settings object so reloadSettings() is visible to plugins.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add timeslider line numbers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf: coalesce timeslider line-number updates
Addresses Qodo review: updateLineNumbers() was called synchronously
from applyChangeset() on every changeset, forcing full-document layout
reads/writes during timeslider scrubbing/playback. scheduleLineNumberUpdate()
also queued a fresh double-rAF pair for every resize tick. Add a pending
flag so only one rAF pair is in flight, and route applyChangeset() through
the scheduler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: warn when a pending edit is not accepted
Show a gritter warning only when the pad disconnects while a local commit is still awaiting acceptance, leaving normal editing UI unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: cover unaccepted-commit warning path
Addresses Qodo review: adds regression coverage for the two contract
changes this PR introduces — acceptCommit() must clear the pending
marker so hasUnacceptedCommit() returns false after a server ACK, and
the disconnect handler must surface the unsaved-edit gritter when a
commit is still pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: allow undo of clear authorship colors without disconnect (#2802)
When a user clears authorship colors and then undoes, the undo changeset
re-applies author attributes for all authors who contributed text. The
server was rejecting this because it treated any changeset containing
another author's ID as impersonation, disconnecting the user.
The fix distinguishes between:
- '+' ops (new text): still reject if attributed to another author
- '=' ops (attribute changes on existing text): allow restoring other
authors' attributes, which is needed for undo of clear authorship
Also removes the client-side workaround in undomodule.ts that prevented
clear authorship from being undone at all, and adds backend + frontend
tests covering the multi-author undo scenario.
Fixes: https://github.com/ether/etherpad-lite/issues/2802
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use robust Playwright assertions in authorship undo tests
- Use toHaveAttribute with regex instead of raw getAttribute + toContain
- Check div/span attributes within pad body instead of broad selectors
- Use Playwright auto-retry (expect with timeout) instead of toHaveCount(0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle confirm dialog and sync timing in Playwright tests
- Add page.on('dialog') handler to accept the confirm dialog triggered
by clearAuthorship when no text is selected (clears whole pad)
- Use auto-retrying toHaveAttribute assertions instead of raw getAttribute
- Increase cross-user sync timeouts to 15s for CI reliability
- Add retries: 2 to multi-user test for CI flakiness
- Scope assertions to pad body spans instead of broad selectors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use persistent socket listeners to avoid missing messages in CI
Replace sequential waitForSocketEvent loops with single persistent
listeners that filter messages inline. This prevents race conditions
where messages arrive between off/on listener cycles, causing timeouts
on slower CI runners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reject - ops with foreign author to prevent pool injection
The '-' op attribs are discarded from the document but still get added
to the pad's attribute pool by moveOpsToNewPool. Without this check, an
attacker could inject a fabricated author ID into the pool via a '-' op,
then use a '=' op to attribute text to that fabricated author (bypassing
the pool existence check).
Now all non-'=' ops (+, -) with foreign author IDs are rejected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: use not.toHaveClass for cleared authorship spans
Addresses Qodo review: linestylefilter skips attribs with empty values,
so a span with author='' has no class attribute at all. The previous
negative-lookahead regex on the class attribute failed against a null
attribute and was flaky in CI. Switch to not.toHaveClass(/author-/),
which also passes when the attribute is missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a core timeslider playback speed setting with an original-speed default, a realtime mode that uses revision timestamps, and frontend coverage for the new behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* 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>
* docs(openapi): document apikey auth in openapi.json (#7532)
The API accepts the key via ?apikey=, ?api_key=, or the apikey header, but
only ?apikey= was advertised in /api-docs.json. /api/{version}/openapi.json
was worse: it hardcoded an OAuth2 scheme even when Etherpad was started in
apikey auth mode.
Switch both generators on settings.authenticationMethod and publish apiKey
schemes for the query (apikey, api_key) and header (apikey) variants. The
openapi.ts definition is now regenerated per request so runtime settings
are reflected.
The raw authorization: <key> header still works in code but is deliberately
not documented — pinning it in the spec would ossify a quirk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(openapi): add apiKeyAlias/apiKeyHeader conditionally in RestAPI.ts
In SSO mode, apiKeyAlias and apiKeyHeader were always present in
securitySchemes even though they're only relevant when
authenticationMethod is 'apikey'. Mirror the pattern used for the sso
scheme: add these two schemes dynamically inside the apikey branch, and
mark them optional in the TypeScript type annotation.
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/1d440432-7389-462e-9aac-9a3c027640e8
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.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>
* checkPlugin: flag absolute /static/plugins/ paths in templates (#5203)
Plugin templates that reference assets as \`/static/plugins/...\`
(absolute) silently break any Etherpad instance hosted behind a reverse
proxy at a sub-path — the browser resolves the path against the domain
root instead of the proxy prefix and the asset 404s. The right form is
\`../static/plugins/...\` (relative), which ep_embedmedia PR #4 fixed
manually and which #5203 asked for as a mechanical check.
Walk \`templates/\` and \`static/\` of the plugin, scan every \`*.ejs\` /
\`*.html\` for \`/static/plugins/\` not preceded by a URL scheme, dot, or
word char (so \`https://host/static/plugins/...\` and already-correct
\`../static/plugins/...\` stay untouched). Warn normally; in \`autofix\`
mode rewrite to the relative form in place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* checkPlugin: skip autofix for static/*.html
Addresses Qodo review: HTML served from a plugin's static/ directory
resolves against /static/plugins/<plugin>/static/..., so rewriting
/static/plugins/... to ../static/plugins/... yields a broken URL. Keep
scanning static/ for warnings but no longer rewrite, and clarify the
remediation guidance to point at the file's own location.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pnpm's default \`auto\` package-import-method eventually falls through
to \`copyfile\`, which uses \`copy_file_range\`. That syscall fails on
ZFS with \`EAGAIN: resource temporarily unavailable\` (see
https://github.com/pnpm/pnpm/issues/7024), so \`docker compose build\`
aborts inside the \`RUN pnpm install\` step on any host with a ZFS
root. Operators had to hand-patch every pnpm invocation in the
Dockerfile and install scripts.
Force \`package-import-method=hardlink\` in \`.npmrc\` so all pnpm
invocations (Docker build, \`bin/installDeps.sh\`,
\`bin/installLocalPlugins.sh\`, \`bin/updatePlugins.sh\`) pick up the
setting automatically. Hardlinks are fast, save disk, and work on
every filesystem Etherpad supports.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rewrite title + About to lead with what Etherpad is for
(authorship, sovereignty, malleability) before features
- Rewrite Project Status to make the maintainer ask specific and
to situate the project's 16-year track record
- Add new "Who uses Etherpad" section with categorical adopter
profiles so institutional evaluators have proof points on-page
No code or behaviour changes.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These files are stale: there's no CI/tooling left that reads them.
- .travis.yml — Etherpad moved to GitHub Actions years ago. The
workflows in .github/workflows/ are the source of truth.
- .lgtm.yml — LGTM was sunset by GitHub in late 2022.
- start.bat — README only documents the PowerShell installer for
Windows now (irm .../installer.ps1 | iex), no docs or scripts
reference start.bat.
- bin/installOnWindows.bat — same; not referenced by README, docs
or workflows.
Also drop the .travis.yml line from the plugin layout in
doc/plugins.md and replace it with a pointer at .github/workflows/.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending changesets immediately after reconnect
After reconnecting, setUpSocket() did not call handleUserChanges(),
so any edits made while disconnected were not sent to the server until
the user made another change. This caused divergent pad state between
users.
Now calls handleUserChanges() after reconnect to immediately transmit
any pending local changesets.
Fixes#5108
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: defer handleUserChanges on connect to avoid editor init race
Calling handleUserChanges() synchronously in setUpSocket() caused
"Cannot read properties of null (reading 'changeset')" because the
editor isn't fully initialized on first connect. Deferred with
setTimeout(500ms) to allow initialization to complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add test for pending changeset flush after reconnect
Verifies that edits made while disconnected are transmitted to the
server immediately upon reconnection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending changesets on actual reconnect, not just initial connect
setUpSocket() only runs during initialization. Move handleUserChanges()
to the reconnect code path so pending edits are flushed when the
connection is re-established.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending changes when isPendingRevision clears after reconnect
The existing fix in setChannelState('CONNECTED') calls handleUserChanges(),
but at that point isPendingRevision is still true so changes are blocked.
The real trigger must be in setIsPendingRevision(): when it transitions
from true to false (after all CLIENT_RECONNECT messages are processed),
call handleUserChanges() to flush any locally-queued edits.
Also adds a targeted regression test that simulates the exact reconnect
state transitions and verifies pending edits reach the server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: rewrite reconnect flush test for reliability
Replaced the fragile offline/online simulation with a direct test that
uses separate browser contexts. Simplified to a single test that
exercises the exact setIsPendingRevision(false) -> handleUserChanges()
codepath and verifies the flushed text is visible from another client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: remove Playwright reconnect test (not feasible)
The reconnect test requires access to pad.collabClient internals which
are not exposed on window in the browser context. Playwright cannot
call setStateIdle/setIsPendingRevision/setChannelState. The backend
tests adequately cover the code fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: don't flush in setChannelState to avoid editor init race
Calling handleUserChanges() in setChannelState('CONNECTED') fires
synchronously on first connect before the editor is fully initialized,
breaking chat/user_name tests. The setIsPendingRevision(false) trigger
is sufficient for the reconnect path, and the existing
setTimeout(handleUserChanges, 500) in setUpSocket() already handles
the initial connect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove setTimeout flush in setUpSocket — rely on setIsPendingRevision trigger
The setTimeout(handleUserChanges, 500) in setUpSocket was a timing hack
that:
- Only fires on initial connect (setUpSocket is called once at the end
of getCollabClient; reconnects go through pad.ts:248 which calls
setChannelState('CONNECTED') directly, bypassing setUpSocket).
- Doesn't actually fix issue #5108 (the reconnect-flush bug). That's
fixed deterministically by the wasPending && !value trigger in
setIsPendingRevision, which fires whenever the server's CLIENT_RECONNECT
message lands (both for noChanges and after replaying revisions).
- Introduced a 500ms race window on initial pad load.
The reconnect path now relies entirely on the deterministic event-based
trigger (setIsPendingRevision), with no timing assumptions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plugin CI is still failing on ERR_PNPM_IGNORED_BUILDS even with the
build-script policy declared in both pnpm-workspace.yaml (#7523) and
package.json (#7525). pnpm's strict-dep-builds defaults to true in 10+,
so any transitive dep with an unrecognized postinstall fails the build.
For etherpad-lite — and especially for downstream plugin repos that
pull this codebase as their core install — that's a footgun: the moment
some new transitive ships a postinstall, every plugin's CI explodes.
Set strictDepBuilds: false in pnpm-workspace.yaml AND
strict-dep-builds=false in .npmrc as a defensive layer, so unknown
postinstalls become a warning instead of a hard failure. The
allow/ignore lists still control what actually runs.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some pnpm versions don't read onlyBuiltDependencies / ignoredBuiltDependencies
from pnpm-workspace.yaml — leaving CI on plugin repos to fail with
ERR_PNPM_IGNORED_BUILDS even after #7523 added the workspace.yaml entries.
Mirror the same configuration into package.json's "pnpm" field, which is
the older (and more widely supported) location. The two files are kept in
sync; whichever pnpm version reads the values picks them up from one or
the other.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test was failing intermittently in CI with "Element is not attached
to the DOM" at selectText(). The long text typed by writeToPad triggers
DOM re-renders in Etherpad, and on slower CI runners the div locator
could detach between resolve and action.
Switch to keyboard-based selection (Ctrl+A) via the existing
selectAllText helper, which doesn't depend on a specific DOM element
staying attached. Verified stable across 10 repeated runs on chromium.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: explicitly ignore scarf build scripts to fix bin install CI
My earlier commit removed @scarf/scarf from onlyBuiltDependencies, but
pnpm 10.7+ requires every encountered build script to be either allowed
(onlyBuiltDependencies) or explicitly ignored (ignoredBuiltDependencies),
otherwise it errors with ERR_PNPM_IGNORED_BUILDS.
The Update Plugins workflow runs pnpm install inside ./bin, which pulls
in ep_etherpad-lite -> swagger-ui-dist -> @scarf/scarf as a transitive
dep. Add scarf to ignoredBuiltDependencies so pnpm silently skips its
install-time telemetry script instead of failing the install.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tests): serialize admin-spec tests to avoid shared-state races
Admin tests mutate global server state (install/uninstall plugins,
save settings, restart etherpad). Running them in parallel — both
across browsers (chromium + firefox) and with playwright's
fullyParallel mode — caused intermittent failures where one test's
install would leak into another test's assertions (e.g. seeing 3
installed plugins when expecting 2, or "ep_font_colormain" in the
hooks list when expecting only core hooks).
Two changes:
1. Drop firefox from test-admin — admin UI is browser-agnostic and
running two browsers concurrently was the main race source
2. Add test.describe.configure({ mode: 'serial' }) to each admin
spec file as a safety net in case the project list or worker
count changes in the future
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tests): use ep_set_title_on_pad for admin plugin install test
ep_font_color depends on ep_plugin_helpers, so installing it via the
admin UI actually installs two plugins (ep_font_color + ep_plugin_helpers),
making the installed plugins table show 3 rows (including ep_etherpad-lite)
instead of the 2 the test asserts.
Switch to ep_set_title_on_pad which has no transitive deps, so install
produces exactly one new row as the test expects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pnpm 10.7+ blocks dependency postinstall scripts unless explicitly
approved via onlyBuiltDependencies. Only esbuild genuinely needs its
postinstall (to download platform-specific native binaries).
Remove @scarf/scarf (install-time telemetry from swagger-ui-dist) and
@swc/core (not in the dependency tree) from the approved list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>