* 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>
* 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
* 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>
* 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>
* fix: capture head revision atomically with atext to prevent mismatched apply
When constructing CLIENT_VARS, pad.atext was captured at one point but
pad.getHeadRevisionNumber() was called later. If concurrent edits
advanced the revision between these two reads, the client received
initialAttributedText from rev N but rev=N+3, causing "mismatched apply"
errors when the next changeset arrived (expecting rev N+3 text).
Now captures headRev at the same time as atext and uses the captured
value consistently in CLIENT_VARS and sessionInfo.
Fixes#4040
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush missed revisions after socket joins pad room
During handleClientReady(), the server awaits the clientVars hook before
socket.join(). Any revisions appended during that await window are
broadcast to existing room members but the connecting socket misses them.
Call updatePadClients(pad) after joining to flush any such revisions.
Also adds a regression test that injects a slow clientVars hook and
verifies the connecting client receives catch-up changesets for edits
that occurred during the hook await window.
Fixes#4040
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: fix race condition in clientVars hook test
Listen for messages during handshake to avoid missing NEW_CHANGES that
arrive before the explicit waitForSocketEvent listener is attached.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: initialize sessionInfo.time before catch-up updatePadClients
The catch-up updatePadClients() call introduced in this PR could send
NEW_CHANGES with timeDelta=NaN because sessionInfo.time was never set
for new sessions. NaN poisons the client-side broadcast/timeslider
currentTime tracking.
Initialize sessionInfo.time to the timestamp of the snapshot revision
before the catch-up flush, with a fallback to Date.now() if the
revision date is unavailable.
Also strengthens the regression tests:
- Validate that initialAttributedText matches the pad AText at the
EXACT advertised rev (not just the latest pad text), using
pad.getInternalRevisionAText(rev).
- Add a load test that hammers the pad with concurrent edits while
multiple clients connect, asserting CLIENT_VARS consistency under
the exact race condition the fix is targeting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: replace open-ended load loop with bounded mid-handshake edit
The previous load test ran 'while (!stopLoad) await pad.setText(...)'
in the background while the test connected clients. This saturated
ueberDB's write queue and on shutdown the queued writes never drained,
hanging the mocha process for the full 6h GitHub Actions job timeout.
Replace it with a bounded approach: a clientVars hook lands 3 edits
mid-handshake (deterministic, no background loop, no shutdown hang).
Still exercises the exact race the fix targets — an edit advancing
the rev after the atext snapshot but before CLIENT_VARS is sent —
and asserts AText / rev consistency via getInternalRevisionAText.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: address remaining Qodo concerns on PR #7480
Addresses Qodo review items 1, 2, 5 from
https://github.com/ether/etherpad-lite/issues/comments/4194702740 :
- Concern 1 (no loadTesting reproduction test): the suite now toggles
settings.loadTest = true in before(), restores in after(). The
middle test also pre-populates the pad with 20 revisions before
connecting so we genuinely exercise a busy/loaded pad rather than a
fresh one.
- Concern 2 (no CLIENT_VARS / NEW_CHANGES delay test): the slow
clientVars hook in the middle test now has explicit setTimeout
delays before AND after the mid-handshake edits, so the race window
between atext snapshot and CLIENT_VARS send is observably wide
rather than relying on async scheduling alone. The test also
collects post-handshake messages and asserts a NEW_CHANGES catch-up
arrives when the pad advanced past the advertised rev.
- Concern 5 (test doesn't validate rev): both rev-consistency tests
use pad.getInternalRevisionAText(advertisedRev) and assert text and
attribs match, not just `pad.text() === clientVars.text`.
Concerns 3 (connect can miss revisions) and 4 (NaN timeDelta) were
already addressed in earlier commits on this branch via the catch-up
updatePadClients() call and the sessionInfo.time initialization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CJS compatibility block added in fd97532 only defined getters,
making settings properties read-only for plugins using require().
Plugins like ep_webrtc need to mutate settings (e.g. requireAuthentication)
in tests. Add setters so CJS consumers can write properties too.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: increase max socket.io message size to 10MB for large pastes
The default maxHttpBufferSize of 50KB caused socket.io to drop
connections when pasting >10,000 characters. Increased to 10MB which
safely accommodates large paste operations.
Fixes#4951
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: reduce default maxHttpBufferSize to 1MB
10MB was too generous and creates a DoS vector. 1MB (socket.io's own
default) is sufficient for large pastes while limiting memory abuse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent race condition in session cleanup timeout
When the cleanup timeout fires, check the in-memory exp.real before
reading from the DB. If touch() extended the expiry (but the old
timeout fires late, e.g. on slow CI), reschedule instead of reading
potentially stale cached data from the DB and destroying the session.
Also increased test expiry times so the "touch after eligible for
refresh" test isn't sensitive to event loop delays on slow machines.
Fixes flaky SessionStore test from #7448.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: dev mode entrypoint paths respect x-proxy-path header
In dev mode, the /watch/* script paths were hard-coded as absolute
paths without considering the x-proxy-path header used for subdirectory
reverse proxy setups. This caused 404s for the script tags when hosting
Etherpad on a subdirectory URL (e.g., /pad).
Now reads the x-proxy-path header from the request and prefixes the
entrypoint path, matching how admin.ts handles proxy paths.
Fixes#7137
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: make proxy path tests deterministic in production mode
Tests now verify entrypoint paths and x-proxy-path header handling
in production mode (where tests run) rather than conditionally
asserting only in dev mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: sanitize x-proxy-path header to prevent XSS
The header value was injected directly into <script src="...">
without sanitization. An attacker who can set request headers could
inject arbitrary HTML/JS. Now only allows path-safe characters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: very old .etherpad imports could break import due to lack of author metadata, allow this now
* test: add regression tests for old .etherpad import without author
Tests that importing an old .etherpad export (circa 2014) where
revision records lack meta.author succeeds without error, and that
getRevisionAuthor returns '' for such revisions.
Covers the fix for #6785.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sort language dropdown alphabetically by native name
Languages in the settings dropdown were ordered by language code,
making it hard to find specific languages. Now sorted alphabetically
by their native display name.
Fixes#3263
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: verify language dropdown is sorted by native name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: preserve ordered list numbering across unordered list interruptions in export
When ordered lists were interrupted by unordered lists, each new <ol>
segment started at 1 instead of continuing the previous numbering.
Track running counts per indent level and emit start attributes.
Fixes#6471
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: respect explicit start attributes and reset counters per level
- line.start takes priority over counter-based continuation when present
- Counter is seeded from line.start to keep subsequent continuations aligned
- Counters for closed indent levels are cleared when list depth decreases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the cleanup timeout fires, check the in-memory exp.real before
reading from the DB. If touch() extended the expiry (but the old
timeout fires late, e.g. on slow CI), reschedule instead of reading
potentially stale cached data from the DB and destroying the session.
Also increased test expiry times so the "touch after eligible for
refresh" test isn't sensitive to event loop delays on slow machines.
Fixes flaky SessionStore test from #7448.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add periodic cleanup of expired/stale sessions from database
SessionStore now runs a periodic cleanup (every hour, plus once on
startup) that removes:
- Sessions with expired cookies (expires date in the past)
- Sessions with no expiry that contain no data beyond the default
cookie (the empty sessions that accumulate indefinitely per #5010)
Without this, sessions accumulated forever in the database because:
1. Sessions with no maxAge never got an expiry date
2. On server restart, in-memory expiration timeouts were lost
3. There was no mechanism to clean up sessions that were never
accessed again
Fixes#5010
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve TypeScript error for sessionStore.startCleanup()
Use a local variable for the SessionStore instance to avoid type
narrowing issues with the module-level Store|null variable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — chained timeouts, cleanup tests, docs
- Replace setInterval with chained setTimeout to prevent overlapping
cleanup runs on large databases
- Store and clear startup timeout in shutdown() to prevent leaks
- Add .unref() on all timers so they don't delay process exit
- Fix misleading docstring — cleanup removes empty no-expiry sessions,
not sessions older than STALE_SESSION_MAX_AGE_MS (removed unused const)
- Add 5 regression tests: expired sessions removed, empty sessions
removed, sessions with data preserved, valid sessions preserved,
shutdown cancels timer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add cookie.sessionCleanup setting to control session cleanup
Session cleanup is now gated behind cookie.sessionCleanup (default
true). Admins who want to keep stale sessions can set this to false
in settings.json.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: POST API requests with JSON body no longer time out
When express.json() middleware parses the request body before the
OpenAPI handler runs, formidable's IncomingForm hangs forever waiting
for stream data that was already consumed. Now checks req.body first
and only falls back to formidable for multipart/form-data requests.
Also fixed case-insensitive method check (c.request.method may be
uppercase depending on openapi-backend version).
Fixes#7127
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle empty JSON body and missing method safely
- Remove Object.keys().length > 0 check on req.body so empty JSON
objects ({}) don't fall through to formidable (which would hang)
- Guard c.request.method with fallback to empty string to prevent
TypeError if method is undefined
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: prevent parameter pollution by excluding headers from field merge
Previously Object.assign merged headers, params, query, and formData
into a single fields object. This allowed POST body parameters to
override security-sensitive headers like Authorization, or headers to
pollute API parameter values.
Now only merges params, query, and formData. The Authorization header
is passed explicitly as a fallback for legacy API key authentication,
but cannot be overridden by body/query parameters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: make cookie names configurable with prefix setting
Add cookie.prefix setting (default "ep_") that gets prepended to all
cookie names set by Etherpad. This prevents conflicts with other
applications on the same domain that use generic cookie names like
"sessionID" or "token".
Affected cookies: token, sessionID, language, prefs/prefsHttp,
express_sid.
The prefix is passed to the client via clientVars.cookiePrefix in the
bootstrap templates so it's available before the handshake. Server-side
cookie reads fall back to unprefixed names for backward compatibility
during migration.
Fixes#664
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: default cookie prefix to empty string for backward compatibility
Changing the default to "ep_" would invalidate all existing sessions
on upgrade since express-session only looks for the configured cookie
name. Default to "" (no prefix) so upgrades are non-breaking — users
opt-in to prefixed names by setting cookie.prefix in settings.json.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — cookie prefix migration and fallbacks
- l10n.ts: Read prefixed language cookie with fallback to unprefixed
- welcome.ts: Use cookiePrefix for token transfer reads
- timeslider.ts: Use prefix for sessionID in socket messages
- pad_cookie.ts: Fall back to unprefixed prefs cookie for migration
- indexBootstrap.js: Pass cookiePrefix via clientVars to welcome page
- specialpages.ts: Pass settings to indexBootstrap template
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: escape regex metacharacters in cookie prefix, document Vite hardcode
- l10n.ts: Escape special regex characters in cookiePrefix before using
it in RegExp constructor to prevent runtime errors
- padViteBootstrap.js: Add comment noting the hardcoded prefix is
dev-only and must match settings.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: validate cookie prefix to prevent header injection
Reject cookie.prefix values containing characters outside
[a-zA-Z0-9_-] to prevent HTTP header injection via crafted cookie
names (e.g., \r\n sequences). Falls back to empty prefix with an
error log.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: list bugs — indent export, renumber performance, and batching
Addresses four list-related bugs:
#4426: Indented text exports as bulleted lists. Added list-style-type:none
to indent-type <ul> elements in ExportHtml.ts so exported indented content
doesn't show bullet markers.
#3504 / #5546: List operations (indent, outdent, toggle) on large lists
are O(n²) because renumberList() runs after each individual line change.
Added _skipRenumber batching flag to setLineListType() — bulk operations
in doInsertList() and doIndentOutdent() now set all line types first,
then renumber once at the end.
#6471: Ordered list numbering in exports — the start attribute is already
read from the pad's atext during export. The client-side renumberList()
correctly sets start attributes which are persisted. Added export test
to verify numbering is preserved across bullet interruptions.
Fixes#4426, #3504, #5546
Related: #6471
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — exception safety, batch removal, renumber scope
- Wrap _skipRenumber in try/finally to prevent permanent disabling on error
- Move list removal (togglingOff) into the batched mods array instead of
calling setLineListType directly (fixes O(n²) for list removal)
- Use firstLine instead of mods[0][0] for renumbering since the first
mod may be an indent/removal that renumberList skips
- Rewrite indent export test to actually create indent lines via setHTML
and unconditionally assert list-style-type:none is present
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rewrite export tests to use importHtml/exportHtml directly
The HTTP API approach (setHTML via supertest) was hanging when tests
ran standalone because the API endpoint waited for something in the
request pipeline. Using importHtml.setPadHTML and exportHtml.getPadHTML
directly is faster and more reliable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update importexport test to expect list-style-type on indent ul
The indent export fix adds style="list-style-type: none;" to indent
<ul> elements, which broke the golden test string comparison.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
numConnectedUsers in CLIENT_VARS was computed from roomSockets.length
before the new socket joined the room, so the joining user always saw
a count one less than the actual number. Added +1 to include the
joining user in the count.
Fixes#6145
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pad object's toJSON() intentionally strips the id property (since
it's part of the database key), which caused confusion when plugins
serialized the hook context. Adding padId as a top-level property on
the hook context makes it directly accessible without relying on the
pad object's internal properties.
Fixes#5814
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: appendText API now attributes text to the specified author
spliceText() was calling makeSplice() without passing author attributes,
so inserted text had no authorship attribution in the changeset — even
though the authorId was recorded in the revision metadata. Now passes
[['author', authorId]] and the pool to makeSplice() so the changeset
ops carry the author attribute, making the text show the author's color
in the editor and appear in listAuthorsOfPad.
Also fixed the same issue in pad init (first changeset creation) and
updated PadType interface to include the authorId parameter.
Fixes#6873
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: assert API response code on createPad and anonymous appendText
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: createDiffHTML API fails with "Not a changeset: undefined"
Root cause: An empty prototype override (`PadDiff.prototype._createDeletionChangeset = function() {}`)
silently replaced the real class method with a no-op returning undefined.
This caused `applyToAText(undefined, ...)` to throw "Not a changeset".
Also fixed a crash when `startRev === endRev`: the `self` property used
to access `_authors` was only initialized inside `_addAuthors()`, which
is never called when there are no changesets to process. Replaced all
`this.self!._authors` with direct `this._authors` access.
Fixes#6847
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — random pad ID and assert cleanup
- Use random ID instead of Date.now() to avoid collisions in parallel runs
- Assert HTTP 200 on deletePad in after() hook
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: filter already-deleted sessions when deleting a group
deleteSession uses setSub(..., undefined) to remove session references
from group2sessions and author2sessions, but this can leave null entries
in the sessionIDs object. When deleteGroup later iterates Object.keys
of sessionIDs and calls deleteSession on each, it throws "sessionID
does not exist" for the already-deleted sessions.
Now deleteGroup filters out null/falsy session entries before attempting
to delete them.
Fixes: https://github.com/ether/etherpad-lite/issues/5798
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add regression test for deleteGroup after deleteSession (#5798)
Creates a group, author, and session, then deletes the session first,
then deletes the group. Without the fix, deleteGroup would throw
"sessionID does not exist" when encountering the null session entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: wait for server confirmation before navigating after pad delete
The delete pad handler navigated to '/' immediately after sending the
PAD_DELETE message. Firefox (and some mobile Chrome) would close the
WebSocket before the message reached the server, causing the delete to
silently fail.
Now the client waits for the server's {disconnect: 'deleted'} response
before navigating. Also awaits pad.remove() on the server side to
ensure the operation completes before the response is sent.
Fixes: https://github.com/ether/etherpad-lite/issues/7306
Fixes: https://github.com/ether/etherpad-lite/issues/7311
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle non-creator delete and add timeout fallback
- Listen for 'shout' event to show error when non-creator tries to
delete (server sends shoutMessage instead of deleting)
- Add 5-second timeout fallback in case the server doesn't respond
(socket dropped, server crashed, etc.)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Enforce 2-space indentation across codebase
Convert all 4-space indented source files to 2-space to match
.editorconfig and project contributor guidelines.
74 files converted: admin UI components, type definitions, security
modules, test files, helpers, and utilities.
No functional changes — 2882 insertions, 2882 deletions (pure
whitespace).
Fixes#7353
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Limit admin tests to chromium and firefox
Webkit is already tested in the dedicated frontend-tests workflow.
Running it again in admin tests causes flaky failures due to slow
socket connections and external API timeouts on webkit CI runners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Pin plugins to last-known-good versions in backend tests
Pin ep_font_size@0.4.65, ep_headings2@0.2.76, ep_markdown@10.0.1
to the versions that passed on March 31. The newer versions cause
a template crash: Cannot read properties of undefined (reading
'indexOf') at pad.html:67 in toolbar.menu().
This will help narrow down which plugin update is the culprit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Unpin ep_markdown, 1.0.8 is latest and code-identical to 10.0.1
Only ep_font_size@0.4.65 and ep_headings2@0.2.76 remain pinned to
narrow down which plugin update causes the toolbar template crash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use pnpm instead of gnpm for plugin install in backend tests
gnpm ignores version pins — it reports installing the pinned version
but the plugin loader picks up the latest from its store. Switching
to pnpm for the plugin install step so version pins actually work.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Use gnpm exec pnpm for plugin install to bypass gnpm caching
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove ep_hash_auth from backend test plugin list
ep_hash_auth blocks unauthenticated requests, causing 28 backend tests
to get 500 Internal Server Error when accessing pads. The tests don't
provide credentials, so any auth plugin will break them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix ESM/CJS interop for Settings module and harden toolbar
Plugins use require('ep_etherpad-lite/node/utils/Settings') (CJS) but
Settings.ts uses export default (ESM). With tsx, CJS require puts the
default export under .default, so settings.toolbar is undefined and
ep_font_size crashes with "Cannot read properties of undefined
(reading 'indexOf')" when rendering pad.html.
Two fixes:
- Settings.ts: add property getters on module.exports so CJS consumers
can access settings properties directly
- toolbar.ts: guard against undefined buttons array to prevent crashes
if Settings interop doesn't propagate through gnpm's plugin_packages
Tested locally: 735 passing, 0 failing with all plugins.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: started with implementation
* chore: finished index page
* chore: started with double sided modal
* chore: continue
* chore: completed implementation of transfer token
* chore: fixed typescript checks