Compare commits

...

94 commits

Author SHA1 Message Date
John McLear
9a6109bd1d 10,000 commits. Worth a pause.
Thanks to every contributor: PRs, issues, reviews, translations, plugins, answered questions. This project runs on volunteer effort, not one person or one company. That's by design and it's why it's lasted.

Thanks to everyone running an instance: schools, newsrooms, gov departments, or just yourself. You trusted us with your documents. We don't take that lightly.

Thanks to the wider FOSS community. We've relied on other people's open source work more than we can credit individually, and tried to give some back. If you've never contributed to a FOSS project, this is your sign. Code, docs, translations, bug reports, or just running an instance and telling people about it, all of it counts.

The migration to modern JS tooling has been one of the best things to happen to this codebase. Easier to contribute to, easier to maintain, easier to build on, which is what matters for a 15+ year old project that intends to keep going.

Going forward: keep Etherpad boring, stable, self-hostable, no enshittification, while staying open to the plugins and integrations that make it useful. We need more maintainers and more instances. If that's you, open an issue.

Here's to the next 10,000. John McLear
2026-07-14 09:53:11 +01:00
translatewiki.net
037f4a25b5
Localisation updates from https://translatewiki.net. 2026-07-13 14:04:15 +02:00
dependabot[bot]
4755bd3e17
build(deps-dev): bump the dev-dependencies group with 3 updates (#8043)
Bumps the dev-dependencies group with 3 updates: [i18next](https://github.com/i18next/i18next), [react-i18next](https://github.com/i18next/react-i18next) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `i18next` from 26.3.5 to 26.3.6
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.5...v26.3.6)

Updates `react-i18next` from 17.0.8 to 17.0.9
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.8...v17.0.9)

Updates `vite` from 8.1.3 to 8.1.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.4/packages/vite)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-i18next
  dependency-version: 17.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vite
  dependency-version: 8.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 11:11:18 +01:00
dependabot[bot]
05d0f62403
build(deps): bump @radix-ui/react-switch from 1.3.2 to 1.3.3 (#8029)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) from 1.3.2 to 1.3.3.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 10:27:58 +01:00
dependabot[bot]
3efcbfd358
build(deps): bump ueberdb2 from 6.1.15 to 6.1.16 (#8034)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.15 to 6.1.16.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.15...v6.1.16)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 10:07:23 +01:00
dependabot[bot]
2b5d521923
build(deps): bump oidc-provider from 9.9.0 to 9.9.1 (#8036)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.9.0 to 9.9.1.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.9.0...v9.9.1)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 10:07:14 +01:00
dependabot[bot]
631caf911c
build(deps): bump lru-cache from 11.5.1 to 11.5.2 (#8037)
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.5.1 to 11.5.2.
- [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-lru-cache/compare/v11.5.1...v11.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 10:07:06 +01:00
dependabot[bot]
0aba424357
build(deps-dev): bump the dev-dependencies group across 1 directory with 2 updates (#8041)
Bumps the dev-dependencies group with 2 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [i18next](https://github.com/i18next/i18next).


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

Updates `i18next` from 26.3.4 to 26.3.5
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.4...v26.3.5)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: i18next
  dependency-version: 26.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 16:51:09 +01:00
John McLear
194f144940
housekeeping: Adjust dependabot cooldown settings for NPM
Removed cooldown settings for Docker package ecosystem and added them for NPM.
2026-07-09 15:24:26 +01:00
John McLear
c045fcf698
housekeeping: Configure cooldown for Docker dependency updates
Added cooldown settings for Docker dependencies.
2026-07-09 15:23:00 +01:00
translatewiki.net
451df9f01c
Localisation updates from https://translatewiki.net. 2026-07-09 14:03:51 +02:00
SamTV12345
ce27525d97 chore: reinstall lockfile 2026-07-07 21:31:25 +02:00
SamTV12345
9c48900092 chore: updated lockfile 2026-07-07 21:27:35 +02:00
dependabot[bot]
173dbb3448
build(deps): bump undici from 8.6.0 to 8.7.0 (#8028)
Bumps [undici](https://github.com/nodejs/undici) from 8.6.0 to 8.7.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.6.0...v8.7.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 21:20:58 +02:00
dependabot[bot]
24e0c44d91
build(deps): bump oidc-provider from 9.8.6 to 9.9.0 (#8030)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.6 to 9.9.0.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.8.6...v9.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 21:20:31 +02:00
dependabot[bot]
f65701eaaf
build(deps): bump mysql2 from 3.22.5 to 3.22.6 (#8031)
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.22.5 to 3.22.6.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.22.5...v3.22.6)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-version: 3.22.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 21:20:22 +02:00
dependabot[bot]
2f766d075d
build(deps-dev): bump the dev-dependencies group across 1 directory with 8 updates (#8032)
Bumps the dev-dependencies group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.9` | `4.1.10` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.18` | `1.1.19` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.18` | `1.2.19` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.62.1` | `8.63.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.62.1` | `8.63.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.80.0` | `7.81.0` |
| [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.138.0` | `0.139.0` |
| [vitepress](https://github.com/vuejs/vitepress) | `2.0.0-alpha.17` | `2.0.0-alpha.18` |



Updates `vitest` from 4.1.9 to 4.1.10
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest)

Updates `@radix-ui/react-dialog` from 1.1.18 to 1.1.19
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-toast` from 1.2.18 to 1.2.19
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@typescript-eslint/eslint-plugin` from 8.62.1 to 8.63.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.63.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.62.1 to 8.63.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.63.0/packages/parser)

Updates `react-hook-form` from 7.80.0 to 7.81.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.80.0...v7.81.0)

Updates `oxc-minify` from 0.138.0 to 0.139.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/minify/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.139.0/napi/minify)

Updates `vitepress` from 2.0.0-alpha.17 to 2.0.0-alpha.18
- [Release notes](https://github.com/vuejs/vitepress/releases)
- [Changelog](https://github.com/vuejs/vitepress/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vitepress/compare/v2.0.0-alpha.17...v2.0.0-alpha.18)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.81.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.139.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vitepress
  dependency-version: 2.0.0-alpha.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 21:20:12 +02:00
translatewiki.net
2c94ef9493
Localisation updates from https://translatewiki.net. 2026-07-06 14:03:45 +02:00
dependabot[bot]
fdc23c658f
build(deps): bump tsx from 4.22.4 to 4.23.0 (#8023)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.22.4 to 4.23.0.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.22.4...v4.23.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-version: 4.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 11:25:26 +02:00
dependabot[bot]
e5d6142061
build(deps): bump nano from 11.0.5 to 11.0.6 (#8025)
Bumps [nano](https://github.com/apache/couchdb-nano) from 11.0.5 to 11.0.6.
- [Release notes](https://github.com/apache/couchdb-nano/releases)
- [Commits](https://github.com/apache/couchdb-nano/compare/v11.0.5...v11.0.6)

---
updated-dependencies:
- dependency-name: nano
  dependency-version: 11.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-05 11:25:10 +02:00
dependabot[bot]
b7d2477840
build(deps): bump undici from 8.5.0 to 8.6.0 (#8021)
Bumps [undici](https://github.com/nodejs/undici) from 8.5.0 to 8.6.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.5.0...v8.6.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 21:35:34 +02:00
dependabot[bot]
4bc80d1439
build(deps-dev): bump vite in the dev-dependencies group (#8020)
Bumps the dev-dependencies group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 8.1.2 to 8.1.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.3/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 21:35:18 +02:00
dependabot[bot]
8a9c0a6e20
build(deps): bump redis from 6.0.1 to 6.1.0 (#8019)
Bumps [redis](https://github.com/redis/node-redis) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/redis/node-redis/releases)
- [Changelog](https://github.com/redis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/node-redis/compare/redis@6.0.1...redis@6.1.0)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 13:46:09 +01:00
dependabot[bot]
df1666cffa
build(deps-dev): bump the dev-dependencies group across 1 directory with 7 updates (#8017)
Bumps the dev-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.1` | `26.1.0` |
| [@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon) | `21.0.1` | `22.0.0` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.17` | `1.2.18` |
| [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) | `1.2.6` | `1.2.7` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.22.0` | `1.23.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.1` | `8.1.2` |



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

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

Updates `@radix-ui/react-dialog` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-toast` from 1.2.17 to 1.2.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-visually-hidden` from 1.2.6 to 1.2.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

Updates `lucide-react` from 1.22.0 to 1.23.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.23.0/packages/lucide-react)

Updates `vite` from 8.1.1 to 8.1.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.2/packages/vite)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@types/sinon"
  dependency-version: 22.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: lucide-react
  dependency-version: 1.23.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vite
  dependency-version: 8.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 13:33:17 +01:00
dependabot[bot]
afdd9b9500
build(deps): bump @radix-ui/react-switch from 1.3.1 to 1.3.2 (#8018)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 21:39:53 +02:00
dependabot[bot]
3b9acd487c
build(deps): bump mssql from 12.5.5 to 12.6.0 (#8010)
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.5.5 to 12.6.0.
- [Release notes](https://github.com/tediousjs/node-mssql/releases)
- [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt)
- [Commits](https://github.com/tediousjs/node-mssql/compare/v12.5.5...v12.6.0)

---
updated-dependencies:
- dependency-name: mssql
  dependency-version: 12.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 21:39:45 +02:00
dependabot[bot]
82d1fd63a5
build(deps): bump @tanstack/react-query-devtools from 5.101.0 to 5.101.2 (#8005)
Bumps [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) from 5.101.0 to 5.101.2.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.101.2/packages/react-query-devtools)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.101.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 11:22:09 +01:00
dependabot[bot]
75be62c801
build(deps): bump ueberdb2 from 6.1.14 to 6.1.15 (#8008)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.14 to 6.1.15.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.14...v6.1.15)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 11:22:03 +01:00
dependabot[bot]
ab4c7beded
build(deps): bump openapi-backend from 5.17.0 to 5.18.0 (#8011)
Bumps [openapi-backend](https://github.com/openapistack/openapi-backend) from 5.17.0 to 5.18.0.
- [Release notes](https://github.com/openapistack/openapi-backend/releases)
- [Commits](https://github.com/openapistack/openapi-backend/compare/5.17.0...5.18.0)

---
updated-dependencies:
- dependency-name: openapi-backend
  dependency-version: 5.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 11:21:49 +01:00
dependabot[bot]
be8120b14e
build(deps): bump nodemailer from 9.0.1 to 9.0.3 (#8015)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 9.0.1 to 9.0.3.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.0.3)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 9.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 11:21:39 +01:00
dependabot[bot]
33160db454
build(deps): bump mongodb from 7.3.0 to 7.4.0 (#8009)
Bumps [mongodb](https://github.com/mongodb/node-mongodb-native) from 7.3.0 to 7.4.0.
- [Release notes](https://github.com/mongodb/node-mongodb-native/releases)
- [Changelog](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md)
- [Commits](https://github.com/mongodb/node-mongodb-native/compare/v7.3.0...v7.4.0)

---
updated-dependencies:
- dependency-name: mongodb
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 22:50:26 +02:00
dependabot[bot]
9e850e17f3
build(deps): bump redis from 6.0.0 to 6.0.1 (#8013)
Bumps [redis](https://github.com/redis/node-redis) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/redis/node-redis/releases)
- [Changelog](https://github.com/redis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/node-redis/compare/redis@6.0.0...redis@6.0.1)

---
updated-dependencies:
- dependency-name: redis
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 20:41:41 +02:00
dependabot[bot]
3ac0b0f0a4
build(deps): bump surrealdb from 2.0.3 to 2.0.4 (#8006)
Bumps [surrealdb](https://github.com/surrealdb/surrealdb.js) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/surrealdb/surrealdb.js/releases)
- [Commits](https://github.com/surrealdb/surrealdb.js/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: surrealdb
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 20:09:42 +02:00
dependabot[bot]
cce7cc6cd4
build(deps): bump @tanstack/react-query from 5.101.0 to 5.101.2 (#8007)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.101.0 to 5.101.2.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.101.2/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.101.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 19:40:20 +02:00
dependabot[bot]
bce18df898
build(deps): bump oidc-provider from 9.8.5 to 9.8.6 (#8012)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.5 to 9.8.6.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.8.5...v9.8.6)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.8.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 19:40:13 +02:00
dependabot[bot]
1442aee756
build(deps): bump awalsh128/cache-apt-pkgs-action from 1.6.1 to 1.6.3 (#8014)
Bumps [awalsh128/cache-apt-pkgs-action](https://github.com/awalsh128/cache-apt-pkgs-action) from 1.6.1 to 1.6.3.
- [Release notes](https://github.com/awalsh128/cache-apt-pkgs-action/releases)
- [Commits](https://github.com/awalsh128/cache-apt-pkgs-action/compare/v1.6.1...v1.6.3)

---
updated-dependencies:
- dependency-name: awalsh128/cache-apt-pkgs-action
  dependency-version: 1.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 19:40:08 +02:00
dependabot[bot]
00e1892704
build(deps-dev): bump the dev-dependencies group across 1 directory with 12 updates (#8016)
Bumps the dev-dependencies group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.61.0` | `1.61.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.0` | `26.0.1` |
| [eslint](https://github.com/eslint/eslint) | `10.5.0` | `10.6.0` |
| [set-cookie-parser](https://github.com/nfriedly/set-cookie-parser) | `3.1.0` | `3.1.1` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.61.1` | `8.62.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.61.1` | `8.62.1` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.2` | `6.0.3` |
| [i18next](https://github.com/i18next/i18next) | `26.3.1` | `26.3.4` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.21.0` | `1.22.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.18.0` | `7.18.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.1` |
| [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.137.0` | `0.138.0` |



Updates `@playwright/test` from 1.61.0 to 1.61.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.0...v1.61.1)

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

Updates `eslint` from 10.5.0 to 10.6.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.5.0...v10.6.0)

Updates `set-cookie-parser` from 3.1.0 to 3.1.1
- [Changelog](https://github.com/nfriedly/set-cookie-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nfriedly/set-cookie-parser/compare/v3.1.0...v3.1.1)

Updates `@typescript-eslint/eslint-plugin` from 8.61.1 to 8.62.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.61.1 to 8.62.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/parser)

Updates `@vitejs/plugin-react` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react)

Updates `i18next` from 26.3.1 to 26.3.4
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.1...v26.3.4)

Updates `lucide-react` from 1.21.0 to 1.22.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.22.0/packages/lucide-react)

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

Updates `vite` from 8.0.16 to 8.1.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.1/packages/vite)

Updates `oxc-minify` from 0.137.0 to 0.138.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/minify/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.138.0/napi/minify)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: set-cookie-parser
  dependency-version: 3.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: i18next
  dependency-version: 26.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: lucide-react
  dependency-version: 1.22.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.18.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vite
  dependency-version: 8.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.138.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 19:40:04 +02:00
translatewiki.net
2ecd748fca
Localisation updates from https://translatewiki.net. 2026-06-29 14:04:24 +02:00
translatewiki.net
0cd01fce41
Localisation updates from https://translatewiki.net. 2026-06-25 14:03:14 +02:00
dependabot[bot]
40327d59d6
build(deps): bump actions/cache from 5 to 6 (#8001)
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 11:31:56 +01:00
dependabot[bot]
b45f60e20a
build(deps): bump reitzig/actions-asciidoctor from 2.0.4 to 2.0.5 (#7998)
Bumps [reitzig/actions-asciidoctor](https://github.com/reitzig/actions-asciidoctor) from 2.0.4 to 2.0.5.
- [Release notes](https://github.com/reitzig/actions-asciidoctor/releases)
- [Changelog](https://github.com/reitzig/actions-asciidoctor/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reitzig/actions-asciidoctor/compare/v2.0.4...v2.0.5)

---
updated-dependencies:
- dependency-name: reitzig/actions-asciidoctor
  dependency-version: 2.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 16:55:47 +01:00
dependabot[bot]
f5b61c3388
build(deps): bump semver from 7.8.4 to 7.8.5 (#8000)
Bumps [semver](https://github.com/npm/node-semver) from 7.8.4 to 7.8.5.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.4...v7.8.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 16:55:42 +01:00
dependabot[bot]
ff0bd2e819
build(deps): bump ueberdb2 from 6.1.13 to 6.1.14 (#7999)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.13 to 6.1.14.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.13...v6.1.14)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 16:55:22 +01:00
dependabot[bot]
8e04130969
build(deps): bump undici from 7.27.2 to 8.5.0 (#7997)
Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:01:12 +01:00
translatewiki.net
c8b544802a
Localisation updates from https://translatewiki.net. 2026-06-22 14:03:35 +02:00
John McLear
01500ca70c
chore(deps): vendor the unmaintained security escaper into core (#7993)
* chore(deps): vendor the unmaintained `security` escaper into core

The `security` npm package (escapeHTML / escapeHTMLAttribute and the JS/CSS
encoders) has had no release since 2012, yet it sits directly in Etherpad's
client-side XSS-defense path (pad_utils, domline) and the server-side HTML
export. Rather than keep a 14-year-old, single-maintainer dependency guarding
output encoding, vendor its implementation into core.

- static/js/security.ts now contains the escaping logic directly (reproduced
  verbatim from security@1.0.0, MIT, Chad Weider — byte-identical output) and
  no longer does `require('security')`. The full public API is preserved, so
  plugins that `require('ep_etherpad-lite/static/js/security')` keep working
  unchanged.
- pad_utils.ts requires the local './security' module instead of the bare
  'security' specifier (domline.ts and ExportHtml.ts already did).
- Drop `security` from src/package.json dependencies and from Minify's
  LIBRARY_WHITELIST (no bare specifier is served to the browser anymore).

Added tests/backend/specs/security.ts locking the byte-for-byte escaping
output so the vendored copy can never silently drift.

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

* fix: use ESM named exports so vitest can resolve the security module

CI "Run the new vitest tests" failed with `Cannot find module './security'`
from pad_utils.ts. vitest/vite's CJS require() shim doesn't add a `.ts`
extension when resolving a relative specifier, so `require('./security')`
couldn't locate security.ts. (The old bare `require('security')` resolved to
a real .js in node_modules, which is why this only surfaced after vendoring.)

- security.ts now uses ESM `export const` for the seven helpers instead of a
  `module.exports = {...}` block.
- pad_utils.ts imports it as `import * as Security from './security'`, which
  goes through vite's resolver (knows .ts) and is also properly typed.

CJS consumers (domline.ts, ExportHtml.ts, the backend spec) keep working via
tsx/esbuild ESM->CJS interop. Verified: tsc clean, full vitest suite 721
passing, and the mocha security/export/import specs 27 passing.

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

* ci: force fresh run (prior run used a stale merge ref after reopen)

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

* fix: remove ReDoS in vendored JSON-string-literal regex

CodeQL flagged a high-severity exponential-backtracking alert on the
JSON-string-literal regex vendored from the `security` package:
`/"(?:\\.|[^"])*"/`. The `[^"]` class also matches a backslash, so it overlaps
with the `\\.` alternative and backtracks exponentially on adversarial input
like `"\!\!\!...` (no closing quote). The original lived inside node_modules so
it was never scanned; vendoring it surfaced the alert.

Fix to the canonical linear form `/"(?:[^"\\]|\\.)*"/`, where the backslash is
excluded from the character class so the two alternatives are mutually
exclusive. It matches exactly the same well-formed JSON string literals (and
encodeJavaScriptData only ever runs it over JSON.stringify output), so behaviour
is unchanged for valid input.

Added tests: encodeJavaScriptData output + a ReDoS guard that runs the regex
over 50k adversarial chars and asserts it returns in well under a second.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:56:40 +01:00
John McLear
7168f14f0a
chore(deps): drop three unmaintained dependencies (unorm, find-root, jsonminify) (#7992)
* chore(deps): drop three unmaintained dependencies from core

Remove dependencies whose upstreams are effectively abandoned, replacing
each with a maintained alternative or native API. No behaviour change for
users; reduces the production dependency surface.

- unorm (last publish 2019): replace `UNorm.nfc(s)` in contentcollector
  with native `String.prototype.normalize('NFC')`, available in every
  supported Node and browser. Also drop it from Minify's LIBRARY_WHITELIST.
- find-root (last publish 2017): inline a ~10-line equivalent in
  AbsolutePaths.findEtherpadRoot(), mirroring find-root's semantics
  (closest ancestor containing package.json, throw if none).
- jsonminify (last publish 2021): swap settings parsing to jsonc-parser
  (already used by the admin workspace, actively maintained). The old
  `jsonminify(str).replace(',]', ']').replace(',}', '}')` had two bugs that
  jsonc-parser's allowTrailingComma fixes: String#replace only swapped the
  FIRST trailing comma of each kind, and the blind replace corrupted ',]' /
  ',}' byte sequences inside string values (e.g. URLs).

Added a regression test in settings.ts covering multiple trailing commas
and ',]'/',}' inside string values.

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

* fix: drop stale $unorm bundle entry from tar.json

The unorm removal left `$unorm/lib/unorm.js` listed in the ace2_inner.js
client bundle manifest. With unorm uninstalled, getTar() would point at a
node_modules asset that no longer exists, producing 404s when loading that
bundle. Nothing imports unorm anymore (contentcollector now uses native
String.prototype.normalize), so the entry is dead and removed.

Caught by Qodo review.

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

* fix: update container loadSettings helper to jsonc-parser

tests/container/loadSettings.js is a standalone helper (separate from
node/utils/Settings.ts) that parsed settings.json.docker with jsonminify
directly. Removing jsonminify from dependencies broke the container test
suite with MODULE_NOT_FOUND. Switch it to jsonc-parser to match Settings.ts.

Verified loadSettings() parses settings.json.docker and applies the
container ip/port overrides.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:53:27 +01:00
John McLear
01d0b08a4e
docs: migrate useful wiki content into the manual (#7990) (#7994)
* docs: migrate useful wiki content into the VitePress manual (#7990)

The GitHub wiki is being retired; documentation should ship with the
software. This migrates the still-accurate, non-duplicate wiki pages into
the published VitePress site (doc/**/*.md + the sidebar in
doc/.vitepress/config.mts) so they are versioned, searchable and portable:

- deployment.md: reverse-proxy configs (Nginx/Apache/Caddy/Traefik/
  HAProxy) with the WebSocket-upgrade rules, subdirectory hosting via
  X-Proxy-Path, native HTTPS via the ssl block, a systemd unit, and the
  Istio manifest (with the Redis-adapter multi-replica caveat).
- accessibility.md: editor keyboard shortcuts (verified against
  ace2_inner.ts / broadcast_slider.ts / pad_editbar.ts), toolbar
  navigation, NVDA notes.
- faq.md: install methods, URL-path reference, listing/deleting pads
  (API-first), backup/restore, and history pruning.
- development.md: source-tree tour, the pad<->format conversion pipeline,
  the internal DB API, and the Fontello toolbar-icon workflow.
- database.md: the key/value schema plus connecting MySQL/PostgreSQL/Redis
  backends and a pgloader MySQL->PostgreSQL migration (database docs were
  previously absent from the VitePress site).

Every page was checked against the current source before inclusion:
corrected the apt instructions to the live signed repo (stable/main,
signed-by key), dropped the unpublished snap, fixed the Redis dbSettings
(flat host/port/password or url, not the obsolete client_options),
dropped charset from the PostgreSQL example, and removed a phantom
getEtherpad API reference. The VitePress site builds cleanly
(pnpm run docs:build) with the dead-link checker enabled.

Closes #7990

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

* docs: add verified hands-on changeset/atext walkthrough (#7990)

Migrate the practical Changeset-library tutorial from the wiki into
changeset_library.md, rewritten against the current API: unpack(),
deserializeOps() (replacing the deprecated opIterator) and
new AttributePool() (replacing the removed AttributePoolFactory). Every
example output was produced by running the code against the current
Changeset.ts / AttributePool.ts, not copied from the wiki. Also fixes a
stale ether/etherpad-lite source link.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:52:33 +01:00
Etherpad Release Bot
41dc87edf9 Merge branch 'master' into develop 2026-06-21 18:45:44 +00:00
Etherpad Release Bot
93e5bcc1e2 bump version 2026-06-21 18:45:43 +00:00
Etherpad Release Bot
3c90fa07c3 Merge branch 'develop' 2026-06-21 18:45:43 +00:00
SamTV1998
851b1fb613 feat(changelog): added readme for 3.3.2 2026-06-21 20:43:09 +02:00
dependabot[bot]
9047aacc0a
build(deps): bump undici from 7.27.2 to 8.5.0 (#7991)
Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 13:11:58 +01:00
John McLear
773a9042a7
fix(import): correct outdated import-format help message (#7988) (#7989)
* fix(import): correct outdated import-format help message (#7988)

The import dialog's "no converter" notice claimed only plain text and
HTML could be imported and linked users to the legacy AbiWord wiki page,
prompting them to install LibreOffice for formats that already work
natively.

Etherpad imports .txt, .html, .docx (via mammoth) and .etherpad files
without LibreOffice; only .pdf/.odt/.doc/.rtf still need it. Update the
message to say so and move the help link to the ether/etherpad org.

Closes #7988

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

* fix(import): point help link to new LibreOffice wiki page (Qodo #7989)

The AbiWord wiki pages were vandalized/empty. Added a clean LibreOffice
setup page on the wiki and point the import dialog there instead.

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

* fix(import): reference Etherpad docs instead of wiki for LibreOffice

The wiki is being retired, so don't link to it. Point users at the
documentation site for installing LibreOffice for extra import formats.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:08:46 +01:00
dependabot[bot]
926c94ea8a
build(deps): bump undici from 7.27.2 to 8.5.0 (#7986)
Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 12:39:27 +01:00
dependabot[bot]
bf5988580e
build(deps-dev): bump the dev-dependencies group across 1 directory with 2 updates (#7987)
Bumps the dev-dependencies group with 2 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [react-hook-form](https://github.com/react-hook-form/react-hook-form).


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

Updates `react-hook-form` from 7.79.0 to 7.80.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.79.0...v7.80.0)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.80.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 12:38:16 +01:00
dependabot[bot]
b9ec85779f
build(deps): bump pg from 8.21.0 to 8.22.0 (#7985)
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.21.0 to 8.22.0.
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.22.0/packages/pg)

---
updated-dependencies:
- dependency-name: pg
  dependency-version: 8.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-20 16:02:17 +01:00
dependabot[bot]
acba429e1a
build(deps): bump actions/checkout from 6 to 7 (#7977)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 12:47:45 +01:00
John McLear
8c5de8446c
fix(bin): migrate importSqlFile & migrateDirtyDBtoRealDB to ueberdb2 promise API (#7983)
Both scripts still called the pre-v6 callback-style ueberdb2 API, producing
type errors (masked in places by `// @ts-ignore`) against the current
promise-based signatures (`set(key, value)`, `init()`, `close()` — no
callback/extra args):

  importSqlFile.ts(73)            initDb(null)        Expected 0 arguments, but got 1
  migrateDirtyDBtoRealDB.ts(51)   db.set(k,v,bcb,wcb) Expected 2 arguments, but got 4
  migrateDirtyDBtoRealDB.ts(56)   db.close(null)      Expected 0 arguments, but got 1
  migrateDirtyDBtoRealDB.ts(57)   dirty.close(null)   Expected 0 arguments, but got 1

- importSqlFile: drop the unused `util` import and the `util.promisify`
  wrappers; `await db.init()`, `await db.set(...)`, `await db.close()`
  directly. Removes two `// @ts-ignore` that were hiding the broken calls.
- migrateDirtyDBtoRealDB: replace the bcb/wcb callback machinery with
  `await db.set(key, value)` in the loop and call `close()` with no args.
  Also fixes the progress log which referenced an undefined `length`
  instead of `keys.length`.

Pure type/correctness cleanup; behaviour is unchanged (writes are now
awaited, which is equivalent or safer). `tsc --noEmit` on the bin package
is now clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:51:34 +01:00
John McLear
9d2dae1b63
fix(bin): close DBs in migrateDB so it flushes and exits (#7982)
`bin/migrateDB.ts` opens a source and target ueberdb2 Database, copies all
keys, then resolves without closing either connection or calling
process.exit(). Two problems with ueberdb2 6.1.x:

- 6.1.x keeps an internal keep-alive timer running until close() is called,
  so the migration process hangs forever after "Done syncing dbs" instead
  of exiting. (Pre-6.1.x it exited on its own once the work was done.)
- Target writes are buffered and only guaranteed flushed to disk on close()
  /flush(), so an operator who Ctrl-Cs the apparently-finished process could
  end up with an incomplete migration.

Close the target then the source on both the success and error paths (which
flushes buffered writes and clears the keep-alive timer) and exit with an
explicit status code, matching the pattern already used in
migrateDirtyDBtoRealDB.ts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:48:39 +01:00
John McLear
32249d99e2
fix(ci): reap whole server tree in installer smoke test so it can't hang 6h (#7981)
The "Installer test" workflow has hung for 6 hours (until GitHub's job
ceiling cancels it) on every ubuntu/macos run since v3.2.0. The smoke
test starts `pnpm run prod` in the background, confirms /api responds,
then tears it down with:

    kill "$PID"
    wait "$PID"

`pnpm run prod` is a nested launcher (pnpm -> pnpm --filter -> node), so
$PID is only the outer pnpm. SIGTERM is forwarded down the chain and the
script then `wait`s on it, but if the node server doesn't exit (e.g. a
live flush timer keeping the event loop alive) the wait blocks forever
and the step never releases its output pipe -> 6h hang. Windows passed
because it uses `Stop-Process -Force`.

Fix the teardown to be robust regardless of server shutdown behaviour:
- `set -m` so the launcher gets its own process group
- kill the whole group (SIGTERM, then SIGKILL fallback) via a trap
- drop the blocking `wait`
- add `timeout-minutes: 8` to both smoke steps as a hard backstop so a
  future hang fails in minutes, not 6 hours

This unblocks CI on PRs that touch the installer workflow. The
underlying clean-shutdown regression (server not exiting on SIGTERM,
likely the ueberDB flush-timer setInterval) is tracked separately.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:37:04 +01:00
dependabot[bot]
2109c05ad4
build(deps): bump undici from 7.27.2 to 8.5.0 (#7980)
Bumps [undici](https://github.com/nodejs/undici) from 7.27.2 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.2...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 22:31:52 +01:00
dependabot[bot]
f6f665ff60
build(deps-dev): bump the dev-dependencies group with 2 updates (#7978)
Bumps the dev-dependencies group with 2 updates: [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) and [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify).


Updates `lucide-react` from 1.20.0 to 1.21.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.21.0/packages/lucide-react)

Updates `oxc-minify` from 0.136.0 to 0.137.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/minify/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.137.0/napi/minify)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.137.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 21:48:32 +01:00
dependabot[bot]
b680076534
build(deps): bump ueberdb2 from 6.1.9 to 6.1.13 (#7979)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.9 to 6.1.13.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.9...v6.1.13)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 21:47:16 +01:00
translatewiki.net
b1792469b1
Localisation updates from https://translatewiki.net. 2026-06-18 14:02:55 +02:00
dependabot[bot]
3aa13197e4
build(deps): bump nodemailer from 9.0.0 to 9.0.1 (#7976)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 9.0.0 to 9.0.1.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 9.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:19:51 +01:00
dependabot[bot]
dfdbceebe2
build(deps-dev): bump the dev-dependencies group across 1 directory with 7 updates (#7970)
Bumps the dev-dependencies group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.16` | `1.2.17` |
| [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) | `1.2.5` | `1.2.6` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.61.0` | `8.61.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.61.0` | `8.61.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.18.0` | `1.20.0` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.17.0` | `7.18.0` |



Updates `@radix-ui/react-dialog` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-toast` from 1.2.16 to 1.2.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-visually-hidden` from 1.2.5 to 1.2.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

Updates `@typescript-eslint/eslint-plugin` from 8.61.0 to 8.61.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.61.0 to 8.61.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/parser)

Updates `lucide-react` from 1.18.0 to 1.20.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.20.0/packages/lucide-react)

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

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: lucide-react
  dependency-version: 1.20.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.18.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 22:23:04 +01:00
John McLear
a698e34772
chore(release): park the non-functional ep_etherpad npm publish (#7922)
The releaseEtherpad workflow renames ep_etherpad-lite -> ep_etherpad and
publishes ./src to npm, but that publish is not load-bearing:

- `ep_etherpad` has 0 dependents on npm; nothing in this repo depends on it.
- Plugins import `ep_etherpad-lite` resolved from the LOCAL core install, and
  plugin CI clones `ether/etherpad` rather than `npm install`-ing core.
- Etherpad is run via git clone / Docker / zip / snap, never `npm install`.

It has been failing with E404 (the ep_etherpad package has no OIDC trusted
publisher configured on npmjs.com), which is why npm is stuck at 2.5.0 while
3.0/3.1/3.2/3.3 shipped fine without it.

Rather than chase a trusted-publisher setup for a publish nobody consumes,
park the workflow: gate the job behind an explicit `confirm: true` dispatch
input so a stray run fails fast with a clear message instead of a confusing
404, and document the status in the header + the AGENTS.MD Releasing section.

This is the package owner's (samtv12345) call: either finish the trusted-
publisher config to revive it, or remove the workflow. Parked pending that
decision; nothing about the release depends on it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:18:41 +01:00
John McLear
ce038b0b0f
fix(pad): keep token-less Delete pad reachable without pad-wide settings (#7959) (#7960)
* fix(pad): keep token-less Delete pad reachable without pad-wide settings (#7959)

The token-less "Delete pad" button (#delete-pad) was nested inside the
enablePadWideSettings-gated pad-settings section, so disabling pad-wide
settings removed the only way to delete a pad without a recovery token.
Combined with #7926 hiding the token disclosure when deletion needs no
token (e.g. allowPadDeletionByAllUsers), a user who was allowed to delete
could be left with no deletion UI at all.

Pad deletion is unrelated to pad-wide settings, so:

- Move #delete-pad out of the enablePadWideSettings block in pad.html; it
  is now always rendered and hidden by default.
- Add a canDeletePad clientVar (isCreator || allowPadDeletionByAllUsers)
  and drive the button's visibility from it in pad_editor.ts, mirroring the
  existing canDeleteWithoutToken handling for the token disclosure.

The two controls are now mutually coherent and neither depends on
enablePadWideSettings: the plain button shows when this session can delete
without a token, the recovery-token disclosure shows otherwise.

Tests:
- backend padDeletionUiPlacement.ts: #delete-pad is rendered with
  enablePadWideSettings both on and off (fails without the template move).
- backend socketio.ts: canDeletePad reflects the creator/allow-all matrix,
  including a non-creator who only gains it under allowPadDeletionByAllUsers.
- frontend pad_settings.spec.ts: asserts #delete-pad is no longer a
  descendant of #pad-settings-section.

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

* fix(pad): never let readonly sessions delete via token-less paths (#7959)

Qodo review of #7960: `canDeletePad` was `isCreator || allowPadDeletionByAllUsers`,
so under allowPadDeletionByAllUsers a readonly viewer received
canDeletePad=true and the relocated #delete-pad button unhid for them.
Worse, the server-side handlePadDelete `flagOk`/`creatorOk` branches never
checked session.readonly either, so a readonly-link holder could actually
delete the pad without a token — a data-loss hole that the new always-rendered
button would expose.

Exclude readonly sessions from both the clientVar and the server's token-less
authorization paths. A valid recovery token (tokenOk) stays a sufficient
credential regardless of session mode.

Test: socketio.ts asserts a readonly viewer gets canDeletePad=false and that a
token-less PAD_DELETE from a readonly session leaves the pad intact (red before
this change on the clientVar assertion).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:18:23 +01:00
dependabot[bot]
d67aa3ebd3
build(deps): bump undici from 8.4.1 to 8.5.0 (#7972)
Bumps [undici](https://github.com/nodejs/undici) from 8.4.1 to 8.5.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.4.1...v8.5.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 15:17:49 +01:00
dependabot[bot]
4ba72b3919
build(deps): bump oidc-provider from 9.8.4 to 9.8.5 (#7973)
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.4 to 9.8.5.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/node-oidc-provider/compare/v9.8.4...v9.8.5)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 15:17:36 +01:00
dependabot[bot]
bfe3fad256
build(deps): bump @radix-ui/react-switch from 1.3.0 to 1.3.1 (#7974)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) from 1.3.0 to 1.3.1.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 15:17:10 +01:00
John McLear
3e53962fb5
fix(deps): force @opentelemetry/core >=2.8.0 (CVE-2026-54285) (#7975)
Pin the transitive @opentelemetry/core dep (pulled in via
@elastic/elasticsearch -> @elastic/transport) to >=2.8.0 to clear
GHSA-8988-4f7v-96qf / CVE-2026-54285: W3CBaggagePropagator.extract()
did not enforce W3C size limits on inbound baggage headers, allowing
unbounded memory allocation. @elastic/transport declares the dep as
"2.x" so 2.8.0 satisfies the existing range with no parent bump, and
2.8.0's @opentelemetry/api peer range (>=1.0.0 <1.10.0) is satisfied
by the 1.9.1 already in the tree.

Override added to pnpm-workspace.yaml alongside the other CVE
force-bumps (pnpm 11 ignores root package.json pnpm.overrides).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:53:20 +01:00
John McLear
a59b310dbf
deps: pin ueberdb2 to 6.1.9 (fixes standalone-server startup exit) + run deb smoke on PRs (#7969)
ueberdb2 6.1.10 rewrote the cache/buffer layer (CacheAndBufferLayer.ts),
replacing the constructor's always-on, *referenced* `setInterval` flush timer
with a lazily-armed `setTimeout` that is `.unref()`'d and only created when
there are dirty keys. On a fresh, empty dirty DB there are no dirty keys, so no
flush timer is ever armed and ueberdb2 no longer anchors Node's event loop. In
the packaged (.deb/systemd) production boot this exposes a startup window where
the loop has no referenced handle and the process exits cleanly (code 0) before
`server.listen()` binds the port — so the server never serves /health.

Symptom on develop: the Debian-package amd64 smoke test failed on three
consecutive pushes (#7966 ueberdb2 6.1.9->6.1.12, then #7965, #7967), with the
service logging the version banner then "Deactivated successfully" and the
health check on :9001 never connecting. Backend/Docker/downstream-smoke stayed
green because they keep the loop alive by other means; only the bare
fresh-empty-dirty-DB packaged boot hits the gap. ueberdb2 6.1.12 is the latest
published release, so there is no fixed version to roll forward to yet — pin
back to the last green release (6.1.9) on both src/ and bin/.

Also add a `pull_request` trigger to the Debian-package workflow (scoped to the
same production-footprint paths). The smoke step is the only check that catches
this "boots then exits before binding" class of regression, but it previously
ran only on push to develop — i.e. *after* merge — which is exactly why the
Dependabot bump turned develop red instead of being blocked at PR time. The
release/apt-publish jobs are tag-guarded, so PRs run the build+smoke job only.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:39:37 +01:00
forkivan
0c0f5a8ec4
PadManager: reject unreachable '.' and '..' pad ids (#7962)
* PadManager: reject unreachable '.' and '..' pad ids

isValidPadId accepted pad ids that consist only of URL dot-segments
('.' and '..'). Per the WHATWG URL standard a browser normalises
"/p/." to "/p/" and "/p/.." to "/", so such a pad can never be opened
or exported: the request arrives at "/p/" and Etherpad answers
"Cannot GET /p/". The pad is created in the database but is forever
unreachable.

Reject these ids in isValidPadId so the broken pads can no longer be
created, and add a regression test that fails without the fix.

* adminsettings: allow deleting legacy '.'/'..' pads

The isValidPadId tightening makes getPad() reject the pad ids '.' and
'..', which also blocks their deletion: a pad with such an id created
before the change still exists (doesPadExists is true), so the admin
deletePad handler takes the "healthy" branch where getPad() now throws.
The outer catch swallowed that error without emitting a terminal
results:deletePad, leaving an undeletable orphan in the admin UI.

Fall back to the existing raw key purge when getPad() throws, so these
pads can still be removed. Adds a regression test.

* tests: run the isValidPadId regression under the mocha suite

The original unit test lived in the vitest backend-new suite, but PadManager
loads DB, Pad and customError with CommonJS require() at import time. Under
vitest those require() calls are resolved by Node natively and fail on the .ts
sources ("Cannot find module '../utils/customError'"); vi.mock could not
intercept them, so the suite errored before any test ran. It also used a
top-level `await import`, which tripped tsc TS1378 under the project tsconfig.

Move the test to the mocha backend suite, which runs with --import=tsx and
resolves the .ts requires natively, so PadManager can be required directly with
no mocking. isValidPadId is a pure function and DB only connects lazily in
DB.init(), so loading the module has no side effects and no database is needed.
2026-06-16 11:13:02 +01:00
John McLear
099de84cd1
deps: resolve open Dependabot security alerts (#7967)
* deps: resolve open Dependabot security alerts

Bump transitive dependencies flagged by Dependabot via pnpm-workspace
overrides. Refreshes stale override floors that the advisory ranges have
since grown past, and adds overrides for newly-flagged packages:

- form-data  -> >=4.0.6  (GHSA-hmw2-7cc7-3qxx, high)
- ws         -> >=8.21.0 (GHSA-96hv-2xvq-fx4p / GHSA-58qx-3vcg-4xpx, high/med)
- esbuild    -> >=0.28.1 (GHSA-gv7w-rqvm-qjhr / GHSA-g7r4-m6w7-qqqr, high/low)
- basic-ftp  -> >=5.3.1  (GHSA-rpmf-866q-6p89, high) [stale floor: 5.3.0 still vuln]
- tar        -> >=7.5.16 (GHSA-vmf3-w455-68vh, med)  [stale floor: 7.5.11]
- js-yaml    -> >=4.2.0  (GHSA-h67p-54hq-rp68, med)  [stale floor: 4.1.1 now vuln]
- qs         -> >=6.15.2 (GHSA-q8mj-m7cp-5q26, med)  [stale floor: 6.14.2 still vuln]
- ip-address -> >=10.1.1 (GHSA-v2v4-37r5-5v8g, med)
- @babel/core-> >=7.29.6 (GHSA-4x5r-pxfx-6jf8, low)

ts-check, backend test-utils, and the vite/esbuild production build all
pass locally on Node 24.

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

* deps: cap basic-ftp override to 5.x to avoid major bump

Qodo flagged the open-ended `basic-ftp@<5.3.1: '>=5.3.1'` override as
resolving to 6.0.1 (a surprise major) on the runtime plugin-install path
(live-plugin-manager -> proxy-agent -> get-uri -> basic-ftp). The CVE fix
(5.3.1) exists on the 5.x line, so bound the override to '>=5.3.1 <6.0.0'
for the minimal, lowest-risk patch. Now resolves to basic-ftp@5.3.1.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:32:06 +01:00
dependabot[bot]
8a4e1feed8
build(deps): bump awalsh128/cache-apt-pkgs-action from 1.6.0 to 1.6.1 (#7963)
Bumps [awalsh128/cache-apt-pkgs-action](https://github.com/awalsh128/cache-apt-pkgs-action) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/awalsh128/cache-apt-pkgs-action/releases)
- [Commits](https://github.com/awalsh128/cache-apt-pkgs-action/compare/v1.6.0...v1.6.1)

---
updated-dependencies:
- dependency-name: awalsh128/cache-apt-pkgs-action
  dependency-version: 1.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 19:08:07 +01:00
dependabot[bot]
2fdf202089
build(deps): bump nodemailer from 8.0.11 to 9.0.0 (#7965)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.11 to 9.0.0.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 19:08:00 +01:00
dependabot[bot]
abdfa03f84
build(deps): bump ueberdb2 from 6.1.9 to 6.1.12 (#7966)
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 6.1.9 to 6.1.12.
- [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ether/ueberDB/compare/v6.1.9...v6.1.12)

---
updated-dependencies:
- dependency-name: ueberdb2
  dependency-version: 6.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 19:07:47 +01:00
dependabot[bot]
c8e3248bcd
build(deps-dev): bump the dev-dependencies group with 6 updates (#7964)
Bumps the dev-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.60.0` | `1.61.0` |
| [eslint](https://github.com/eslint/eslint) | `10.4.1` | `10.5.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.78.0` | `7.79.0` |
| [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.135.0` | `0.136.0` |


Updates `@playwright/test` from 1.60.0 to 1.61.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.0)

Updates `eslint` from 10.4.1 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/HEAD/packages/vitest)

Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3)

Updates `react-hook-form` from 7.78.0 to 7.79.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.78.0...v7.79.0)

Updates `oxc-minify` from 0.135.0 to 0.136.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/minify/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.136.0/napi/minify)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.79.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: oxc-minify
  dependency-version: 0.136.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 16:38:06 +01:00
translatewiki.net
4371af77c2
Localisation updates from https://translatewiki.net. 2026-06-15 14:03:51 +02:00
John McLear
2ba72de1de
feat(pad): suppress deletion token for durable identities + relabel recovery action (#7926) (#7930)
* feat(pad): suppress deletion token for durable identities; relabel recovery action (#7926)

Builds on the allowPadDeletionByAllUsers suppression with the rest of the
ideas discussed on the issue.

Server (handleClientReady):
- A creator's deletion token is now also withheld when they have a *durable*
  identity: authenticated (req.session.user with a username) AND the deployment
  pins that identity to a stable authorID via a getAuthorId hook. Only then does
  the creator path (author === revision-0 author) survive a cookie clear or a
  different device, making the recovery token redundant.
- This deliberately tightens the previous `requireAuthentication => always
  suppress` rule: without a getAuthorId hook the authorID still comes from the
  per-browser token cookie, so an authenticated user on a second device is NOT
  the creator. Withholding the token there would strand them, so they now keep
  getting one. SSO deployments using the documented getAuthorId pattern get the
  clean no-modal experience.
- New `canDeleteWithoutToken` clientVar (allowPadDeletionByAllUsers OR durable
  identity) drives the client label.

Client (pad_editor.ts):
- When canDeleteWithoutToken, the recovery-token disclosure summary is labelled
  plainly "Delete Pad" (reusing the already-translated pad.settings.deletePad
  key) instead of the jargon "Delete with token".

Tests (socketio.ts): anonymous -> token; allowPadDeletionByAllUsers -> none;
authenticated without a getAuthorId hook -> token; authenticated with one ->
none. Verified in a browser for both label/modal outcomes.

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

* fix(pad): render recovery disclosure for all sessions; hide it (not relabel) when no token is needed

Addresses Qodo review on #7930:

1. Token UI fully suppressed when not needed (was: only the summary relabelled).
   When canDeleteWithoutToken (allowPadDeletionByAllUsers or a durable identity),
   pad_editor.ts now hides the whole #delete-pad-with-token disclosure — label,
   token field and submit — so no deletion-token wording remains. With the plain
   "Delete Pad" button present this is also the cleaner UX.

2. Recovery disclosure no longer gated on !requireAuthentication. Because a token
   can now be issued under requireAuthentication when the deployment lacks a
   durable getAuthorId mapping, the template must render the recovery form there
   too — otherwise an authenticated creator gets a token with no UI to enter it
   on another device. It is rendered hidden by default and shown by the client
   only when canDeleteWithoutToken is false.

3. API.createPad aligned: also returns null deletionToken under
   allowPadDeletionByAllUsers, matching the socket/UI path.

Tests: deletePad.ts gains a createPad-under-allowPadDeletionByAllUsers case.
Verified live in Chromium: allowAll -> disclosure hidden, no modal; default
anonymous -> disclosure visible, modal shown.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:47:11 +01:00
dependabot[bot]
6a5ad2d90d
build(deps-dev): bump lucide-react in the dev-dependencies group (#7951)
Bumps the dev-dependencies group with 1 update: [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react).


Updates `lucide-react` from 1.17.0 to 1.18.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.18.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.18.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-13 14:34:55 +01:00
John McLear
f79cabe74c
Env-var overrides for offline/air-gapped installs (update check, plugin catalog, updater) (#7917)
* feat(settings): env-var overrides for update-check/plugin-catalog/updater (offline installs)

Air-gapped and firewalled deployments could not disable Etherpad's outbound
calls (the hourly version check, the admin plugin catalogue, and the
self-updater) without editing settings.json inside the container image — the
shipped settings.json.docker hardcoded updates.tier and omitted the privacy
block entirely, so there was no env-var to flip.

Wire the relevant keys through the existing ${ENV:default} substitution in both
settings.json.docker and settings.json.template:

  - PRIVACY_UPDATE_CHECK            (privacy.updateCheck, default true)
  - PRIVACY_PLUGIN_CATALOG          (privacy.pluginCatalog, default true)
  - UPDATES_TIER                    (updates.tier, default notify; "off" = no calls)
  - UPDATES_SOURCE / UPDATES_CHANNEL / UPDATES_CHECK_INTERVAL_HOURS /
    UPDATES_GITHUB_REPO / UPDATES_REQUIRE_ADMIN_FOR_STATUS (docker)
  - UPDATE_SERVER                   (updateServer endpoint)

Document the full set in doc/docker.md (new "Updates & privacy" section) and
cross-link from doc/admin/updates.md. Add backend regression tests that parse
the shipped settings.json.docker and settings.json.template and assert the
overrides apply with correct boolean/numeric coercion, so a future edit that
drops the ${ENV} placeholders fails loudly.

Addresses #7911 (item 2). No code changes — config + docs + tests only.

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

* docs: address Qodo review — point to PRIVACY.md, clarify tier=off scope

- Outbound-call docs/comments referenced doc/privacy.md (the storage/logging
  doc); the canonical outbound-call inventory is repo-root PRIVACY.md, which the
  runtime messages in UpdateCheck.ts / Settings.ts also reference. Re-point
  settings.json.{template,docker} and doc/docker.md there.
- doc/admin/updates.md said updates.tier="off" means "no HTTP request will leave
  the instance", but the legacy UpdateCheck.ts call to ${updateServer}/info.json
  is gated by privacy.updateCheck, not updates.tier. Clarify that air-gapped
  installs must set PRIVACY_UPDATE_CHECK=false too.

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

* docs: link PRIVACY.md via absolute URL to satisfy VitePress dead-link check

PRIVACY.md lives at the repo root, outside the doc/ tree VitePress builds, so a
relative link to it (../PRIVACY.md / ../../PRIVACY.md) is flagged as a dead link
and fails `docs:build`. Use the absolute GitHub URL instead, matching how
doc/configuration.md already links settings.json.template.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:32:46 +01:00
dependabot[bot]
662678275a
build(deps): bump esbuild from 0.28.0 to 0.28.1 (#7952)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.28.0 to 0.28.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 21:29:42 +02:00
dependabot[bot]
8e4eb2646a
build(deps): bump semver from 7.8.3 to 7.8.4 (#7943)
Bumps [semver](https://github.com/npm/node-semver) from 7.8.3 to 7.8.4.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.3...v7.8.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 12:42:25 +01:00
dependabot[bot]
e313366b49
build(deps): bump nodemailer and @types/nodemailer (#7950)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) and [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer). These dependencies needed to be updated together.

Updates `nodemailer` from 8.0.10 to 8.0.11
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.10...v8.0.11)

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

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 8.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/nodemailer"
  dependency-version: 8.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 12:42:14 +01:00
dependabot[bot]
3cddff7b27
build(deps): bump mongodb from 7.2.0 to 7.3.0 (#7941)
Bumps [mongodb](https://github.com/mongodb/node-mongodb-native) from 7.2.0 to 7.3.0.
- [Release notes](https://github.com/mongodb/node-mongodb-native/releases)
- [Changelog](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md)
- [Commits](https://github.com/mongodb/node-mongodb-native/compare/v7.2.0...v7.3.0)

---
updated-dependencies:
- dependency-name: mongodb
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 12:36:39 +01:00
dependabot[bot]
817ca2546e
build(deps-dev): bump @types/node in the dev-dependencies group (#7944)
Bumps the dev-dependencies group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 12:15:23 +01:00
dependabot[bot]
eefdfc44b9
build(deps): bump pdfkit from 0.19.0 to 0.19.1 (#7945)
Bumps [pdfkit](https://github.com/foliojs/pdfkit) from 0.19.0 to 0.19.1.
- [Release notes](https://github.com/foliojs/pdfkit/releases)
- [Changelog](https://github.com/foliojs/pdfkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/foliojs/pdfkit/compare/v0.19.0...v0.19.1)

---
updated-dependencies:
- dependency-name: pdfkit
  dependency-version: 0.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 12:15:17 +01:00
John McLear
73a9b88bb4
test(timeslider): port legacy mocha specs to Playwright, retire orphaned suite (#7949)
* test(timeslider): port legacy mocha specs to Playwright, retire orphaned suite

The legacy src/tests/frontend/specs/ mocha suite is run by no CI workflow,
so its timeslider coverage was dead — a regression in the in-pad history UI
(#7659/#7946) sailed through CI. Port the still-meaningful cases to
frontend-new Playwright specs, re-targeted at the real in-pad UI (outer
banner / slider / export links that pad_mode.ts drives) rather than the
isolated ?embed=1 iframe the existing specs use:

- timeslider_revision_labels.spec.ts (from timeslider_labels.js): the
  #history-banner shows 'Version N' + a valid (non-NaN) date and timer, and
  both update when scrubbing to revision 0.
- timeslider_export_links.spec.ts (from timeslider_numeric_padID.js and the
  'checks the export url' case of timeslider_revisions.js): the outer export
  hrefs target /p/<pad>/<rev>/export/<type> for the viewed revision, including
  a numeric pad id, and follow the slider to revision 0.
- timeslider_deeplink.spec.ts (from the 'jumps to a revision given in the url'
  case): a #rev/N hash — and the legacy #N shortlink form — boots straight
  into history mode at that revision.

Delete the three now-ported legacy specs. Verified passing on Chromium and
Firefox. The star-marker case of timeslider_revisions.js is already covered by
timeslider_saved_revisions.spec.ts (#7948).

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

* test(timeslider): make deep-link spec robust on Firefox

Assert the canonical #rev/0 URL and the slider landing on revision 0 rather
than the #history-banner-rev label text. The banner label is populated via a
MutationObserver bridge that races the iframe load on the bootstrap path in
Firefox (it is already covered for the normal button-entry flow by
timeslider_revision_labels.spec.ts); the URL + slider value are the
deterministic signals that the deep link entered history at the right revision.

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

* test(timeslider): address Qodo review on #7949

- Pin locale to en-US in the revision-labels spec so the localized 'Version N'
  label and 'Saved <Month> <day>, <year>' date assertions are deterministic and
  the date stays Date-parseable, instead of depending on the runner's locale.
- Use a high-entropy numeric pad id (timestamp + random) in the export-links
  spec so reruns against a persistent DB can't collide on the same pad.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:14:39 +01:00
John McLear
a65e3b75a4
fix(pad): show saved-revision markers in in-pad history mode (#7946) (#7948)
* fix(pad): show saved-revision markers in in-pad history mode (#7946)

When #7659 moved the timeslider into the pad as an embedded iframe, the
user-facing control became the outer #history-slider-input range input.
The saved-revision stars are still drawn by broadcast_slider.ts into the
iframe's #ui-slider-bar, but that DOM is hidden in embed mode, and
pad_mode.ts bridged rev/max/value/timer/authors to the outer slider but
never the saved revisions. Result: clicking "Save Revision" appeared to
work but no markers showed in the timeslider (regression in 3.3.x).

Bridge the embedded timeslider's clientVars.savedRevisions onto the outer
slider as percentage-positioned star markers, rendered on first sync and
re-rendered when the slider max changes. Markers are a purely visual,
aria-hidden overlay (keyboard/SR users already reach any revision via the
slider + step buttons) with a click-to-seek convenience for mouse users.

Adds a frontend-new Playwright spec exercising the real user flow (save a
revision in the pad, enter in-pad history mode, assert a visible marker on
the outer slider). The only prior coverage lived in the legacy mocha
suite (src/tests/frontend/specs/timeslider_revisions.js) which no CI
workflow runs, and the modern timeslider specs drive the ?embed=1 iframe
directly and never exercise the outer history UI — so this regression was
invisible to CI.

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

* fix(pad): live saved-revision markers + Qodo review fixes (#7946)

Address Qodo review on #7948:

- Live updates (bug 1): the server's SAVE_REVISION handler never broadcast
  NEW_SAVEDREV, so the client handler that adds a star was dead and no open
  timeslider ever updated live. Wire it up: Pad.addSavedRevision returns the
  new revision (undefined on duplicate); handleSaveRevisionMessage broadcasts
  NEW_SAVEDREV to the pad room. pad_mode.ts now sources outer markers from the
  embedded slider's live #ui-slider-bar .star DOM (labels from clientVars) and
  observes that bar so a revision saved by a collaborator appears on an
  already-open history slider. Live editors ignore the unknown message type.

- Single-revision pad (bug 2): allow max === 0 so a revision saved at rev 0
  still renders a marker instead of being cleared by the old max <= 0 guard.

- Test rigor (bug 3): assert the marker's inline left percentage directly
  instead of falling back to a layout coordinate, which let left:0% pass.

Adds a two-client Playwright test for the live path (verified it fails with
the server broadcast removed). Backend Pad + pad API specs (88) still pass.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 12:12:36 +01:00
translatewiki.net
8b6a0a2f3a
Localisation updates from https://translatewiki.net. 2026-06-11 14:04:05 +02:00
Etherpad Release Bot
4fe94c9b68 Merge branch 'master' into develop 2026-06-10 10:03:40 +00:00
92 changed files with 5311 additions and 2423 deletions

View file

@ -17,4 +17,11 @@ updates:
open-pull-requests-limit: 30
groups:
dev-dependencies:
dependency-type: "development"
dependency-type: "development"
cooldown:
default-days: 1 # fallback for anything not covered below
semver-major-days: 7
semver-minor-days: 3
semver-patch-days: 1
include:
- "*"

View file

@ -32,8 +32,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -51,7 +51,7 @@ jobs:
cache: pnpm
-
name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0
@ -88,8 +88,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -107,7 +107,7 @@ jobs:
cache: pnpm
-
name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0
@ -168,8 +168,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -234,8 +234,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -36,15 +36,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache vitepress build
with:
path: doc/.vitepress/cache

View file

@ -25,7 +25,7 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View file

@ -15,6 +15,24 @@ on:
- 'src/node/utils/run_cmd.ts'
- 'src/static/js/pluginfw/**'
- 'settings.json.template'
# Also build + smoke-test the package on PRs that touch the production
# footprint. The smoke step boots the packaged server and waits for /health,
# which is the only check that catches "server starts then exits before
# binding the port" startup regressions (e.g. a dependency bump whose change
# lets the event loop drain mid-boot). Previously this workflow ran only on
# push to develop, so such a regression was caught *after* merge — by which
# point develop was already red. Running it pre-merge blocks the PR instead.
# The release/apt-publish jobs are tag-guarded, so PRs run the build job only.
pull_request:
paths:
- 'packaging/**'
- '.github/workflows/deb-package.yml'
- 'src/package.json'
- 'pnpm-lock.yaml'
- 'src/node/server.ts'
- 'src/node/utils/run_cmd.ts'
- 'src/static/js/pluginfw/**'
- 'settings.json.template'
workflow_dispatch:
inputs:
ref:
@ -44,7 +62,7 @@ jobs:
runner: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.ref }}
@ -328,7 +346,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout etherpad source (for packaging/apt/key.asc)
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1

View file

@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: 'Dependency Review'
uses: actions/dependency-review-action@v5

View file

@ -26,7 +26,7 @@ jobs:
steps:
-
name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: etherpad
-
@ -42,7 +42,7 @@ jobs:
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -190,7 +190,7 @@ jobs:
steps:
-
name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: etherpad
-
@ -261,7 +261,7 @@ jobs:
steps:
-
name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: etherpad
-
@ -358,7 +358,7 @@ jobs:
steps:
-
name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: etherpad
-
@ -416,7 +416,7 @@ jobs:
enable-url-completion: true
- name: Check out ether-charts
if: github.ref == 'refs/heads/develop'
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
path: ether-charts
repository: ether/ether-charts

View file

@ -26,9 +26,9 @@ jobs:
APIKEY: downstream-smoke-key
steps:
- name: Checkout core (PR)
uses: actions/checkout@v6
uses: actions/checkout@v7
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -27,8 +27,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -48,7 +48,7 @@ jobs:
name: Install all dependencies and symlink for ep_etherpad-lite
run: pnpm i
- name: Cache Playwright browsers
uses: actions/cache@v5
uses: actions/cache@v6
id: playwright-cache
with:
path: ~/.cache/ms-playwright

View file

@ -20,15 +20,15 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache Playwright browsers
with:
path: ~/.cache/ms-playwright
@ -92,15 +92,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache Playwright browsers
with:
path: ~/.cache/ms-playwright
@ -168,15 +168,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache Playwright browsers
with:
path: ~/.cache/ms-playwright
@ -269,15 +269,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache Playwright browsers
with:
path: ~/.cache/ms-playwright

View file

@ -27,8 +27,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -23,7 +23,7 @@ jobs:
name: shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Run shellcheck on installer.sh
run: |
sudo apt-get update
@ -38,7 +38,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
@ -70,13 +70,30 @@ jobs:
- name: Smoke test - start Etherpad and curl /api
shell: bash
# Hard backstop: if teardown ever fails to reap the server the step
# fails in minutes instead of burning to GitHub's 6h job ceiling.
timeout-minutes: 8
env:
ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test
run: |
set -eu
# Enable job control so the backgrounded launcher gets its own
# process group, letting us reap the whole pnpm -> node tree below.
set -m
cd "$ETHERPAD_DIR"
pnpm run prod >/tmp/etherpad.log 2>&1 &
PID=$!
# `pnpm run prod` is a nested launcher (pnpm -> pnpm --filter -> node),
# so killing $PID alone orphans the node server, which keeps the step's
# output pipe open and hangs CI. Kill the entire process group, with a
# SIGKILL fallback in case SIGTERM is swallowed (e.g. a live flush timer
# keeping the event loop alive), so the step always exits cleanly.
reap() {
kill -TERM "-$PID" 2>/dev/null || true
sleep 5
kill -KILL "-$PID" 2>/dev/null || true
}
trap reap EXIT
# Wait up to 60s for the API to come up.
ok=0
for i in $(seq 1 60); do
@ -90,17 +107,14 @@ jobs:
if [ "$ok" != "1" ]; then
echo "Etherpad did not start within 60s. Last 200 lines of log:" >&2
tail -200 /tmp/etherpad.log >&2 || true
kill "$PID" 2>/dev/null || true
exit 1
fi
kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
installer-windows:
name: end-to-end install (windows-latest)
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
@ -131,6 +145,7 @@ jobs:
- name: Smoke test - start Etherpad and curl /api
shell: pwsh
timeout-minutes: 8
env:
ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test
run: |

View file

@ -24,8 +24,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -62,8 +62,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -125,8 +125,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -24,8 +24,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -27,8 +27,8 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v6
- uses: actions/cache@v5
uses: actions/checkout@v7
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: ether/etherpad-lite
path: etherpad
@ -42,12 +42,12 @@ jobs:
git checkout develop
git reset --hard origin/develop
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: ether/ether.github.com
path: ether.github.com
token: '${{ secrets.ETHER_RELEASE_TOKEN }}'
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -82,7 +82,7 @@ jobs:
with:
ruby-version: 2.7
- uses: reitzig/actions-asciidoctor@v2.0.4
- uses: reitzig/actions-asciidoctor@v2.0.5
with:
version: 2.0.18
- name: Prepare release

View file

@ -1,9 +1,33 @@
# PARKED — npm publish of the core package is not part of the standard release.
#
# This workflow renames `ep_etherpad-lite` -> `ep_etherpad` and publishes
# `./src` to npm. As of 2026-06, that publish serves no load-bearing purpose:
# - `ep_etherpad` has 0 dependents on npm and nothing in this repo depends on it;
# - plugins import `ep_etherpad-lite` resolved from the LOCAL core install,
# and plugin CI clones `ether/etherpad` rather than `npm install`-ing core;
# - Etherpad is run via git clone / Docker / zip / snap, never `npm install`.
# The publish has been failing (E404 PUT — the `ep_etherpad` package has no OIDC
# trusted publisher configured on npmjs.com), which is why npm is stuck at 2.5.0
# while 3.x shipped fine without it.
#
# It is therefore gated behind an explicit `confirm: true` dispatch input so a
# stray run fails fast with a clear message instead of a confusing 404. To
# actually publish, the npm owner of `ep_etherpad` (samtv12345) must first
# configure a trusted publisher: npmjs.com -> ep_etherpad -> Settings ->
# Trusted Publisher -> repo `ether/etherpad`, workflow `releaseEtherpad.yml`.
# Decision pending: finish that config, or remove this workflow. See AGENTS.MD.
name: releaseEtherpad.yaml
permissions:
contents: read
id-token: write # for npm OIDC trusted publishing
on:
workflow_dispatch:
inputs:
confirm:
description: 'PARKED — publish ep_etherpad to npm? Requires a trusted publisher configured on npmjs.com first (see workflow header). Set true only if that is done.'
required: true
default: false
type: boolean
env:
PNPM_HOME: ~/.pnpm-store
@ -12,8 +36,16 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Guard — refuse unless explicitly confirmed
if: ${{ inputs.confirm != true }}
run: |
echo "::error::releaseEtherpad is PARKED. The ep_etherpad npm publish is non-functional"
echo "::error::(no trusted publisher configured on npmjs.com; 0 dependents on npm)."
echo "::error::Re-run with confirm=true only after the owner configures a trusted"
echo "::error::publisher. See the workflow header / AGENTS.MD 'Releasing' section."
exit 1
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
# OIDC trusted publishing needs npm >= 11.5.1, which requires
@ -22,7 +54,7 @@ jobs:
registry-url: https://registry.npmjs.org/
- name: Upgrade npm to >=11.5.1 (required for trusted publishing)
run: npm install -g npm@latest
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}

View file

@ -34,7 +34,7 @@ jobs:
name: Wrapper unit tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Run snap/tests/run-all.sh
run: bash snap/tests/run-all.sh
@ -43,7 +43,7 @@ jobs:
needs: wrapper-tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Install snapcraft
run: sudo snap install --classic snapcraft

View file

@ -35,7 +35,7 @@ jobs:
snap-file: ${{ steps.build.outputs.snap }}
steps:
- name: Check out
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Build snap
id: build

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out etherpad-lite
uses: actions/checkout@v6
uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
name: Install pnpm

View file

@ -32,13 +32,13 @@ jobs:
steps:
-
name: Check out latest release
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
-
name: Check out latest release tag
run: git checkout "$(git tag --list 'v*' --sort=-version:refname | head -n1)"
- uses: actions/cache@v5
- uses: actions/cache@v6
name: Cache pnpm store
with:
path: ${{ env.PNPM_HOME }}
@ -55,7 +55,7 @@ jobs:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0

View file

@ -231,7 +231,7 @@ Releases are driven almost entirely by GitHub Actions. A maintainer dispatches *
- `handleRelease.yml` → builds Etherpad, extracts the matching changelog section via `generateChangelog` (`bin/generateReleaseNotes.ts`), and publishes the **GitHub Release** (`make_latest: true`);
- `docker.yml` → builds & pushes the Docker images;
- `snap-publish.yml` → publishes the snap.
5. **npm publish is a separate manual step:** dispatch **"releaseEtherpad.yaml"** (`workflow_dispatch`), which runs `npm publish --provenance --access public` via npm **OIDC trusted publishing**. It is *not* fired by the tag.
5. **npm publish — PARKED, not part of the release.** `releaseEtherpad.yaml` publishes the core as `ep_etherpad` to npm, but that package is **not load-bearing**: it has 0 dependents, nothing depends on it (plugins import `ep_etherpad-lite` from the *local* core install; plugin CI clones the repo), and Etherpad is run via clone/Docker/zip/snap — never `npm install`. The publish currently fails with `E404` because `ep_etherpad` has no OIDC trusted publisher configured on npmjs.com, which is why npm sits at 2.5.0 while 3.x shipped fine without it. The workflow is gated behind a `confirm: true` input so it can't run by accident. **Skip it for a normal release.** To revive it, the npm owner of `ep_etherpad` (`samtv12345`) configures a trusted publisher (npmjs.com → ep_etherpad → Settings → Trusted Publisher → repo `ether/etherpad`, workflow `releaseEtherpad.yml`); otherwise the workflow can be removed. Decision pending.
### Documentation

View file

@ -1,3 +1,39 @@
# 3.3.2
3.3.2 is a bug-fix and dependency-hardening follow-up to 3.3.1. It rounds out the pad-deletion UX rework (suppressing the recovery token for durable identities, keeping the token-less Delete button reachable, and closing a read-only deletion hole), restores the saved-revision markers that went missing from in-pad history mode in 3.3.x, and adds env-var overrides so air-gapped installs can switch off Etherpad's outbound calls without editing the image. It also fixes the `migrateDB` / `importSqlFile` / `migrateDirtyDBtoRealDB` CLI scripts against the promise-based ueberdb2 API, rejects unreachable `.`/`..` pad ids, and clears a batch of dependency security advisories (including CVE-2026-54285). On the CI side it unblocks the installer smoke test (which had been hanging the full 6-hour job ceiling since 3.2.0) and pins ueberdb2 past a startup-exit regression in the packaged boot.
### Security
- **Force `@opentelemetry/core` ≥ 2.8.0 (GHSA-8988-4f7v-96qf / CVE-2026-54285, #7975).** The transitive dep (pulled in via `@elastic/elasticsearch``@elastic/transport`) had a `W3CBaggagePropagator.extract()` that did not enforce W3C size limits on inbound baggage headers, allowing unbounded memory allocation. Pinned via a `pnpm-workspace.yaml` override; satisfies the existing `2.x` range with no parent bump.
- **Resolve open Dependabot security alerts (#7967).** Refreshes stale override floors and adds new ones via `pnpm-workspace` overrides: `form-data` ≥ 4.0.6, `ws` ≥ 8.21.0, `esbuild` ≥ 0.28.1, `basic-ftp` ≥ 5.3.1 (capped `<6.0.0` to avoid a surprise major on the plugin-install path), `tar` ≥ 7.5.16, `js-yaml` ≥ 4.2.0, `qs` ≥ 6.15.2, `ip-address` ≥ 10.1.1, and `@babel/core` ≥ 7.29.6.
- **Reject read-only deletion via token-less paths (part of #7959 / #7960).** Under `allowPadDeletionByAllUsers` a read-only viewer was granted `canDeletePad=true`, and the server's `flagOk`/`creatorOk` branches never checked `session.readonly` — so a read-only link holder could delete a pad without a token. Read-only sessions are now excluded from both the client var and the server's token-less authorization paths; a valid recovery token stays sufficient regardless of session mode.
### Notable enhancements
- **Pad deletion — suppress the recovery token for durable identities and relabel the action (#7926 / #7930).** Building on the `allowPadDeletionByAllUsers` suppression, a creator's deletion token is now also withheld when they have a *durable* identity — authenticated (`req.session.user` with a username) **and** the deployment pins that identity to a stable `authorID` via a `getAuthorId` hook — since only then does the creator survive a cookie clear or a different device, making the token redundant. This tightens the previous "require authentication ⇒ always suppress" rule: without `getAuthorId` the authorID still comes from the per-browser cookie, so an authenticated user on a second device is *not* the creator and keeps getting a token. A new `canDeleteWithoutToken` client var hides the whole recovery-token disclosure (label, field, submit) when no token is needed, and the recovery form now renders for all sessions (hidden by default) so an authenticated creator without a durable mapping still has UI to enter their token. `API.createPad` returns a `null` `deletionToken` under `allowPadDeletionByAllUsers`, matching the socket/UI path.
- **Offline/air-gapped installs — env-var overrides for the update check, plugin catalog, and updater (#7917, addresses #7911).** Firewalled deployments could not disable Etherpad's outbound calls without editing `settings.json` inside the image. The relevant keys are now wired through the `${ENV:default}` substitution in `settings.json.docker` and `settings.json.template`: `PRIVACY_UPDATE_CHECK`, `PRIVACY_PLUGIN_CATALOG`, `UPDATES_TIER` (`off` = no calls), `UPDATE_SERVER`, plus the docker-only `UPDATES_SOURCE` / `UPDATES_CHANNEL` / `UPDATES_CHECK_INTERVAL_HOURS` / `UPDATES_GITHUB_REPO` / `UPDATES_REQUIRE_ADMIN_FOR_STATUS`. A new "Updates & privacy" section in `doc/docker.md` documents the set; backend tests parse the shipped configs and fail if the `${ENV}` placeholders are dropped. Config, docs, and tests only — no runtime code change.
### Notable fixes
- **Pad — keep the token-less Delete button reachable without pad-wide settings (#7959 / #7960).** The token-less `#delete-pad` button was nested inside the `enablePadWideSettings`-gated section, so disabling pad-wide settings removed the only no-token deletion path — and combined with #7926 hiding the token disclosure when no token is needed, a user allowed to delete could be left with no deletion UI at all. The button is now always rendered (hidden by default) and driven by a `canDeletePad` client var (creator or `allowPadDeletionByAllUsers`, excluding read-only sessions), so the plain button and the recovery-token disclosure are mutually coherent and neither depends on pad-wide settings.
- **History mode — restore the saved-revision markers (#7946 / #7948).** When #7659 moved the timeslider into the pad as an embedded iframe, the user-facing control became the outer `#history-slider-input`, but the saved-revision stars were still drawn into the now-hidden iframe `#ui-slider-bar`, so "Save Revision" appeared to do nothing in in-pad history mode (a 3.3.x regression). `pad_mode.ts` now bridges the embedded slider's saved revisions onto the outer slider as percentage-positioned, aria-hidden star markers (with click-to-seek for mouse users), and the server's `SAVE_REVISION` handler broadcasts `NEW_SAVEDREV` to the pad room so a revision saved by a collaborator appears live on an already-open history slider. A single revision saved at rev 0 now renders too. Adds Playwright coverage for both the single-client and two-client live paths.
- **Import dialog — correct the outdated "no converter" help message (#7988 / #7989).** The notice claimed only plain text and HTML could be imported and linked to the legacy AbiWord wiki, prompting LibreOffice installs for formats that already work natively. Etherpad imports `.txt`, `.html`, `.docx` (via mammoth) and `.etherpad` without LibreOffice; only `.pdf`/`.odt`/`.doc`/`.rtf` still need it. The message now says so and points at the documentation site.
- **PadManager — reject unreachable `.` and `..` pad ids (#7962).** `isValidPadId` accepted ids consisting only of URL dot-segments, but per the WHATWG URL standard a browser normalises `/p/.` to `/p/` and `/p/..` to `/`, so such a pad could be created in the database yet never opened or exported. These ids are now rejected, and the admin `deletePad` handler falls back to a raw key purge when `getPad()` throws so any legacy `.`/`..` pad can still be removed.
### Internal / contributor-facing
- **CLI — fix the database migration/import scripts against the ueberdb2 promise API (#7982 / #7983).** `migrateDB.ts` opened source and target databases, copied all keys, then resolved without closing either — so under ueberdb2 6.1.x the keep-alive timer kept the process hanging after "Done syncing dbs", and buffered target writes were only guaranteed flushed on `close()`. It now closes both databases (flushing writes, clearing the timer) on success and error paths and exits with an explicit status. `importSqlFile.ts` and `migrateDirtyDBtoRealDB.ts` were ported off the pre-v6 callback API to `await db.init()` / `db.set(k, v)` / `db.close()`, removing two `@ts-ignore`s that hid broken calls and fixing an undefined `length` in a progress log; `tsc --noEmit` on the bin package is now clean.
- **CI — stop the installer smoke test hanging the 6-hour job ceiling (#7981).** The "Installer test" had hung on every ubuntu/macOS run since 3.2.0: `pnpm run prod` is a nested launcher, so `kill "$PID"; wait "$PID"` only signalled the outer pnpm and blocked forever if the node server didn't exit on SIGTERM. Teardown now runs the launcher in its own process group, kills the whole group (SIGTERM then SIGKILL), drops the blocking `wait`, and adds an 8-minute `timeout-minutes` backstop to both smoke steps.
- **CI — run the Debian-package smoke test on PRs (#7969).** The packaged-boot smoke test previously ran only on push to `develop` — i.e. after merge — which is why the ueberdb2 startup-exit regression turned `develop` red instead of being blocked at PR time. A `pull_request` trigger (scoped to production-footprint paths) now runs the build+smoke job on PRs; the release/apt-publish jobs stay tag-guarded.
- **Release — park the non-functional `ep_etherpad` npm publish (#7922).** The `releaseEtherpad` workflow republished `./src` as `ep_etherpad`, a package with zero dependents that nothing in the repo or any deployment path consumes, and it had been failing with E404 (no OIDC trusted publisher configured). The job is now gated behind an explicit `confirm: true` dispatch input so a stray run fails fast with a clear message, with the status documented in the workflow header and `AGENTS.MD`.
- **Tests — port the orphaned legacy timeslider specs to Playwright (#7949).** The `src/tests/frontend/specs/` mocha suite is run by no CI workflow, so its timeslider coverage was dead — which is how the #7946 history-mode regression reached a release. The still-meaningful cases (revision labels, export links, deep-link entry) were ported to `frontend-new` Playwright specs re-targeted at the real in-pad UI, and the three now-ported legacy specs were deleted.
### Dependencies
- `ueberdb2` pinned to `6.1.13`. 6.1.10 rewrote the cache/buffer layer to lazily arm an `.unref()`'d flush timer only when there are dirty keys, so on a fresh empty dirty DB nothing anchored Node's event loop and the packaged (.deb/systemd) boot could exit cleanly (code 0) before `server.listen()` bound the port — failing the Debian-package health check. The dep was pinned back to the last green release (6.1.9, #7969) and then rolled forward to the now-fixed `6.1.13` (#7979), pinned exactly rather than with a caret.
- `nodemailer` 8.x → 9.0.1 (#7965 / #7950 / #7976), `mongodb` 7.1.1 → 7.3.0 (#7941), `pg` 8.21.0 → 8.22.0 (#7985), `undici` → 8.5.0 (#7980 etc.), `oidc-provider` 9.8.4 → 9.8.5 (#7973), `pdfkit` 0.19.0 → 0.19.1 (#7945), `semver` 7.8.3 → 7.8.4 (#7943), and `@radix-ui/react-switch` 1.3.0 → 1.3.1 (#7974).
- Dev/build dependency group updates (#7964, #7970, #7978, #7987, #7944, #7951, #7952, and others), including `@types/node` 25 → 26, `esbuild` 0.28.0 → 0.28.1, `eslint` 10.4.1 → 10.5.0, `@playwright/test` 1.60 → 1.61, `vitest` 4.1.8 → 4.1.9, and `actions/checkout` 6 → 7 (#7977).
# 3.3.1
3.3.1 is a small bug-fix and hardening follow-up to 3.3.0. It closes a stored-XSS vector in the numbered-list `start` attribute, hardens the database layer so a dropped connection to PostgreSQL / Redis / RethinkDB no longer crashes the process (via ueberdb2 6.1.9), and fixes a handful of pad and admin regressions — the iOS dark-mode status bar, the settings language dropdown, the pad-deletion modal under `allowPadDeletionByAllUsers`, and a single unreadable pad blanking the admin Manage-pads list.

View file

@ -1,7 +1,7 @@
{
"name": "admin",
"private": true,
"version": "3.3.1",
"version": "3.3.2",
"type": "module",
"scripts": {
"dev": "pnpm gen:api && vite",
@ -14,39 +14,39 @@
"test": "pnpm gen:api && tsx --test 'src/**/__tests__/*.test.ts' 'src/**/__tests__/*.test.tsx'"
},
"dependencies": {
"@radix-ui/react-switch": "^1.3.0",
"@tanstack/react-query": "^5.101.0",
"@tanstack/react-query-devtools": "^5.101.0",
"@radix-ui/react-switch": "^1.3.3",
"@tanstack/react-query": "^5.101.2",
"@tanstack/react-query-devtools": "^5.101.2",
"jsonc-parser": "^3.3.1",
"openapi-fetch": "^0.17.0",
"openapi-react-query": "^0.5.4"
},
"devDependencies": {
"@radix-ui/react-dialog": "^1.1.16",
"@radix-ui/react-toast": "^1.2.16",
"@radix-ui/react-visually-hidden": "^1.2.5",
"@radix-ui/react-dialog": "^1.1.19",
"@radix-ui/react-toast": "^1.2.19",
"@radix-ui/react-visually-hidden": "^1.2.7",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"@vitejs/plugin-react": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.63.0",
"@vitejs/plugin-react": "^6.0.3",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"eslint": "^10.4.1",
"eslint": "^10.6.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"i18next": "^26.3.1",
"eslint-plugin-react-refresh": "^0.5.3",
"i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.17.0",
"lucide-react": "^1.23.0",
"openapi-typescript": "^7.13.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-hook-form": "^7.78.0",
"react-i18next": "^17.0.8",
"react-router-dom": "^7.17.0",
"react-hook-form": "^7.81.0",
"react-i18next": "^17.0.9",
"react-router-dom": "^7.18.1",
"socket.io-client": "^4.8.3",
"tsx": "^4.22.4",
"tsx": "^4.23.0",
"typescript": "^6.0.3",
"vite": "^8.0.16",
"vite": "^8.1.4",
"vite-plugin-babel": "^1.7.3",
"zustand": "^5.0.14"
}

View file

@ -2,7 +2,6 @@
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
import util from "node:util";
import fs from 'node:fs';
import log4js from 'log4js';
import readline from 'readline';
@ -69,8 +68,7 @@ const unescape = (val: string) => {
if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE');
log('initializing db');
const initDb = await util.promisify(db.init.bind(db));
await initDb(null);
await db.init();
log('done');
log(`Opening ${sqlFile}...`);
@ -86,8 +84,7 @@ const unescape = (val: string) => {
value = value.substring(0, value.length - 2);
console.log(`key: ${key} val: ${value}`);
console.log(`unval: ${unescape(value)}`);
// @ts-ignore
db.set(key, unescape(value), null);
await db.set(key, unescape(value));
keyNo++;
if (keyNo % 1000 === 0) log(` ${keyNo}`);
}
@ -96,9 +93,7 @@ const unescape = (val: string) => {
process.stdout.write('done. waiting for db to finish transaction. ' +
'depended on dbms this may take some time..\n');
const closeDB = util.promisify(db.close.bind(db));
// @ts-ignore
await closeDB(null);
await db.close();
log(`finished, imported ${keyNo} keys.`);
process.exit(0)
})();

View file

@ -74,10 +74,20 @@ const handleSync = async ()=>{
}
}
handleSync().then(()=>{
handleSync().then(async ()=>{
// Closing flushes any buffered writes to the target DB and clears
// ueberdb2's keep-alive timer (added in 6.1.x). Without this the migrated
// data may not be fully persisted and the process would hang forever
// instead of exiting once the sync is done.
await ueberdb2.close()
await ueberdb1.close()
console.log("Done syncing dbs")
}).catch(e=>{
process.exit(0)
}).catch(async e=>{
console.log(`Error syncing db ${e}`)
await ueberdb2.close().catch(()=>{})
await ueberdb1.close().catch(()=>{})
process.exit(1)
})

View file

@ -35,26 +35,16 @@ process.on('unhandledRejection', (err) => { throw err; });
const keys = await dirty.findKeys('*', '')
console.log(`Found ${keys.length} records, processing now.`);
const p: Promise<void>[] = [];
let numWritten = 0;
for (const key of keys) {
let value = await dirty.get(key);
let bcb, wcb;
p.push(new Promise((resolve, reject) => {
bcb = (err:any) => { if (err != null) return reject(err); };
wcb = (err:any) => {
if (err != null) return reject(err);
if (++numWritten % 100 === 0) console.log(`Wrote record ${numWritten} of ${length}`);
resolve();
};
}));
db.set(key, value, bcb, wcb);
const value = await dirty.get(key);
await db.set(key, value);
if (++numWritten % 100 === 0) console.log(`Wrote record ${numWritten} of ${keys.length}`);
}
await Promise.all(p);
console.log(`Wrote all ${numWritten} records`);
await db.close(null);
await dirty.close(null);
await db.close();
await dirty.close();
console.log('Finished.');
process.exit(0)
})();

View file

@ -1,6 +1,6 @@
{
"name": "bin",
"version": "3.3.1",
"version": "3.3.2",
"description": "",
"main": "checkAllPads.js",
"directories": {
@ -9,12 +9,12 @@
"dependencies": {
"ep_etherpad-lite": "workspace:../src",
"log4js": "^6.9.1",
"semver": "^7.8.3",
"tsx": "^4.22.4",
"ueberdb2": "^6.1.9"
"semver": "^7.8.5",
"tsx": "^4.23.0",
"ueberdb2": "6.1.16"
},
"devDependencies": {
"@types/node": "^25.9.2",
"@types/node": "^26.1.1",
"@types/semver": "^7.7.1",
"typescript": "^6.0.3"
},

View file

@ -27,13 +27,18 @@ export default defineConfig({
items: [
{ text: 'Docker', link: '/docker.md' },
{ text: 'Configuration', link: '/configuration.md' },
{ text: 'Deployment', link: '/deployment.md' },
{ text: 'Database', link: '/database.md' },
{ text: 'Localization', link: '/localization.md' },
{ text: 'Cookies', link: '/cookies.md' },
{ text: 'Plugins', link: '/plugins.md' },
{ text: 'Stats', link: '/stats.md' },
{text: 'Skins', link: '/skins.md' },
{ text: 'Accessibility', link: '/accessibility.md' },
{text: 'Demo', link: '/demo.md' },
{text: 'CLI', link: '/cli.md'},
{ text: 'Development', link: '/development.md' },
{ text: 'FAQ', link: '/faq.md' },
]
},
{

91
doc/accessibility.md Normal file
View file

@ -0,0 +1,91 @@
# Accessibility
Etherpad aims to be usable by everyone, including people who rely on a
keyboard, a screen reader, or other assistive technology. The editor follows
common conventions so that selecting, formatting, and navigating text works the
way you would expect in other applications, and the toolbar can be reached and
operated without a mouse.
If you find a feature that is not accessible, please let us know by opening an
issue so it can be improved.
## Keyboard shortcuts
The following shortcuts are built into the editor. On macOS use the Command
(`Cmd`) key wherever `Ctrl` is listed.
::: tip
Most shortcuts can be individually enabled or disabled through the
`padShortcutEnabled` settings, so a deployment may have customised which of
these are active.
:::
### Editor
| Action | Shortcut |
| --- | --- |
| Bold | `Ctrl` + `B` |
| Italic | `Ctrl` + `I` |
| Underline | `Ctrl` + `U` |
| Strikethrough | `Ctrl` + `5` |
| Ordered (numbered) list | `Ctrl` + `Shift` + `N` or `Ctrl` + `Shift` + `1` |
| Unordered (bulleted) list | `Ctrl` + `Shift` + `L` |
| Indent line or selection | `Tab` |
| Outdent line or selection | `Shift` + `Tab` |
| Undo | `Ctrl` + `Z` |
| Redo | `Ctrl` + `Y` or `Ctrl` + `Shift` + `Z` |
| Save a named revision | `Ctrl` + `S` |
| Duplicate the current line(s) | `Ctrl` + `Shift` + `D` |
| Delete the current line(s) | `Ctrl` + `Shift` + `K` |
| Clear authorship colors on the pad or selection | `Ctrl` + `Shift` + `C` |
| Show the authors of the current line | `Ctrl` + `Shift` + `2` |
| Focus the toolbar (see below) | `Alt` + `F9` |
| Focus the chat input | `Alt` + `C` |
Text selection, cut (`Ctrl` + `X`), copy (`Ctrl` + `C`), paste
(`Ctrl` + `V`), and the arrow keys behave as they do in any standard text
editor.
### Timeslider
The timeslider (revision history) provides its own shortcuts:
| Action | Shortcut |
| --- | --- |
| Play / pause history playback | `Space` |
| Step back one revision | `Left Arrow` |
| Step forward one revision | `Right Arrow` |
| Jump back to the previous starred revision | `Shift` + `Left Arrow` |
| Jump forward to the next starred revision | `Shift` + `Right Arrow` |
## Toolbar navigation
The toolbar holds the formatting controls (bold, italic, lists, and so on) and
can be reached and operated entirely from the keyboard:
* Press `Alt` + `F9` from the editor to move focus to the first button in the
toolbar.
* Use the `Left Arrow` and `Right Arrow` keys to move between buttons. `Tab`
also moves to the next focusable control.
* Press `Enter` to activate the focused button.
* Press `Alt` + `F9` again, or `Escape`, to return focus to the pad.
Pressing `Escape` while a toolbar dropdown (such as the settings or color
picker) is open closes that dropdown first.
## Screen readers
Etherpad provides as much screen reader support as possible. Support quality
varies between platforms and browsers, so the following combinations are
recommended:
* On Windows, Firefox with [NVDA](https://www.nvaccess.org/) currently gives the
best experience.
To reduce verbose feedback while typing collaboratively in NVDA, open the
keyboard settings (`NVDA` + `Ctrl` + `K`) and turn off **Speak typed characters**
and **Speak typed words**.
Support in other screen readers and browsers (for example Orca on Linux, or
Chrome) is more limited. Contributions to improve coverage on these platforms
are very welcome.

View file

@ -102,7 +102,9 @@ The notice auto-fades after 8 seconds and can be dismissed immediately. The publ
## Disabling everything
Set `updates.tier` to `"off"`. No HTTP request will leave the instance and no banner or badge will render.
Set `updates.tier` to `"off"`. The self-updater goes silent — no request to the GitHub Releases API leaves the instance and no banner or badge renders. Note this does **not** cover the separate legacy version check in `UpdateCheck.ts`, which still fetches `${updateServer}/info.json` until you also set `privacy.updateCheck` to `false` (see [PRIVACY.md](https://github.com/ether/etherpad/blob/develop/PRIVACY.md)).
On Docker / air-gapped installs you can do both without editing `settings.json` inside the image by setting `UPDATES_TIER=off` **and** `PRIVACY_UPDATE_CHECK=false` (add `PRIVACY_PLUGIN_CATALOG=false` to also disable the admin plugin browser's catalogue fetch). See the [Updates & privacy](../docker.md#updates--privacy-offline--air-gapped) table in the Docker docs for the full set of environment variables.
## Privacy

View file

@ -1,7 +1,7 @@
# Changeset Library
The [changeset
library](https://github.com/ether/etherpad-lite/blob/develop/src/static/js/Changeset.ts)
library](https://github.com/ether/etherpad/blob/develop/src/static/js/Changeset.ts)
provides tools to create, read, and apply changesets.
## Changeset
@ -21,6 +21,42 @@ A transmitted changeset looks like this:
'Z:z>1|2=m=b*0|1+1$\n'
```
### Reading a changeset
`unpack()` splits a changeset string into its parts:
```javascript
const unpacked = Changeset.unpack('Z:z>1|2=m=b*0|1+1$\n');
// { oldLen: 35, newLen: 36, ops: '|2=m=b*0|1+1', charBank: '\n' }
```
`oldLen` is the document length before the change and `newLen` the length after.
`ops` is the list of operations, and `charBank` holds the characters inserted by
those operations.
Iterate the operations with `deserializeOps()`, which yields one `Op` at a time:
```javascript
for (const op of Changeset.deserializeOps(unpacked.ops)) {
console.log(op);
}
// Op { opcode: '=', chars: 22, lines: 2, attribs: '' }
// Op { opcode: '=', chars: 11, lines: 0, attribs: '' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '*0' }
```
There are three kinds of operation, each applied starting from the current
position in the text:
- `=` keeps text (it may still change the text's attributes, e.g. make it bold).
- `-` removes text.
- `+` inserts text (taking the characters from the changeset's `charBank`).
`opcode` is the operation type; `chars` and `lines` are how much text it covers;
and `attribs` are the attributes applied, written as `*` references into the
pad's attribute pool. In the example above the final op inserts one character
(the newline from `charBank`) carrying attribute `*0`.
## Attribute Pool
```javascript
@ -36,6 +72,59 @@ are used many times.
There is one attribute pool per pad, and it includes every current and
historical attribute used in the pad.
A pool can be serialized to and from a plain object with `toJsonable()` and
`fromJsonable()`:
```javascript
const pool = new AttributePool();
pool.fromJsonable({
numToAttrib: {
0: ['author', 'a.kVnWeomPADAT2pn9'],
1: ['bold', 'true'],
2: ['italic', 'true'],
},
nextNum: 3,
});
pool.getAttrib(1); // [ 'bold', 'true' ]
pool.getAttribKey(1); // 'bold'
pool.getAttribValue(1); // 'true'
```
Each attribute is a `[key, value]` pair — `['bold', 'true']`, or
`['author', '<authorId>']`. A character can carry several attributes (bold *and*
italic), but only one value per key (so it cannot belong to two authors).
## Attributed text (atext)
A pad's content is stored as *attributed text* (`atext`): the plain text plus an
attribute string describing which attributes apply to each span.
```javascript
const atext = {
text: 'bold text\nitalic text\nnormal text\n\n',
attribs: '*0*1+9*0|1+1*0*1*2+b|1+1*0+b|2+2',
};
```
The attribute string is a sequence of `+` operations — the same encoding used by
changesets — which you can read with `deserializeOps()`:
```javascript
for (const op of Changeset.deserializeOps(atext.attribs)) {
console.log(op);
}
// Op { opcode: '+', chars: 9, lines: 0, attribs: '*0*1' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '*0' }
// Op { opcode: '+', chars: 11, lines: 0, attribs: '*0*1*2' }
// Op { opcode: '+', chars: 1, lines: 1, attribs: '' }
// Op { opcode: '+', chars: 11, lines: 0, attribs: '*0' }
// Op { opcode: '+', chars: 2, lines: 2, attribs: '' }
```
Read against the pool above, the first nine characters (`bold text`) carry
attributes `*0*1` (author + bold), the following newline carries `*0`, and so on.
## Further Reading
Detailed information about the changesets & Easysync protocol:

204
doc/database.md Normal file
View file

@ -0,0 +1,204 @@
# Database structure
## Keys and their values
### groups
A list of all existing groups (a JSON object with groupIDs as keys and `1` as values).
### pad:$PADID
Contains all information about pads
- **atext** - the latest attributed text
- **pool** - the attribute pool
- **head** - the number of the latest revision
- **chatHead** - the number of the latest chat entry
- **public** - flag that disables security for this pad
- **passwordHash** - string that contains a salted sha512 sum of this pad's password
### pad:$PADID:revs:$REVNUM
Saves a revision $REVNUM of pad $PADID
- **meta**
- **author** - the autorID of this revision
- **timestamp** - the timestamp of when this revision was created
- **changeset** - the changeset of this revision
### pad:$PADID:chat:$CHATNUM
Saves a chat entry with num $CHATNUM of pad $PADID
- **text** - the text of this chat entry
- **userId** - the authorID of this chat entry
- **time** - the timestamp of this chat entry
### pad2readonly:$PADID
Translates a padID to a readonlyID
### readonly2pad:$READONLYID
Translates a readonlyID to a padID
### token2author:$TOKENID
Translates a token to an authorID
### globalAuthor:$AUTHORID
Information about an author
- **name** - the name of this author as shown in the pad
- **colorID** - the colorID of this author as shown in the pad
### mapper2group:$MAPPER
Maps an external application identifier to an internal group
### mapper2author:$MAPPER
Maps an external application identifier to an internal author
### group:$GROUPID
a group of pads
- **pads** - object with pad names in it, values are 1
### session:$SESSIONID
a session between an author and a group
- **groupID** - the groupID the session belongs too
- **authorID** - the authorID the session belongs too
- **validUntil** - the timestamp until this session is valid
### author2sessions:$AUTHORID
saves the sessions of an author
- **sessionsIDs** - object with sessionIDs in it, values are 1
### group2sessions:$GROUPID
- **sessionsIDs** - object with sessionIDs in it, values are 1
# Connecting to a database backend
Etherpad stores everything in a single key/value table through
[ueberDB](https://www.npmjs.com/package/ueberdb2), so the same data model works
across many backends. The backend is selected with `dbType` in `settings.json`,
and backend-specific connection options go in `dbSettings`.
The default `dirty` backend writes to a local file (`var/dirty.db`) and needs no
setup, which is convenient for development but not recommended for production.
For a production instance, point Etherpad at a real database such as MySQL/MariaDB,
PostgreSQL or Redis. Etherpad creates its own table on first run; you only need
to provision an empty database and a user with access to it.
## MySQL / MariaDB
Create the database and a user, then grant access:
```sql
CREATE DATABASE `etherpad` CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
CREATE USER 'etherpad'@'localhost' IDENTIFIED BY 'a-secure-password';
GRANT CREATE,ALTER,SELECT,INSERT,UPDATE,DELETE ON `etherpad`.* TO 'etherpad'@'localhost';
```
Then configure `settings.json`:
```json
"dbType": "mysql",
"dbSettings": {
"user": "etherpad",
"host": "localhost",
"port": 3306,
"password": "a-secure-password",
"database": "etherpad",
"charset": "utf8mb4"
}
```
Setting `charset` to `utf8mb4` is strongly recommended so that the full range of
Unicode (including emoji) is stored correctly. To connect over a local socket
instead of TCP, replace `host`/`port` with `"socketPath": "/var/run/mysqld/mysqld.sock"`.
## PostgreSQL
Create the user and a database owned by it:
```sql
CREATE USER etherpad WITH PASSWORD 'a-secure-password';
CREATE DATABASE etherpad OWNER etherpad;
```
Then configure `settings.json`:
```json
"dbType": "postgres",
"dbSettings": {
"user": "etherpad",
"host": "localhost",
"port": 5432,
"password": "a-secure-password",
"database": "etherpad"
}
```
The `dbSettings` object is passed straight to the `node-postgres` connection
pool, so any option it accepts (including a single `"connectionString"`) works.
On Debian/Ubuntu you can use peer authentication over the local socket by
setting `"host": "/var/run/postgresql"` and an empty password, provided the
operating-system user that runs Etherpad matches the PostgreSQL role.
## Redis
Install Redis and make sure it persists data to disk. Configure `settings.json`
with either discrete fields or a single connection URL:
```json
"dbType": "redis",
"dbSettings": {
"host": "localhost",
"port": 6379,
"password": "a-secure-redis-password"
}
```
```json
"dbType": "redis",
"dbSettings": {
"url": "redis://:a-secure-redis-password@localhost:6379"
}
```
## Migrating from MySQL to PostgreSQL
[pgloader](https://pgloader.io/) can copy an existing Etherpad database from
MySQL to PostgreSQL. Stop Etherpad first so the source database is quiescent.
```bash
sudo apt-get install postgresql pgloader
# Create the target role and database
sudo -u postgres createuser etherpad
sudo -u postgres createdb -O etherpad etherpad
# Describe and run the migration
cat > pgloader.load <<'EOF'
LOAD DATABASE
FROM mysql://etherpad:MYSQL_PASSWORD@127.0.0.1/etherpad
INTO postgresql:///etherpad
WITH preserve index names, prefetch rows = 100
ALTER SCHEMA 'etherpad' RENAME TO 'public';
EOF
pgloader --verbose pgloader.load
```
Afterwards set the PostgreSQL user's password and make sure it can read and
write the migrated table:
```sql
ALTER USER etherpad WITH PASSWORD 'a-secure-password';
GRANT pg_read_all_data TO etherpad;
GRANT pg_write_all_data TO etherpad;
```
Then point `settings.json` at PostgreSQL as shown above and start Etherpad.
::: tip
To move data between *any* two backends supported by ueberDB, you can also
use the `migrateDB` CLI tool, which reads every record from a source database
descriptor and writes it to a target one. See the [CLI chapter](./cli.md).
:::

359
doc/deployment.md Normal file
View file

@ -0,0 +1,359 @@
# Deployment
This page collects working configurations for deploying Etherpad in production:
running it behind a reverse proxy, hosting it under a subdirectory, terminating
HTTPS natively, running it as a system service, and deploying it on Kubernetes.
Etherpad listens on port `9001` by default. Throughout this page the upstream
Etherpad server is assumed to be reachable at `http://127.0.0.1:9001`.
## Running behind a reverse proxy
The recommended production setup is to run Etherpad on `127.0.0.1:9001` and put a
reverse proxy in front of it to terminate TLS, serve a virtual host, and forward
requests.
Etherpad uses WebSockets (via socket.io). The load-bearing part of every proxy
config below is the WebSocket upgrade: the proxy **must** forward the `Upgrade`
and `Connection` headers, or real-time editing will silently fail back to slow
long-polling (or break entirely).
When Etherpad runs behind a proxy you should also set `trustProxy: true` in your
settings so that Etherpad honours the `X-Forwarded-*` headers (correct client IP,
secure-cookie flag, etc.). See the `trustProxy` section in the [Configuration documentation](./configuration.md) for the full details of which headers are trusted.
### Nginx
```nginx
# Map the Upgrade header so WebSockets work. Place this in the http context.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name pad.example.com;
ssl_certificate /etc/nginx/ssl/etherpad.crt;
ssl_certificate_key /etc/nginx/ssl/etherpad.key;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_buffering off;
proxy_set_header Host $host;
proxy_pass_header Server;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
# Redirect plain HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name pad.example.com;
return 301 https://$host$request_uri;
}
```
### Apache
Enable `mod_proxy`, `mod_proxy_http`, `mod_proxy_wstunnel` and `mod_headers`.
The `mod_proxy_wstunnel` `upgrade=websocket` syntax requires Apache 2.4.47 or
newer.
```apache
<VirtualHost *:443>
ServerName pad.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/etherpad/etherpad.crt
SSLCertificateKeyFile /etc/ssl/etherpad/etherpad.key
ProxyVia On
ProxyRequests Off
ProxyPreserveHost On
# WebSocket traffic (socket.io) must be matched first.
<Location "/socket.io">
ProxyPass "ws://127.0.0.1:9001/socket.io" upgrade=websocket timeout=30
ProxyPassReverse "ws://127.0.0.1:9001/socket.io"
</Location>
<Location "/">
ProxyPass "http://127.0.0.1:9001/" retry=0 timeout=30
ProxyPassReverse "http://127.0.0.1:9001/"
</Location>
</VirtualHost>
```
### Caddy
Caddy v2 proxies WebSocket connections automatically and obtains/renews a
certificate for you, so the configuration is minimal:
```caddy
pad.example.com {
reverse_proxy 127.0.0.1:9001
}
```
### Traefik
Traefik v2 also proxies WebSockets transparently. For a Docker deployment, attach
these labels to the Etherpad container:
```yaml
labels:
- "traefik.enable=true"
- "traefik.http.routers.etherpad.rule=Host(`pad.example.com`)"
- "traefik.http.routers.etherpad.entrypoints=websecure"
- "traefik.http.routers.etherpad.tls.certresolver=myresolver"
- "traefik.http.services.etherpad.loadbalancer.server.port=9001"
- "traefik.http.services.etherpad.loadbalancer.passhostheader=true"
```
### HAProxy
HAProxy detects the `Connection: Upgrade` exchange automatically and switches to
tunnel mode once the WebSocket is established. The important value is
`timeout tunnel`, which governs the lifetime of the upgraded connection.
```haproxy
frontend http
mode http
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/etherpad.pem alpn h2,http/1.1
http-request redirect scheme https code 301 unless { ssl_fc }
http-request add-header X-Forwarded-Proto https if { ssl_fc }
default_backend etherpad
backend etherpad
mode http
option forwardfor
timeout client 25s
timeout server 25s
timeout tunnel 3600s
server pad 127.0.0.1:9001
```
## Hosting under a subdirectory
To serve Etherpad from a path such as `https://example.com/pad` rather than from
the root of a domain, the proxy must send the `X-Proxy-Path` header so that
Etherpad rewrites its own asset and API URLs to include the prefix. This header
is honoured regardless of the `trustProxy` setting — see the [Configuration documentation](./configuration.md).
```nginx
location /pad/ {
rewrite ^/pad/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:9001;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Proxy-Path /pad;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
```
## Native HTTPS without a proxy
Etherpad can terminate TLS itself using Node's native HTTPS server, with no
reverse proxy required. Configure the `ssl` block in `settings.json`:
```json
"ssl": {
"key": "/path-to-your/etherpad-server.key",
"cert": "/path-to-your/etherpad-server.crt",
"ca": ["/path-to-your/intermediate-cert1.crt", "/path-to-your/intermediate-cert2.crt"]
}
```
* `key` — path to the private key file.
* `cert` — path to the certificate file.
* `ca` — an (optional) array of intermediate/chain certificate paths.
Restart Etherpad after editing the settings. It will now serve HTTPS on its
configured port.
For local testing you can generate a self-signed certificate with a single
command:
```bash
openssl req -x509 -newkey rsa:4096 -nodes -days 365 \
-keyout etherpad-server.key -out etherpad-server.crt \
-subj "/CN=localhost"
```
Make sure the files are readable only by the user that runs Etherpad:
```bash
chmod 400 etherpad-server.key etherpad-server.crt
chown etherpad etherpad-server.key etherpad-server.crt
```
::: tip
Self-signed certificates trigger browser warnings and are only suitable for
testing. For production, obtain a free, trusted certificate from
[Let's Encrypt](https://letsencrypt.org/), or terminate TLS at a reverse proxy
(see above) and let it manage certificate issuance and renewal.
:::
## Running as a service (systemd)
On a modern Linux distribution, run Etherpad as a `systemd` service so it starts
on boot and restarts automatically on failure.
Create a dedicated unprivileged user and install Etherpad into its home
directory (for example `/opt/etherpad`), owned by that user. Etherpad refuses to
start as root.
Create `/etc/systemd/system/etherpad.service`:
```ini
[Unit]
Description=Etherpad collaborative editor
After=network.target
[Service]
Type=simple
User=etherpad
Group=etherpad
WorkingDirectory=/opt/etherpad
Environment=NODE_ENV=production
ExecStart=/usr/bin/pnpm run prod
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
Adjust `WorkingDirectory` to your install path and the `ExecStart` path to
wherever `pnpm` lives (`which pnpm`). Then enable and start the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now etherpad.service
# check status and follow logs
sudo systemctl status etherpad.service
sudo journalctl -u etherpad.service -f
```
## Kubernetes (Istio)
The following manifest deploys Etherpad behind an Istio ingress gateway. It
defines three resources: a `Gateway` (TLS + hostname), a `VirtualService`
(routing with WebSocket-friendly timeouts), and a `DestinationRule` (sticky
sessions via the socket.io `io` cookie).
It assumes:
* Istio >= 1.18
* A `Service` named `etherpad` in the `etherpad` namespace, on port `9001`
* A TLS secret `etherpad-tls` provisioned in the gateway namespace
* You replace `<your-host>` with your own hostname
::: warning
Sticky sessions are necessary but **not** sufficient for a multi-replica
Etherpad deployment. Multi-replica also needs the socket.io Redis adapter so
that pad state is shared across pods. Without it, two clients editing the same
pad but routed to different pods will see divergent state.
Recommendation: start with `replicas: 1` plus good failover, and only go
multi-replica once the Redis adapter is wired up.
:::
```yaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: etherpad
namespace: etherpad
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: etherpad-tls
hosts:
- <your-host>
- port:
number: 80
name: http
protocol: HTTP
hosts:
- <your-host>
tls:
httpsRedirect: true
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: etherpad
namespace: etherpad
spec:
hosts:
- <your-host>
gateways:
- etherpad
http:
- match:
- uri:
prefix: /
route:
- destination:
host: etherpad
port:
number: 9001
# No per-request timeout — websockets and long-polling sit on the
# connection indefinitely. The default of 15s kills WS upgrades.
timeout: 0s
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: etherpad
namespace: etherpad
spec:
host: etherpad
trafficPolicy:
loadBalancer:
# Sticky sessions on the socket.io session cookie. Required so that
# long-polling fallback requests land on the same pod that owns the
# session state.
consistentHash:
httpCookie:
name: io
ttl: 0s # session cookie, expires with the browser tab
connectionPool:
tcp:
maxConnections: 10000
http:
# Must comfortably exceed socket.io's pingInterval (25s) +
# pingTimeout (20s). 1h is conservative.
idleTimeout: 3600s
h2UpgradePolicy: UPGRADE
http1MaxPendingRequests: 1000
```

240
doc/development.md Normal file
View file

@ -0,0 +1,240 @@
# Development
This page is a contributor-oriented tour of the Etherpad source tree and of a
few internals that plugin authors and core contributors commonly need to
understand: how the source is laid out, how pads are converted to and from
other formats, and how to access the database from server-side code.
The Etherpad server is written in TypeScript (`.ts`). Most server code lives
under `src/node/` and most client code under `src/static/js/`.
## Source tree overview
The repository root contains, among others, the following directories:
```
etherpad/
|- bin/ # maintenance and build scripts (run.sh, pad tools, docs, release)
|- doc/ # this manual, in AsciiDoc and Markdown
|- src/ # the Etherpad source code
|- packaging/ # OS/distribution packaging helpers
|- var/ # runtime data (e.g. the dirty.db database file)
```
`bin/` contains scripts for running and maintaining Etherpad. For example
`bin/run.sh` starts the server, and there are TypeScript utilities such as
`bin/checkPad.ts`, `bin/deletePad.ts`, `bin/repairPad.ts`,
`bin/rebuildPad.ts`, `bin/migrateDB.ts` and `bin/make_docs.ts`.
The HTML manual is built from the AsciiDoc sources in `doc/` by
`bin/make_docs.ts` (exposed as the `makeDocs` script), which shells out to
`asciidoctor` and writes the result to `out/doc/`. From the repository root you
can run it with `pnpm run makeDocs`. (`asciidoctor` must be installed.)
The `src/` directory looks like this:
```
src/
|- locales/ # translations, managed via https://translatewiki.net
|- node/ # server-side code
|- static/ # client-side code, CSS and fonts
|- templates/ # server-rendered page templates
|- ep.json # core plugin/hook registration
|- package.json # package name: ep_etherpad-lite
```
### src/node/ (server side)
```
src/node/
|- db/ # database access and pad/author/group/session state
|- eejs/ # server-side embedded-JS templating
|- handler/ # import/export and collaboration message handling
|- hooks/ # express route registration and i18n
|- security/ # crypto, OAuth2/OIDC, secret rotation
|- types/ # shared TypeScript types
|- updater/ # in-place self-update machinery
|- utils/ # settings, import/export format helpers, toolbar, minification
|- server.ts # entry point
```
`db/` contains the modules that read and write pad state. `Pad.ts` manages an
individual pad; `PadManager.ts`, `AuthorManager.ts`, `GroupManager.ts`,
`SessionManager.ts` and `ReadOnlyManager.ts` manage the corresponding records;
`DB.ts` exposes the low-level key/value store (see
[Accessing the database from server code / plugins](#accessing-the-database-from-server-code-plugins)); and `API.ts` implements
the public HTTP API.
`handler/` contains the request and message handlers. `PadMessageHandler.ts`
drives real-time collaboration, while `ImportHandler.ts` and `ExportHandler.ts`
handle import and export.
`hooks/` contains mostly Express-related code. `i18n.ts` builds the translation
files and registers routes to serve them, and `hooks/express/` registers the
routes that serve pads, the timeslider, static assets and the admin pages.
`utils/` contains the import/export format converters (`ImportHtml.ts`,
`ExportHtml.ts`, `ExportTxt.ts`, `ExportEtherpad.ts`, `ImportEtherpad.ts`,
`ExportHelper.ts`, and native converters such as `ExportPdfNative.ts` and
`ImportDocxNative.ts`), the settings parser (`Settings.ts`), the toolbar builder
(`toolbar.ts`) and the asset minifier (`Minify.ts`).
### src/static/ (client side)
```
src/static/
|- css/ # stylesheets, including css/pad/icons.css
|- font/ # web fonts, including the fontawesome-etherpad icon font
|- img/
|- js/ # client-side TypeScript
|- skins/ # bundled UI skins
|- vendor/
```
`js/` contains the client-side editor code. Notable modules include
`ace2_inner.ts` and `ace2_common.ts` (the editor core), `contentcollector.ts`,
`linestylefilter.ts` and `domline.ts` (content/attribute processing, shared
with the server import/export pipeline), `Changeset.ts` and `AttributePool.ts`
(the changeset and attribute model), and `collab_client.ts` (the
client side of real-time collaboration).
### src/templates/
`templates/` contains the server-rendered page templates for the index, the
pad, the timeslider and the admin pages, plus the bootstrap scripts that load
the client bundles. The templates expose named `eejs` blocks that plugins can
hook into to inject custom HTML.
## How Etherpad converts pads to and from other formats
Internally a pad is not stored as HTML. A pad is a sequence of lines, and each
line carries **attributes** (for example `heading1`, `bullet` or a list number).
The set of attributes that a pad can use is stored in its **attribute pool**; the
pool only records which attributes exist, not where they are applied. The
pool grows over the history of the pad.
Where an attribute is applied to a line is recorded in an **attribute string**,
and a line that carries a line-level attribute is prefixed with a **line marker**
(`lmkr`). Attribute strings and changesets are defined by
`src/static/js/Changeset.ts` and `src/static/js/AttributePool.ts`.
### Collecting content
`src/static/js/contentcollector.ts` is the shared starting point for both the
client (when content is typed or pasted) and the server (when content is
imported). It walks the incoming DOM/HTML, decides which attributes apply to
each line, adds the discovered attributes to the attribute pool, and emits the
resulting attribute strings. On import, `src/node/utils/ImportHtml.ts` calls
`contentcollector.makeContentCollector(...)` to do exactly this, and the HTML
import path in `src/node/handler/ImportHandler.ts` ultimately drives it.
### From attributes to HTML/text (export)
On export the flow is, conceptually:
```
contentcollector.ts
-> linestylefilter.ts
-> ExportHtml.ts / ExportTxt.ts (helped by ExportHelper.ts)
-> ExportHandler.ts
-> the HTTP API / /export/* route
```
- `src/static/js/linestylefilter.ts` walks each line, reads its attributes,
and turns them into the classes/markup the line should render with.
- `src/node/utils/ExportHelper.ts` adds export-only logic that does not belong
in the live editor. The clearest example is lists: in the editor each list
item is rendered as its own line-level block, but a clean export needs the
items collapsed into a single properly nested list. The helper performs that
reshaping for export only.
- `src/node/utils/ExportHtml.ts` and `src/node/utils/ExportTxt.ts` (and
`ExportEtherpad.ts` for the native `.etherpad` format) turn the attributed
text (`atext`) into the final HTML or plain text.
- `src/node/handler/ExportHandler.ts` receives the export request and dispatches
on the requested format — for instance, office formats such as `.docx` and
`.pdf` are routed through the native converters / LibreOffice rather than
through the plain HTML/text path.
On the client side, edits are turned into changesets by the editor, attributes
are translated into CSS classes (so `heading2` becomes
`class="heading2"`), and `src/static/js/domline.ts` (`createDomLine`) renders
the final DOM for each line.
## Accessing the database from server code / plugins
Etherpad stores everything in a single key/value store backed by
[ueberDB](https://www.npmjs.com/package/ueberdb2), which abstracts over the
configured database (dirtyDB, MySQL/MariaDB, PostgreSQL, SQLite, MongoDB, Redis,
and others). Server-side code and plugins access it through
`src/node/db/DB.ts`.
The package name of the core module is, for historical reasons, still
`ep_etherpad-lite`, so plugins import the database module like this:
```javascript
const db = require('ep_etherpad-lite/node/db/DB');
```
The exposed methods are asynchronous and return promises (use `await`), not the
old callback style. The available methods are `get`, `set`, `remove`, `getSub`,
`setSub`, `findKeys` and `findKeysPaged`:
```javascript
// Read a record (returns undefined/null if it does not exist)
const value = await db.get('record_key');
// Create or replace a record
await db.set('record_key', data);
// Read or write a nested value inside a record
const colorId = await db.getSub('author_key', ['colorId']);
await db.setSub('author_key', ['email'], 'tutti@frutti.org');
// Delete a record
await db.remove('record_key');
```
For example, given the author record:
```json
{"colorId":"#79d9d9","name":"tutti","timestamp":1364832712430,"padIDs":{"mypad":1}}
```
calling `await db.setSub('author_key', ['email'], 'tutti@frutti.org')` yields:
```json
{"colorId":"#79d9d9","name":"tutti","timestamp":1364832712430,"padIDs":{"mypad":1},"email":"tutti@frutti.org"}
```
::: warning
Keys are namespaced (for example `pad:<padId>`,
`pad:<padId>:revs:<rev>`, `globalAuthor:<authorId>`). Prefer the high-level
managers (`Pad.ts`, `AuthorManager.ts`, etc.) over direct `DB` access where one
exists; reach for `DB` directly only for data your plugin owns, and use a key
prefix unique to your plugin to avoid collisions.
:::
## Adding a toolbar icon
Etherpad's toolbar icons come from the bundled `fontawesome-etherpad` icon
font in `src/static/font/`. Toolbar buttons reference an icon by a
`buttonicon-<name>` CSS class (see `src/node/utils/toolbar.ts`, which builds
each button's class as `buttonicon buttonicon-<name>`), and those classes are
defined in `src/static/css/pad/icons.css`. The font itself is generated with
[Fontello](http://fontello.com) from `src/static/font/config.json` (whose
`css_prefix_text` is `buttonicon-`).
To add a new icon:
1. Go to [Fontello](http://fontello.com) and import the existing
`src/static/font/config.json` (Fontello's "import" loads the current icon
set and pre-selects the icons it contains).
2. Select the additional icon(s) you want, then click **Download webfont**.
3. From the unzipped download, copy `config.json` and the
`font/fontawesome-etherpad.*` files over the ones in `src/static/font/`.
4. From the unzipped `css/fontawesome-etherpad.css`, copy the new
`.buttonicon-<name>:before { content: '\\eXXX'; }` rules into
`src/static/css/pad/icons.css`, replacing the existing block of icon rules.
The icon is then available wherever a `buttonicon-<name>` class can be used,
including toolbar button definitions.

View file

@ -118,6 +118,25 @@ The `settings.json.docker` available by default allows to control almost every s
| `USER_PASSWORD` | the password for the first user `user` (leave unspecified if you do not want to create it) | |
### Updates & privacy (offline / air-gapped)
Etherpad makes a small number of outbound calls (a periodic version check and the admin plugin catalogue). In an air-gapped or firewalled deployment these can be disabled entirely without editing `settings.json` inside the image — set the variables below. See [PRIVACY.md](https://github.com/ether/etherpad/blob/develop/PRIVACY.md) and [doc/admin/updates.md](admin/updates.md) for what each call sends.
| Variable | Description | Default |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `PRIVACY_UPDATE_CHECK` | Set to `false` to disable the hourly version check (`UpdateCheck.ts`). | `true` |
| `PRIVACY_PLUGIN_CATALOG` | Set to `false` to disable the admin plugin browser (manual install-by-name via CLI still works). | `true` |
| `UPDATES_TIER` | Self-updater tier: `off` \| `notify` \| `manual` \| `auto` \| `autonomous`. Set to `off` to suppress the GitHub Releases check entirely. | `notify` |
| `UPDATES_SOURCE` | Where update metadata is fetched from. | `github` |
| `UPDATES_CHANNEL` | Release channel to track. | `stable` |
| `UPDATES_CHECK_INTERVAL_HOURS` | How often (hours) the updater polls when not `off`. | `6` |
| `UPDATES_GITHUB_REPO` | Repository the updater checks for releases. | `ether/etherpad` |
| `UPDATES_REQUIRE_ADMIN_FOR_STATUS`| Lock `/admin/update/status` to authenticated admins. | `false` |
| `UPDATE_SERVER` | Endpoint backing the version check. Point elsewhere (or disable the check above) for offline installs. | `https://etherpad.org/ep_infos` |
> **Fully offline:** set `UPDATES_TIER=off`, `PRIVACY_UPDATE_CHECK=false`, and `PRIVACY_PLUGIN_CATALOG=false`. The version check is fire-and-forget and already fails closed (a blocked endpoint is caught and logged, it does not prevent startup), but disabling it removes the outbound attempt and the log noise.
### Database
| Variable | Description | Default |

204
doc/faq.md Normal file
View file

@ -0,0 +1,204 @@
# FAQ
This page answers common operational questions about running and maintaining
an Etherpad instance. It collects material previously kept on the project wiki.
## How do I install Etherpad?
There are several supported ways to install Etherpad. Pick whichever suits your
environment.
### Docker
The official image is published to Docker Hub (`etherpad/etherpad`) and to the
GitHub Container Registry (`ghcr.io/ether/etherpad`) with identical tags.
```bash
docker pull etherpad/etherpad
docker run -p 9001:9001 etherpad/etherpad
```
See the [Docker chapter](./docker.md) for building personalized images, enabling plugins, and
configuring office-format import/export.
### One-line installer (macOS / Linux / WSL)
```bash
curl -fsSL https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.sh | sh
```
On Windows (PowerShell):
```powershell
irm https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.ps1 | iex
```
The installer clones Etherpad, installs dependencies and builds the frontend.
Set `ETHERPAD_RUN=1` to also start it once the install finishes.
### apt repository (Debian / Ubuntu)
Etherpad publishes a signed APT repository (`stable` channel). Import the signing
key, add the repository and install:
```bash
curl -fsSL https://etherpad.org/key.asc \
| sudo gpg --dearmor -o /usr/share/keyrings/etherpad.gpg
echo "deb [signed-by=/usr/share/keyrings/etherpad.gpg] https://etherpad.org/apt stable main" \
| sudo tee /etc/apt/sources.list.d/etherpad.list
sudo apt-get update
sudo apt-get install etherpad
```
The repository provides `amd64` and `arm64` builds. Etherpad depends on
Node.js >= 24, so on older distributions you may also need NodeSource's apt
repository to satisfy that dependency.
### From source
Etherpad requires [Node.js](https://nodejs.org/) >= 24 and `pnpm`.
```bash
git clone -b master https://github.com/ether/etherpad
cd etherpad
pnpm i
pnpm run build:etherpad
pnpm run prod
```
Then open `http://localhost:9001`.
## What URL paths does Etherpad serve?
| Path | Description |
|------|-------------|
| `/admin` | Administration dashboard (requires admin login). |
| `/admin/plugins` | Install, update and remove plugins from the web UI. |
| `/admin/settings` | Edit `settings.json` from the web UI. |
| `/p/:padID` | Open (or create) the pad with the given `padID`, e.g. `/p/foo`. |
| `/p/:padID/timeslider` | Open the pad's history/timeslider view. Append `#N` to jump to a specific revision, e.g. `/p/foo/timeslider#5`. |
| `/p/:padID/export/:type` | Export the pad in the given format, e.g. `/p/foo/export/html`. Append `?revs=N` to export a specific revision. |
Supported export types:
- **Native (no extra dependencies):** `txt`, `html`, `etherpad`, `docx`, `pdf`.
- **Via LibreOffice:** `odt`, `doc`, `rtf` — these require the `soffice` setting
to point at a LibreOffice executable. See the office-format notes in the
[Docker chapter](./docker.md).
## How do I list all pads?
The recommended way is the HTTP API method `listAllPads`, combined with `jq`:
```bash
ETHERPAD_HOST='https://pad.example.com'
ETHERPAD_API_KEY='...' # the APIKEY.txt file in the Etherpad root
ETHERPAD_API_VERSION='...' # see https://pad.example.com/api
curl -s "${ETHERPAD_HOST}/api/${ETHERPAD_API_VERSION}/listAllPads?apikey=${ETHERPAD_API_KEY}" \
| jq -r '.data.padIDs[]'
```
For an interactive list with management actions, install the `ep_adminpads2`
plugin and browse to `/admin/pads`.
As a last resort you can query the database directly. The exact query depends on
your configured backend; pad records use keys of the form `pad:<padID>` and
`pad:<padID>:revs:<n>`. For example, with SQLite:
```bash
sqlite3 ./var/sqlite.db "select key from store where key like 'pad:%'" \
| grep -Eo '^pad:[^:]+' \
| sed -e 's/pad://' \
| sort -u
```
Prefer the API or admin plugin over direct SQL: the schema is an implementation
detail and may change.
## How do I delete or manage pads?
Use the HTTP API `deletePad` method:
```bash
curl -s "${ETHERPAD_HOST}/api/${ETHERPAD_API_VERSION}/deletePad?apikey=${ETHERPAD_API_KEY}&padID=foo"
```
The API also offers `copyPad`, `movePad`, `getRevisionsCount` and more — see the
[HTTP API chapter](./api/http_api.md).
For a web UI, install the `ep_adminpads2` plugin and manage pads from
`/admin/pads`, where you can search, view and delete pads.
The `deletePad` CLI tool is also available for operators:
```bash
pnpm run --filter bin deletePad <padID>
```
## How do I back up and restore pads?
### Back up the whole instance
All pad data lives in the configured database. Back it up using the tool
appropriate to your backend (for example `mysqldump` for MySQL/MariaDB,
`pg_dump` for PostgreSQL, or a file copy of `var/*.db` for the file-based
`dirty`/`rusty` engines while Etherpad is stopped). A regular, automated dump of
the database is the canonical backup for a production instance.
### Back up a single pad
Export the pad over HTTP by appending `/export/<type>` to its URL. Plain text,
HTML and the round-trippable `etherpad` format are most useful for backups:
```bash
curl -o mypad.txt https://pad.example.com/p/foo/export/txt
curl -o mypad.html https://pad.example.com/p/foo/export/html
curl -o mypad.etherpad https://pad.example.com/p/foo/export/etherpad
```
The `etherpad` export preserves the pad's full history and can be re-imported,
making it the best choice for migrating or archiving an individual pad.
### Restore or inspect an old revision
Every state the pad has been in is stored in the database, so you can retrieve
an earlier revision without a separate backup:
- Open `/p/:padID/timeslider` to browse the history and find the revision
number you want.
- Export a specific revision directly with the `?revs=N` query parameter, e.g.
`https://pad.example.com/p/foo/export/html?revs=1000`.
### Repairing a damaged pad
If a pad is corrupt, use the CLI repair tools (`checkPad`, `repairPad`,
`rebuildPad`) documented in the [CLI chapter](./cli.md). Always back up the database before
running write operations.
## How do I limit history or prune revisions?
Etherpad keeps the full revision history of every pad, so the database grows
over time. To reclaim space, use the pad-compaction CLI tools, which collapse or
trim revision history for one pad, every pad, or only stale pads:
```bash
# Collapse all history of one pad
pnpm run --filter bin compactPad <padID>
# Keep only the last 50 revisions of one pad
pnpm run --filter bin compactPad <padID> --keep 50
# Compact every pad on the instance
pnpm run --filter bin compactAllPads
# Compact only pads not edited in the last 90 days, keeping the last 50 revisions
pnpm run --filter bin compactStalePads --older-than 90 --keep 50
```
These tools require `cleanup.enabled = true` in `settings.json` and are
**destructive** — history is collapsed or trimmed. Export anything you can't
afford to lose via the pad's `/export/etherpad` route first. The same primitive
is available over the wire as the `compactPad` HTTP API method. See the [CLI chapter](./cli.md) for full details.

View file

@ -1,7 +1,7 @@
{
"devDependencies": {
"oxc-minify": "^0.135.0",
"vitepress": "^2.0.0-alpha.17"
"oxc-minify": "^0.139.0",
"vitepress": "^2.0.0-alpha.18"
},
"scripts": {
"docs:dev": "vitepress dev",

View file

@ -45,13 +45,13 @@
"node": ">=24.0.0",
"pnpm": ">=11.1.2"
},
"packageManager": "pnpm@11.1.2",
"packageManager": "pnpm@11.10.0",
"repository": {
"type": "git",
"url": "https://github.com/ether/etherpad.git"
},
"engineStrict": true,
"version": "3.3.1",
"version": "3.3.2",
"license": "Apache-2.0",
"pnpm": {
"onlyBuiltDependencies": [

3778
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -21,20 +21,38 @@ strictDepBuilds: false
# As of pnpm 11, overrides must live here (root package.json's pnpm.overrides
# is no longer read). Force-bump transitive deps with known CVEs.
overrides:
basic-ftp: '>=5.3.0'
'@babel/core@<7.29.6': '>=7.29.6'
'@opentelemetry/core@<2.8.0': '>=2.8.0'
basic-ftp@<5.3.1: '>=5.3.1 <6.0.0'
brace-expansion@>=2.0.0 <2.0.3: '>=2.0.3'
diff@>=6.0.0 <8.0.3: '>=8.0.3'
esbuild@<0.28.1: '>=0.28.1'
flatted: '>=3.4.2'
follow-redirects: '>=1.16.0'
form-data@>=4.0.0 <4.0.6: '>=4.0.6'
glob@>=10.2.0 <10.5.0: '>=10.5.0'
js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
ip-address@<10.1.1: '>=10.1.1'
js-yaml@>=4.0.0 <4.2.0: '>=4.2.0'
lodash: '>=4.18.0'
minimatch@>=9.0.0 <9.0.7: '>=9.0.7'
path-to-regexp@>=8.0.0 <8.4.0: '>=8.4.0'
picomatch@>=4.0.0 <4.0.4: '>=4.0.4'
qs@>=6.7.0 <6.14.2: '>=6.14.2'
qs@>=6.7.0 <6.15.2: '>=6.15.2'
serialize-javascript@<7.0.5: '>=7.0.5'
socket.io-parser@>=4.0.0 <4.2.6: '>=4.2.6'
tar@<7.5.11: '>=7.5.11'
tar@<7.5.16: '>=7.5.16'
uuid@<14.0.0: '>=14.0.0'
vite@>=7.0.0 <7.3.2: '>=7.3.2'
ws@>=8.0.0 <8.21.0: '>=8.21.0'
minimumReleaseAgeExclude:
- '@radix-ui/primitive@1.1.5'
- '@radix-ui/react-collection@1.1.12'
- '@radix-ui/react-context@1.2.0'
- '@radix-ui/react-dialog@1.1.19'
- '@radix-ui/react-dismissable-layer@1.1.15'
- '@radix-ui/react-focus-scope@1.1.12'
- '@radix-ui/react-presence@1.1.7'
- '@radix-ui/react-toast@1.2.19'
- mysql2@3.22.6
- oidc-provider@9.9.0
- sql-escaper@1.4.0

View file

@ -210,15 +210,17 @@
* tier: "off" | "notify" | "manual" | "auto" | "autonomous"
* Default "notify" shows a banner when an update is available.
* Docker installs are read-only — tiers above "notify" are not applied even if requested.
* Air-gapped / offline deployments should set UPDATES_TIER=off to suppress the
* periodic check against the GitHub Releases API entirely.
*/
"updates": {
"tier": "notify",
"source": "github",
"channel": "stable",
"tier": "${UPDATES_TIER:notify}",
"source": "${UPDATES_SOURCE:github}",
"channel": "${UPDATES_CHANNEL:stable}",
"installMethod": "docker",
"checkIntervalHours": 6,
"githubRepo": "ether/etherpad",
"requireAdminForStatus": false,
"checkIntervalHours": "${UPDATES_CHECK_INTERVAL_HOURS:6}",
"githubRepo": "${UPDATES_GITHUB_REPO:ether/etherpad}",
"requireAdminForStatus": "${UPDATES_REQUIRE_ADMIN_FOR_STATUS:false}",
"preApplyGraceMinutes": 0,
"drainSeconds": 60,
"rollbackHealthCheckSeconds": 60,
@ -321,7 +323,19 @@
* https://etherpad.org/ep_infos
*/
"updateServer": "https://etherpad.org/ep_infos",
"updateServer": "${UPDATE_SERVER:https://etherpad.org/ep_infos}",
/*
* Outbound network calls. See PRIVACY.md for what each one sends.
* - PRIVACY_UPDATE_CHECK=false : disables the hourly version check (UpdateCheck.ts)
* - PRIVACY_PLUGIN_CATALOG=false : disables the admin plugin browser
* (manual install-by-name via CLI still works)
* Air-gapped / firewalled deployments should set both to false.
*/
"privacy": {
"updateCheck": "${PRIVACY_UPDATE_CHECK:true}",
"pluginCatalog": "${PRIVACY_PLUGIN_CATALOG:true}"
},
/*
* The type of the database.

View file

@ -216,7 +216,7 @@
* Default "notify" shows a banner when an update is available. "off" disables the version check.
*/
"updates": {
"tier": "notify",
"tier": "${UPDATES_TIER:notify}",
"source": "github",
"channel": "stable",
"installMethod": "auto",
@ -443,17 +443,19 @@
* https://etherpad.org/ep_infos
*/
"updateServer": "https://etherpad.org/ep_infos",
"updateServer": "${UPDATE_SERVER:https://etherpad.org/ep_infos}",
/*
* Outbound network calls. See PRIVACY.md for what each one sends.
* - updateCheck=false : disables hourly version check (UpdateCheck.ts)
* - pluginCatalog=false: disables admin plugin browser
* (manual install-by-name via CLI still works)
* Air-gapped / firewalled deployments should set both PRIVACY_UPDATE_CHECK and
* PRIVACY_PLUGIN_CATALOG to false (or UPDATES_TIER=off, which covers the version check).
*/
"privacy": {
"updateCheck": true,
"pluginCatalog": true
"updateCheck": "${PRIVACY_UPDATE_CHECK:true}",
"pluginCatalog": "${PRIVACY_PLUGIN_CATALOG:true}"
},
/*

View file

@ -124,7 +124,7 @@
"pad.importExport.exportword": "مايكروسوفت وورد",
"pad.importExport.exportpdf": "صيغة المستندات المحمولة",
"pad.importExport.exportopen": "ODF (نسق المستند المفتوح)",
"pad.importExport.noConverter.innerHTML": "لا يمكنك الاستيراد إلا من تنسيقات النصوص العادية أو تنسيقات HTML. لمزيد من ميزات الاستيراد المتقدمة، يرجى <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">تثبيت LibreOffice</a> .",
"pad.importExport.noConverter.innerHTML": "يمكنك استيراد النصوص العادية، وملفات HTML، وملفات Microsoft Word (.docx)، وملفات Etherpad مباشرةً. لاستيراد تنسيقات أخرى مثل PDF، وODT، وDOC، وRTF، يحتاج مسؤول الخادم إلى تثبيت LibreOffice - راجع <a href=\"https://docs.etherpad.org/\">وثائق Etherpad</a> .",
"pad.modals.connected": "متصل.",
"pad.modals.reconnecting": "إعادة الاتصال ببادك..",
"pad.modals.forcereconnect": "فرض إعادة الاتصال",

View file

@ -116,7 +116,7 @@
"pad.modals.looping.explanation": "Праблемы далучэньня да сэрвэра сынхранізацыі.",
"pad.modals.looping.cause": "Магчыма, вы падключыліся празь несумяшчальны брандмаўэр або проксі.",
"pad.modals.initsocketfail": "Сэрвэр недаступны.",
"pad.modals.initsocketfail.explanation": "Не атрымалася падлучыцца да сэрвэра сынхранізацыі.",
"pad.modals.initsocketfail.explanation": "Не ўдалося падлучыцца да сэрвэра сынхранізацыі.",
"pad.modals.initsocketfail.cause": "Імаверна, гэта зьвязана з праблемамі з вашым браўзэрам або інтэрнэт-злучэньнем.",
"pad.modals.slowcommit.explanation": "Сэрвэр не адказвае.",
"pad.modals.slowcommit.cause": "Гэта можа быць выклікана праблемамі зь сеткавым падлучэньнем.",
@ -178,9 +178,9 @@
"pad.impexp.importbutton": "Імпартаваць зараз",
"pad.impexp.importing": "Імпартаваньне…",
"pad.impexp.confirmimport": "Імпарт файла перазапіша цяперашні тэкст дакумэнту. Вы ўпэўненыя, што хочаце працягваць?",
"pad.impexp.convertFailed": "Не атрымалася імпартаваць гэты файл. Калі ласка, выкарыстайце іншы фармат дакумэнту або скапіюйце ўручную.",
"pad.impexp.convertFailed": "Не ўдалося імпартаваць гэты файл. Калі ласка, выкарыстайце іншы фармат дакумэнту або скапіюйце рукамі.",
"pad.impexp.padHasData": "Мы не змаглі імпартаваць гэты файл, бо дакумэнт ужо мае зьмены, калі ласка, імпартуйце ў новы дакумэнт",
"pad.impexp.uploadFailed": "Загрузка не атрымалася, калі ласка, паспрабуйце яшчэ раз",
"pad.impexp.uploadFailed": "Загрузка не ўдалася, калі ласка, паспрабуйце яшчэ раз",
"pad.impexp.importfailed": "Памылка імпарту",
"pad.impexp.copypaste": "Калі ласка, скапіюйце і ўстаўце",
"pad.impexp.exportdisabled": "Экспарт у фармаце {{type}} адключаны. Калі ласка, зьвярніцеся да вашага сыстэмнага адміністратара па падрабязнасьці.",

View file

@ -16,13 +16,80 @@
]
},
"admin.page-title": "Ovládací panel Správce - Etherpad",
"admin.loading": "Načítám…",
"admin.loading_description": "Počkejte prosím, než se stránka načte.",
"admin.toggle_sidebar": "Přepnout postranní panel",
"admin.shout": "Komunikace",
"admin_shout.online_one": "Aktuálně je online {{count}} uživatelů",
"admin_shout.online_other": "Aktuálně je online {{count}} uživatelů",
"admin_shout.sticky_toggle": "Změnit připevněnou zprávu",
"admin_login.title": "Etherpad",
"admin_login.username": "Uživatelské jméno",
"admin_login.password": "Heslo",
"admin_login.submit": "Přihlásit se",
"admin_login.failed": "Přihlášení se nezdařilo",
"admin_pads.all_pads": "Všechny Pady",
"admin_pads.bulk.cleanup_history": "Vyčistit historii",
"admin_pads.bulk.clear_selection": "Vymazat výběr",
"admin_pads.bulk.delete": "Smazat",
"admin_pads.cancel": "Zrušit",
"admin_pads.col.pad": "Pad",
"admin_pads.col.revisions": "Revize",
"admin_pads.col.users": "Uživatelé",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Vyberte název pro nový Pad.",
"admin_pads.delete_pad_dialog_description": "Potvrdit nebo zrušit smazání Padu.",
"admin_pads.delete_pad_dialog_title": "Smazat pad",
"admin_pads.empty_never_edited": "prázdné · nikdy neupravené",
"admin_pads.error_dialog_description": "Došlo k chybě.",
"admin_pads.error_prefix": "Chyba",
"admin_pads.filter.active": "Aktivní",
"admin_pads.filter.all": "Vše",
"admin_pads.filter.empty": "Prázdné",
"admin_pads.filter.recent": "Tento týden",
"admin_pads.filter.stale": "Zastaralé (>1 rok)",
"admin_pads.open": "Otevřít",
"admin_pads.pagination.next": "Další",
"admin_pads.pagination.previous": "Předchozí",
"admin_pads.refresh": "Obnovit",
"admin_pads.relative.days": "před {{count}} dny",
"admin_pads.relative.hours": "před {{count}}h",
"admin_pads.relative.just_now": "právě teď",
"admin_pads.relative.minutes": "před {{count}} minutami",
"admin_pads.relative.months": "před {{count}} měsíci",
"admin_pads.relative.weeks": "před {{count}} týdny",
"admin_pads.relative.years": "před {{count}} lety",
"admin_pads.revisions_count": "{{count}} revizí",
"admin_pads.selected_count": "Vybráno {{count}}",
"admin_pads.show": "Zobrazit",
"admin_pads.sort.name": "Jméno (AZ)",
"admin_pads.sort.revision_number": "Revize",
"admin_pads.sort.user_count": "Uživatelé",
"admin_pads.stats.across_pads": "napříč všemi pady",
"admin_pads.stats.active_users": "Aktivní uživatelé",
"admin_pads.stats.empty_pads": "Prázdné pady",
"admin_pads.stats.last_activity": "Poslední aktivita:",
"admin_pads.stats.no_active_users": "Žádní aktivní uživatelé",
"admin_pads.stats.revisions_zero": "0 revizí",
"admin_pads.stats.total": "Celkový počet padů",
"admin_pads.stats.users_active": "{{count}} aktuálně aktivních",
"admin_pads.subtitle": "Přehled všech padů na této instanci Etherpadu. Vyhledat, vyčistit, otevřít.",
"admin_plugins": "Správce zásuvných moodulů",
"admin_plugins.available": "Dostupné zásuvné moduly",
"admin_plugins.available_not-found": "Nejsou žádné zásuvné moduly",
"admin_plugins.available_fetching": "Načítání...",
"admin_plugins.available_install.value": "Instalovat",
"admin_plugins.available_search.placeholder": "Vyhledat zásuvné moduly k instalaci",
"admin_plugins.check_updates": "Zkontrolovat aktualizace",
"admin_plugins.core_count": "{{count}} jádro",
"admin_plugins.catalog_disabled": "Katalog pluginů je zakázán vaším operátorem (privacy.pluginCatalog=false). Chcete-li plugin nainstalovat, spusťte příkaz `pnpm run plugins i ep_<name> ` ze serveru.",
"admin_plugins.crumbs": "Pluginy",
"admin_plugins.description": "Popis",
"admin_plugins.disables.label": "Vypnuto:",
"admin_plugins.disables.warning_title": "Tento plugin záměrně odstraňuje uvedené funkce Etherpadu.",
"admin_plugins.error_retrieving": "Chyba při načítání pluginů",
"admin_plugins.install_error": "Instalace pluginu {{plugin}} se nezdařila: {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Nelze nainstalovat {{plugin}}: vyžaduje novější verzi Etherpadu. Prosím, aktualizujte Etherpad a zkuste to znovu.",
"admin_plugins.installed": "Nainstalované zásuvné moduly",
"admin_plugins.installed_fetching": "Načítání instalovaných zásuvných modulů...",
"admin_plugins.installed_nothing": "Dosud jste nenainstalovali žádné zásuvné moduly.",
@ -30,23 +97,61 @@
"admin_plugins.last-update": "Poslední aktualizace",
"admin_plugins.name": "Název",
"admin_plugins.page-title": "Správce zásuvných modulů - Etherpad",
"admin_plugins.reload_catalog": "Obnovit katalog",
"admin_plugins.search_npm": "Hledat na npm",
"admin_plugins.sort_ascending": "Seřadit vzestupně",
"admin_plugins.sort_descending": "Seřadit sestupně",
"admin_plugins.sort.last_updated": "Naposledy aktualizováno",
"admin_plugins.sort.name": "Jméno (AZ)",
"admin_plugins.sort.version": "Verze",
"admin_plugins.source": "Zdroj pluginu",
"admin_plugins.subtitle": "Instalace, aktualizace a odebrání pluginů Etherpad. Změny vyžadují restart serveru.",
"admin_plugins.tag_core": "Jádro",
"admin_plugins.update_tooltip": "Aktualizovat",
"admin_plugins.updates_available": "Dostupné aktualizace",
"admin_plugins.update_now": "Aktualizovat",
"admin_plugins.version": "Verze",
"admin_plugins_info": "Informace o řešení problému",
"admin_plugins_info.bindings_label": "{{count}} vazeb",
"admin_plugins_info.copy_diagnostics": "Diagnostika kopírování",
"admin_plugins_info.copy_value": "Kopírovat {{label}}",
"admin_plugins_info.git_sha": "SHA v Gitu",
"admin_plugins_info.hooks": "Instalované hooks",
"admin_plugins_info.hooks_client": "hooks na straně klienta",
"admin_plugins_info.hooks_server": "hooks na straně serveru",
"admin_plugins_info.parts": "Nainstalované součásti",
"admin_plugins_info.plugins": "Nainstalované zásuvné moduly",
"admin_plugins_info.page-title": "Informace o zásuvných modulech - Etherpad",
"admin_plugins_info.tab_client": "Klient",
"admin_plugins_info.tab_server": "Server",
"admin_plugins_info.up_to_date": "Aktuální",
"admin_plugins_info.update_available": "Aktualizace k dispozici: {{version}}",
"admin_plugins_info.version": "Verze Etherpad",
"admin_plugins_info.version_latest": "Poslední dostupná verze",
"admin_plugins_info.version_number": "Číslo verze",
"admin_settings": "Nastavení",
"admin_settings.create_pad": "Vytvořit pad",
"admin_settings.current": "Aktuální konfugurace",
"admin_settings.current_example-devel": "Příklad ukázkové vývojové šablony",
"admin_settings.current_example-prod": "Příklad šablony nastavení výroby",
"admin_settings.current_restart.value": "Restartovat Etherpad",
"admin_settings.current_save.value": "Uložit nastavení",
"admin_settings.invalid_json": "Neplatný JSON",
"admin_settings.current_test.value": "Ověření JSON",
"admin_settings.current_prettify.value": "Zkrášlit JSON",
"admin_settings.toast.saved": "Nastavení bylo úspěšně uloženo.",
"admin_settings.toast.save_failed": "Uložení se nezdařilo: soubor settings.json se nepodařilo zapsat.",
"admin_settings.toast.json_invalid": "Syntaktická chyba: zkontrolujte čárky, závorky a uvozovky.",
"admin_settings.toast.disconnected": "Nelze uložit: nejsem připojen k serveru.",
"admin_settings.toast.validation_ok": "JSON je platný.",
"admin_settings.toast.validation_failed": "JSON je neplatný: opravte syntaktické chyby.",
"admin_settings.toast.prettify_failed": "Nelze upravovat: nejprve opravte syntaktické chyby.",
"admin_settings.prettify_confirm": "Zkrášlováním odstraníte všechny komentáře. Pokračovat?",
"admin_settings.mode.form": "Formulář",
"admin_settings.mode.effective": "Efektivní",
"admin_settings.mode.effective_tooltip": "Zobrazení hodnot, které Etherpad aktuálně používá, pouze pro čtení, po substituci proměnných prostředí. Tajné kódy jsou redigovány.",
"admin_settings.mode.aria_label": "Režim editoru",
"admin_settings.envvar_banner.title": "Tento soubor je šablona, nikoli živá konfigurace.",
"admin_settings.page-title": "Nastavení - Etherpad",
"index.newPad": "Založ nový Pad",
"index.settings": "Nastavení",

View file

@ -10,6 +10,7 @@
"Metalhead64",
"Mklehr",
"Mukeber",
"Nbux",
"Nipsky",
"Predatorix",
"SamTV",
@ -22,13 +23,80 @@
]
},
"admin.page-title": "Admin Dashboard - Etherpad",
"admin.loading": "Lade …",
"admin.loading_description": "Bitte warten, die Seite wird aktualisiert ...",
"admin.toggle_sidebar": "Seitenleiste ein-/ausblenden",
"admin.shout": "Kommunikation",
"admin_shout.online_one": "Es ist derzeit {{count}} Benutzer online",
"admin_shout.online_other": "Es sind aktuell {{count}} Benutzer online",
"admin_shout.sticky_toggle": "Nachricht bleibt angeheftet",
"admin_login.title": "Etherpad",
"admin_login.username": "Benutzername",
"admin_login.password": "Passwort",
"admin_login.submit": "Login",
"admin_login.failed": "Login fehlgeschlagen.",
"admin_pads.all_pads": "Alle Pads",
"admin_pads.bulk.cleanup_history": "Verlauf löschen",
"admin_pads.bulk.clear_selection": "Auswahl löschen",
"admin_pads.bulk.delete": "Löschen",
"admin_pads.cancel": "Abbrechen",
"admin_pads.col.pad": "Pad",
"admin_pads.col.revisions": "Bearbeitungen",
"admin_pads.col.users": "Benutzer",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Wähle einen Namen für das neue Pad.",
"admin_pads.delete_pad_dialog_description": "Bestätigen oder stornieren Sie die Löschung des Pads.",
"admin_pads.delete_pad_dialog_title": "Pad löschen",
"admin_pads.empty_never_edited": "leer · nie bearbeitet",
"admin_pads.error_dialog_description": "Ein Fehler ist aufgetreten",
"admin_pads.error_prefix": "Fehler",
"admin_pads.filter.active": "Aktiv",
"admin_pads.filter.all": "Alle",
"admin_pads.filter.empty": "Leer",
"admin_pads.filter.recent": "Diese Woche",
"admin_pads.filter.stale": "Veraltet (> 1 Jahr)",
"admin_pads.open": "Offen",
"admin_pads.pagination.next": "Weiter",
"admin_pads.pagination.previous": "Zurück",
"admin_pads.refresh": "Aktualisieren",
"admin_pads.relative.days": "vor {{count}} Tag(en)",
"admin_pads.relative.hours": "vor {{count}} Stunde(n)",
"admin_pads.relative.just_now": "Soeben",
"admin_pads.relative.minutes": "vor {{count}} Minute(n)",
"admin_pads.relative.months": "vor {{count}} Monat(en)",
"admin_pads.relative.weeks": "vor {{count}} Woche(n)",
"admin_pads.relative.years": "vor {{count}} Jahr(en)",
"admin_pads.revisions_count": "{{count}} Bearbeitungen",
"admin_pads.selected_count": "{{count}} ausgewählt",
"admin_pads.show": "Anzeigen",
"admin_pads.sort.name": "Name (AZ)",
"admin_pads.sort.revision_number": "Bearbeitungen",
"admin_pads.sort.user_count": "Nutzer",
"admin_pads.stats.across_pads": "über alle Pads",
"admin_pads.stats.active_users": "Aktive Nutzer",
"admin_pads.stats.empty_pads": "Leere Pads",
"admin_pads.stats.last_activity": "Letzte Aktivität",
"admin_pads.stats.no_active_users": "Keine aktiven Benutzer",
"admin_pads.stats.revisions_zero": "0 Bearbeitungen",
"admin_pads.stats.total": "Gesamtanzahl Pads",
"admin_pads.stats.users_active": "{{count}} aktuell aktiv",
"admin_pads.subtitle": "Überblick über alle Pads auf dieser Etherpad-Instanz. Suchen, aufräumen, öffnen.",
"admin_plugins": "Pluginverwaltung",
"admin_plugins.available": "Verfügbare Plugins",
"admin_plugins.available_not-found": "Keine Plugins gefunden.",
"admin_plugins.available_fetching": "Wird abgerufen...",
"admin_plugins.available_install.value": "Installieren",
"admin_plugins.available_search.placeholder": "Suche nach Plugins zum Installieren",
"admin_plugins.check_updates": "Nach Updates suchen",
"admin_plugins.core_count": "{{count}} Kern(e)",
"admin_plugins.catalog_disabled": "Plugin-Katalog wurde vom Betreiber deaktiviert (privacy.pluginCatalog=false). Um ein Plugin zu installieren, führen Sie `pnpm run plugins i ep_<name>` auf dem Server aus.",
"admin_plugins.crumbs": "Plugins",
"admin_plugins.description": "Beschreibung",
"admin_plugins.disables.label": "Deaktiviert:",
"admin_plugins.disables.warning_title": "Dieses Plugin entfernt absichtlich die aufgeführten Etherpad-Funktionen.",
"admin_plugins.error_retrieving": "Fehler beim Abrufen von Plugins",
"admin_plugins.install_error": "Installation von {{plugin}} fehlgeschlagen: {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Installation von {{plugin}} nicht möglich: Hierfür ist neuere Version von Etherpad notwendig. Bitte führe ein Upgrade von Etherpad durch und versuche es erneut.",
"admin_plugins.installed": "Installierte Plugins",
"admin_plugins.installed_fetching": "Rufe installierte Plugins ab...",
"admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.",
@ -36,23 +104,67 @@
"admin_plugins.last-update": "Letze Aktualisierung",
"admin_plugins.name": "Name",
"admin_plugins.page-title": "Plugin Manager - Etherpad",
"admin_plugins.reload_catalog": "Katalog neu laden",
"admin_plugins.search_npm": "Suche auf npm",
"admin_plugins.sort_ascending": "Aufsteigend sortieren",
"admin_plugins.sort_descending": "Absteigend sortieren",
"admin_plugins.sort.last_updated": "Zuletzt aktualisiert",
"admin_plugins.sort.name": "Name (AZ)",
"admin_plugins.sort.version": "Version",
"admin_plugins.source": "Plugin-Quelle",
"admin_plugins.subtitle": "Installieren, aktualisieren und entfernen Sie Etherpad-Plugins. Änderungen erfordern einen Server-Neustart.",
"admin_plugins.tag_core": "Kern",
"admin_plugins.update_tooltip": "Update",
"admin_plugins.updates_available": "Updates verfügbar",
"admin_plugins.update_now": "Update",
"admin_plugins.version": "Version",
"admin_plugins_info": "Hilfestellung",
"admin_plugins_info.copy_diagnostics": "Kopiere Diagnose-Daten",
"admin_plugins_info.copy_value": "Kopiere {{label}}",
"admin_plugins_info.git_sha": "Git SHA",
"admin_plugins_info.hook_bindings": "Hook-Bindungen",
"admin_plugins_info.hooks": "Installierte Hooks",
"admin_plugins_info.hooks_client": "Client-seitige Hooks",
"admin_plugins_info.hooks_server": "Server-seitige Hooks",
"admin_plugins_info.no_hooks": "Keine Hooks gefunden",
"admin_plugins_info.parts": "Installierte Teile",
"admin_plugins_info.plugins": "Installierte Plugins",
"admin_plugins_info.page-title": "Plugin Informationen - Etherpad",
"admin_plugins_info.search_placeholder": "Suche Hook oder Teil…",
"admin_plugins_info.subtitle": "Systemdiagnose: installierte Version, registrierte Teile und Hooks.",
"admin_plugins_info.tab_client": "Client",
"admin_plugins_info.tab_server": "Server",
"admin_plugins_info.up_to_date": "Aktuell",
"admin_plugins_info.update_available": "Update verfügbar: {{version}}",
"admin_plugins_info.version": "Etherpad Version",
"admin_plugins_info.version_latest": "Neueste verfügbare Version",
"admin_plugins_info.version_number": "Versionsnummer",
"admin_settings": "Einstellungen",
"admin_settings.create_pad": "Pad erstellen",
"admin_settings.current": "Derzeitige Konfiguration",
"admin_settings.current_example-devel": "Beispielhafte Entwicklungseinstellungs-Templates",
"admin_settings.current_example-prod": "Beispiel eines produktiven Templates",
"admin_settings.current_restart.value": "Etherpad neustarten",
"admin_settings.current_save.value": "Einstellungen speichern",
"admin_settings.invalid_json": "Ungültiges JSON",
"admin_settings.current_test.value": "Validiere JSON",
"admin_settings.current_prettify.value": "Prettify JSON",
"admin_settings.toast.saved": "Einstellungen erfolgreich gespeichert.",
"admin_settings.toast.save_failed": "Speichern fehlgeschlagen: settings.json konnte nicht geschrieben werden.",
"admin_settings.toast.json_invalid": "Syntaxfehler: Überprüfen Sie Kommas, Klammern und Anführungszeichen.",
"admin_settings.toast.disconnected": "Nicht gespeichert: Nicht mit dem Server verbunden.",
"admin_settings.toast.validation_ok": "JSON ist gültig.",
"admin_settings.toast.validation_failed": "JSON ist ungültig: Bitte beheben Sie Syntaxfehler.",
"admin_settings.toast.prettify_failed": "Prettify nicht möglich: Bitte beheben Sie zunächst Syntaxfehler.",
"admin_settings.prettify_confirm": "Prettify entfernt alle Kommentare. Weiter?",
"admin_settings.mode.form": "Formular",
"admin_settings.mode.effective_tooltip": "Read-only-Ansicht der Werte, die Etherpad tatsächlich verwendet, nach Umgebungsvariablen-Ersetzung. Secrets sind geschwärzt.",
"admin_settings.mode.aria_label": "Editor-Modus",
"admin_settings.envvar_banner.title": "Diese Datei ist eine Vorlage, nicht die Live-Konfiguration.",
"admin_settings.envvar_banner.body": "Platzhalter wie ${VAR:default} werden beim Start in den Speicher eingesetzt; sie werden nie in diese Datei zurückgeschrieben. Bearbeiten Sie env vars in Ihrer Umgebung (Docker compose, systemd, .env), um den aufgelösten Wert zu ändern, oder ersetzen Sie den Platzhalter hier durch ein Literal. Wechseln Sie auf die Registerkarte \"Effective\", um zu sehen, was Etherpad gerade verwendet.",
"admin_settings.toast.auth_error": "Sie sind nicht als Administrator authentifiziert. Bitte melden Sie sich erneut an.",
"admin_settings.section.general": "Allgemein",
"admin_settings.parse_error.title": "Parsen von settings.json nicht möglich",
"admin_settings.page-title": "Einstellungen - Etherpad",
"index.newPad": "Neues Pad",
"index.settings": "Einstellungen",
@ -75,7 +187,7 @@
"index.labelPad": "Padname (optional)",
"index.placeholderPadEnter": "Gib den Namen des Pads ein...",
"index.createAndShareDocuments": "Erstelle und teile Dokumente in Echtzeit",
"index.createAndShareDocumentsDescription": "Etherpad ermöglicht die gemeinsame Bearbeitung von Dokumenten in Echtzeit, ähnlich wie ein Live-Multiplayer-Editor, der in Ihrem Browser läuft.",
"index.createAndShareDocumentsDescription": "Etherpad ermöglicht die gemeinsame Bearbeitung von Dokumenten in Echtzeit, ähnlich wie ein Live-Multiplayer-Editor, der in Deinem Browser läuft.",
"pad.toolbar.bold.title": "Fett (Strg-B)",
"pad.toolbar.italic.title": "Kursiv (Strg-I)",
"pad.toolbar.underline.title": "Unterstrichen (Strg-U)",
@ -99,18 +211,31 @@
"pad.loading": "Laden …",
"pad.noCookie": "Das Cookie konnte nicht gefunden werden. Bitte erlaube Cookies in deinem Browser! Deine Sitzung und Einstellungen werden zwischen den Besuchen nicht gespeichert. Dies kann darauf zurückzuführen sein, dass Etherpad in einigen Browsern in einem iFrame enthalten ist. Bitte stelle sicher, dass sich Etherpad auf der gleichen Subdomain/Domain wie der übergeordnete iFrame befindet.",
"pad.permissionDenied": "Du hast keine Berechtigung, um auf dieses Pad zuzugreifen.",
"pad.settings.padSettings": "Pad-Einstellungen",
"pad.settings.title": "Einstellungen",
"pad.settings.padSettings": "Pad-weite Einstellungen",
"pad.settings.userSettings": "Nutzereinstellungen",
"pad.settings.myView": "Eigene Ansicht",
"pad.settings.disablechat": "Chat deaktivieren",
"pad.settings.darkMode": "Dunkler Modus",
"pad.settings.stickychat": "Chat immer anzeigen",
"pad.settings.chatandusers": "Chat und Benutzer anzeigen",
"pad.settings.colorcheck": "Autorenfarben anzeigen",
"pad.settings.fadeInactiveAuthorColors": "Farben inaktiver Autoren ausblenden",
"pad.settings.linenocheck": "Zeilennummern",
"pad.settings.rtlcheck": "Inhalt von rechts nach links lesen?",
"pad.settings.enforcedNotice": "Diese Einstellungen wurden vom Ersteller des Pads gesperrt. Wenden Sie sich an den Ersteller, wenn Sie diese ändern möchten.",
"pad.settings.fontType": "Schriftart:",
"pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Sprache:",
"pad.settings.deletePad": "Pad löschen",
"pad.delete.confirm": "Möchtest du dieses Pad wirklich löschen?",
"pad.deletionToken.modalTitle": "Speichere deinen Pad-Löschtoken.",
"pad.deletionToken.modalBody": "Dieses Token ist die einzige Möglichkeit, dieses Pad zu löschen, falls Deine Browsersitzung unterbrochen wird oder Du das Gerät wechselst. Speichere es an einem sicheren Ort es wird hier nur einmal angezeigt.",
"pad.deletionToken.deleteWithToken": "Pad mit Token löschen",
"pad.deletionToken.tokenFieldLabel": "Pad-Löschtoken",
"pad.deletionToken.tokenValueLabel": "Dein Pad-Löschtoken (schreibgeschützt)",
"pad.deletionToken.invalid": "Das angegebene Token ist für dieses Pad nicht gültig.",
"pad.deletionToken.notCreator": "Sie sind nicht der Ersteller dieses Pads, daher können Sie es nicht löschen.",
"pad.settings.about": "Über",
"pad.settings.poweredBy": "Betrieben von",
"pad.importExport.import_export": "Import/Export",
@ -123,7 +248,13 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.exportetherpada.title": "Export im Etherpad-Format",
"pad.importExport.exporthtmla.title": "Exportieren als HTML",
"pad.importExport.exportplaina.title": "Export als einfacher Text",
"pad.importExport.exportworda.title": "Exportieren als Microsoft Word",
"pad.importExport.exportpdfa.title": "Als PDF exportieren",
"pad.importExport.exportopena.title": "Export als ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "Du kannst nur aus reinen Text- oder HTML-Formaten importieren. Für umfangreichere Importfunktionen <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">muss AbiWord oder LibreOffice auf dem Server installiert werden</a>.",
"pad.modals.connected": "Verbunden.",
"pad.modals.reconnecting": "Dein Pad wird neu verbunden...",
"pad.modals.forcereconnect": "Erneutes Verbinden erzwingen",
@ -151,9 +282,11 @@
"pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.",
"pad.modals.rejected.explanation": "Der Server hat eine Nachricht abgelehnt, die von deinem Browser gesendet wurde.",
"pad.modals.rejected.cause": "Möglicherweise wurde der Server aktualisiert, während du das Pad angesehen hast, oder es existiert ein Fehler in Etherpad. Versuche, die Seite neu zu laden.",
"pad.modals.disconnected": "Ihre Verbindung wurde getrennt.",
"pad.modals.disconnected": "Deine Verbindung wurde getrennt.",
"pad.modals.disconnected.explanation": "Die Verbindung zum Server wurde unterbrochen.",
"pad.modals.disconnected.cause": "Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtige den Dienstadministrator, falls dies weiterhin passiert.",
"pad.gritter.unacceptedCommit.title": "Nicht gespeicherte Bearbeitung",
"pad.gritter.unacceptedCommit.text": "Deine letzte Änderung wurde noch nicht gespeichert. Stelle die Verbindung wieder her und versuche es erneut.",
"pad.share": "Dieses Pad teilen",
"pad.share.readonly": "Eingeschränkter Nur-Lese-Zugriff",
"pad.share.link": "Verknüpfung",
@ -166,12 +299,29 @@
"timeslider.followContents": "Aktualisierungen des Pad-Inhalts verfolgen",
"timeslider.pageTitle": "{{appTitle}} Bearbeitungsverlauf",
"timeslider.toolbar.returnbutton": "Zurück zum Pad",
"pad.historyMode.banner": "Versionsgeschichte ansehen",
"pad.historyMode.return": "Zurück zum Live-Modus",
"pad.historyMode.revisionLabel": "Revision {{rev}}",
"pad.historyMode.controlsLabel": "Steuerung Pad-Verlauf",
"pad.historyMode.sliderLabel": "Pad-Version",
"pad.historyMode.settings.title": "Wiedergabe des Bearbeitungsverlaufs",
"pad.historyMode.settings.follow": "Aktualisierungen des Pad-Inhalts verfolgen",
"pad.historyMode.settings.followShort": "Folgen",
"pad.historyMode.followOn": "Pad-Änderungen verfolgen klicken Sie hier, um die Verfolgung zu beenden.",
"pad.historyMode.followOff": "Änderungen der Pads werden nicht verfolgt zum Folgen klicken",
"pad.historyMode.settings.playbackSpeed": "Wiedergabegeschwindigkeit:",
"pad.historyMode.chat.replayHeader": "Chat mit Stand von {{time}}",
"pad.historyMode.users.authorsHeader": "Autoren bei dieser Revision",
"pad.editor.keyboardHint": "Drücken Sie die Escape-Taste, um den Editor zu verlassen. Drücken Sie Alt+F9, um die Symbolleiste aufzurufen.",
"pad.editor.toolbar.formatting": "Formatierungsleiste",
"pad.editor.toolbar.showMore": "Weitere Schaltflächen anzeigen",
"timeslider.toolbar.authors": "Autoren:",
"timeslider.toolbar.authorsList": "Keine Autoren",
"timeslider.toolbar.exportlink.title": "Diese Version exportieren",
"timeslider.exportCurrent": "Exportiere diese Version als:",
"timeslider.version": "Version {{version}}",
"timeslider.saved": "Gespeichert am {{day}}. {{month}} {{year}}",
"timeslider.settings.playbackSpeed": "Wiedergabegeschwindigkeit:",
"timeslider.playPause": "Padbearbeitung abspielen/pausieren",
"timeslider.backRevision": "Eine Version in diesem Pad zurückgehen",
"timeslider.forwardRevision": "Eine Version in diesem Pad vorwärtsgehen",
@ -193,6 +343,7 @@
"pad.savedrevs.timeslider": "Du kannst gespeicherte Versionen durch den Aufruf des Bearbeitungsverlaufs ansehen.",
"pad.userlist.entername": "Dein Name?",
"pad.userlist.unnamed": "unbenannt",
"pad.userlist.onlineCount": "{[ plural(count) one: {{count}} verbundener Benutzer, other: {{count}} verbundene Benutzer ]}",
"pad.editbar.clearcolors": "Autorenfarben im gesamten Dokument zurücksetzen? Dies kann nicht rückgängig gemacht werden",
"pad.impexp.importbutton": "Jetzt importieren",
"pad.impexp.importing": "Importiere …",
@ -203,5 +354,6 @@
"pad.impexp.importfailed": "Import fehlgeschlagen",
"pad.impexp.copypaste": "Bitte kopieren und einfügen",
"pad.impexp.exportdisabled": "Der Export im {{type}}-Format ist deaktiviert. Für Einzelheiten kontaktiere bitte deinen Systemadministrator.",
"pad.impexp.maxFileSize": "Die Datei ist zu groß. Kontaktiere bitte deinen Administrator, um das Limit für den Dateiimport zu erhöhen."
"pad.impexp.maxFileSize": "Die Datei ist zu groß. Kontaktiere bitte deinen Administrator, um das Limit für den Dateiimport zu erhöhen.",
"pad.social.description": "Ein kollaboratives Dokument, das jeder in Echtzeit bearbeiten kann."
}

View file

@ -314,7 +314,7 @@
"pad.importExport.exportworda.title": "Export as Microsoft Word",
"pad.importExport.exportpdfa.title": "Export as PDF",
"pad.importExport.exportopena.title": "Export as ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "You can only import from plain text or HTML formats. For more advanced import features, please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install LibreOffice</a>.",
"pad.importExport.noConverter.innerHTML": "You can import plain text, HTML, Microsoft Word (.docx) and Etherpad files directly. To import other formats such as PDF, ODT, DOC or RTF, the server administrator needs to install LibreOffice — see the <a href=\"https://docs.etherpad.org/\">Etherpad documentation</a>.",
"pad.modals.connected": "Connected.",
"pad.modals.reconnecting": "Reconnecting to your pad…",

View file

@ -23,6 +23,27 @@
]
},
"admin.page-title": "Ylläpitäjän kojelauta - Etherpad",
"admin.loading": "Ladataan…",
"admin.loading_description": "Odota. Sivu latautuu.",
"admin.toggle_sidebar": "Näytä/piilota sivupalkki",
"admin.shout": "Viestintä",
"admin_shout.online_one": "Tällä hetkellä {{count}} käyttäjä on paikalla",
"admin_shout.online_other": "Tällä hetkellä {{count}} käyttäjää on paikalla",
"admin_shout.sticky_toggle": "Muuta pysyvää viestiä",
"admin_login.title": "Etherpad",
"admin_login.username": "Käyttäjänimi",
"admin_login.password": "Salasana",
"admin_login.submit": "Kirjaudu sisään",
"admin_login.failed": "Kirjautuminen epäonnistui",
"admin_pads.all_pads": "Kaikki muistiot",
"admin_pads.bulk.cleanup_history": "Tyhjennä historia",
"admin_pads.bulk.delete": "Poista",
"admin_pads.cancel": "Peru",
"admin_pads.col.pad": "Muistio",
"admin_pads.col.revisions": "Muokkaushistoria",
"admin_pads.col.users": "Käyttäjät",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Valitse nimi uudelle muistiolle.",
"admin_plugins": "Lisäosien hallinta",
"admin_plugins.available": "Saatavilla olevat liitännäiset",
"admin_plugins.available_not-found": "Lisäosia ei löytynyt.",

View file

@ -7,9 +7,11 @@
"Chpol",
"Cquoi",
"Crochet.david",
"Crowwhailord",
"Derugon",
"Envlh",
"Framafan",
"Framasky",
"Fylip22",
"Gomoko",
"Goofy",
@ -37,13 +39,73 @@
]
},
"admin.page-title": "Tableau de bord administrateur — Etherpad",
"admin.loading": "Chargement en cours…",
"admin.loading_description": "Veuillez patienter pendant le chargement de la page.",
"admin.toggle_sidebar": "Afficher/masquer la barre latérale",
"admin.shout": "Communication",
"admin_shout.online_one": "Il y a actuellement {{count}} utilisateur·ice en ligne",
"admin_shout.online_other": "Il y a actuellement {{count}} utilisateur·ices en ligne",
"admin_shout.sticky_toggle": "Modifier le message épinglé",
"admin_login.title": "Etherpad",
"admin_login.username": "Nom dutilisateur",
"admin_login.password": "Mot de passe",
"admin_login.submit": "Connexion",
"admin_login.failed": "Échec de la connexion",
"admin_pads.all_pads": "Tous les blocs-notes",
"admin_pads.bulk.cleanup_history": "Effacer lhistorique",
"admin_pads.bulk.clear_selection": "Effacer la sélection",
"admin_pads.bulk.delete": "Supprimer",
"admin_pads.cancel": "Annuler",
"admin_pads.col.pad": "Bloc-notes",
"admin_pads.col.revisions": "Révisions",
"admin_pads.col.users": "Utilisateur·ices",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Choisissez un nom pour le nouveau bloc-notes.",
"admin_pads.delete_pad_dialog_description": "Confirmer ou annuler la suppression du bloc-notes.",
"admin_pads.delete_pad_dialog_title": "Supprimer le bloc-notes",
"admin_pads.empty_never_edited": "vide · jamais modifié",
"admin_pads.error_dialog_description": "Une erreur sest produite.",
"admin_pads.error_prefix": "Erreur",
"admin_pads.filter.active": "Actif",
"admin_pads.filter.all": "Tous",
"admin_pads.filter.empty": "Vide",
"admin_pads.filter.recent": "Cette semaine",
"admin_pads.filter.stale": "Sans modifications (> 1 an)",
"admin_pads.open": "Ouvert",
"admin_pads.pagination.next": "Suivant",
"admin_pads.pagination.previous": "Précédent",
"admin_pads.refresh": "Actualiser",
"admin_pads.relative.days": "il y a {{count}} jours",
"admin_pads.relative.hours": "il y a {{count}} heures",
"admin_pads.relative.just_now": "à linstant",
"admin_pads.show": "Afficher",
"admin_pads.sort.name": "Nom (AZ)",
"admin_pads.sort.revision_number": "Révisions",
"admin_pads.sort.user_count": "Utilisateur·ices",
"admin_pads.stats.across_pads": "sur tous les bloc-notes",
"admin_pads.stats.active_users": "Utilisateur·ices actif·ves",
"admin_pads.stats.empty_pads": "Bloc-notes vides",
"admin_pads.stats.last_activity": "Dernière activité :",
"admin_pads.stats.no_active_users": "Aucun·e utilisateur·ice actif·ve",
"admin_pads.stats.revisions_zero": "0 révisions",
"admin_pads.stats.total": "Nombre total de bloc-notes",
"admin_pads.stats.users_active": "{{count}} actuellement actifs",
"admin_pads.subtitle": "Aperçu de tous les bloc-notes de cette instance Etherpad. Rechercher, nettoyer, ouvrir.",
"admin_plugins": "Gestionnaire de greffons",
"admin_plugins.available": "Greffons disponibles",
"admin_plugins.available_not-found": "Aucun greffon trouvé.",
"admin_plugins.available_fetching": "Récupération en cours...",
"admin_plugins.available_install.value": "Installer",
"admin_plugins.available_search.placeholder": "Rechercher des greffons à installer",
"admin_plugins.check_updates": "Vérifier les mises à jour",
"admin_plugins.catalog_disabled": "Le catalogue de greffons est désactivé par votre administrateur (privacy.pluginCatalog=false). Pour installer un greffon, exécutez `pnpm run plugins i ep_<name> ` sur le serveur.",
"admin_plugins.crumbs": "Greffons",
"admin_plugins.description": "Description",
"admin_plugins.disables.label": "Désactive :",
"admin_plugins.disables.warning_title": "Ce greffon supprime intentionnellement les fonctionnalités Etherpad listées.",
"admin_plugins.error_retrieving": "Erreur lors de la récupération des greffons",
"admin_plugins.install_error": "Échec de l'installation de {{plugin}} : {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Impossible d'installer {{plugin}} : une version plus récente d'Etherpad est requise. Veuillez mettre à jour Etherpad et réessayer.",
"admin_plugins.installed": "Greffons installés",
"admin_plugins.installed_fetching": "Récupération des greffons installés en cours...",
"admin_plugins.installed_nothing": "Vous navez encore installé aucun greffon.",
@ -51,8 +113,25 @@
"admin_plugins.last-update": "Dernière mise à jour",
"admin_plugins.name": "Nom",
"admin_plugins.page-title": "Gestionnaire de greffons — Etherpad",
"admin_plugins.reload_catalog": "Recharger le catalogue",
"admin_plugins.search_npm": "Rechercher sur npm",
"admin_plugins.sort_ascending": "Tri croissant",
"admin_plugins.sort_descending": "Tri décroissant",
"admin_plugins.sort.last_updated": "Dernière mise à jour",
"admin_plugins.sort.name": "Nom (AZ)",
"admin_plugins.sort.version": "Version",
"admin_plugins.source": "Source du greffon",
"admin_plugins.subtitle": "Installez, mettez à jour et supprimez les greffons dEtherpad. Toute modification nécessite un redémarrage du serveur.",
"admin_plugins.tag_core": "Cœur",
"admin_plugins.update_tooltip": "Mise à jour",
"admin_plugins.updates_available": "Mises à jour disponibles",
"admin_plugins.update_now": "Mettre à jour",
"admin_plugins.version": "Version",
"admin_plugins_info": "Informations de résolution de problème",
"admin_plugins_info.bindings_label": "{{count}} liaisons",
"admin_plugins_info.copy_diagnostics": "Copie des diagnostics",
"admin_plugins_info.copy_value": "Copier {{label}}",
"admin_plugins_info.git_sha": "Git SHA",
"admin_plugins_info.hooks": "Crochets installés",
"admin_plugins_info.hooks_client": "Crochets côté client",
"admin_plugins_info.hooks_server": "Crochets côté serveur",
@ -156,6 +235,16 @@
"pad.settings.language": "Langue:",
"pad.settings.deletePad": "Supprimer le bloc-notes",
"pad.delete.confirm": "Voulez-vous vraiment supprimer ce bloc-notes ?",
"pad.deletionToken.modalTitle": "Enregistrez votre jeton de suppression de bloc-notes",
"pad.deletionToken.modalBody": "Ce jeton est le seul moyen de supprimer ce bloc-notes si vous perdez votre session de navigateur ou si vous changez d'appareil. Conservez-le en lieu sûr — il n'apparaît ici qu'une seule fois.",
"pad.deletionToken.copy": "Copier",
"pad.deletionToken.copied": "Copié",
"pad.deletionToken.acknowledge": "Je l'ai enregistré",
"pad.deletionToken.deleteWithToken": "Supprimer le bloc-notes avec un jeton",
"pad.deletionToken.tokenFieldLabel": "Jeton de suppression de bloc-notes",
"pad.deletionToken.tokenValueLabel": "Votre jeton de suppression du bloc-notes (lecture seule)",
"pad.deletionToken.invalid": "Ce jeton n'est pas valable pour ce bloc-notes.",
"pad.deletionToken.notCreator": "Vous nêtes pas le créateur du bloc-notes, vous ne pouvez donc pas le supprimer.",
"pad.settings.about": "À propos",
"pad.settings.poweredBy": "Propulsé par",
"pad.importExport.import_export": "Importer/Exporter",
@ -168,7 +257,10 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "Vous pouvez uniquement importer du texte brut ou du HTML. Pour des fonctionnalités d'importation plus avancées, veuillez <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installer LibreOffice</a>.",
"pad.importExport.exportetherpada.title": "Exporter au format Etherpad",
"pad.importExport.exporthtmla.title": "Exporter au format HTML",
"pad.importExport.exportplaina.title": "Exporter en texte brut",
"pad.importExport.noConverter.innerHTML": "Vous pouvez importer directement du texte brut, du HTML,Microsoft Word (.docx) et des fichiers Etherpad. Pour importer d'autres formats tels que PDF, ODT, DOC ou RTF, l'administrateur du serveur doit installer LibreOffice ; consultez la <a href=\"https://docs.etherpad.org/\">documentation Etherpad</a> .",
"pad.modals.connected": "Connecté.",
"pad.modals.reconnecting": "Reconnexion à votre bloc-notes en cours...",
"pad.modals.forcereconnect": "Forcer la reconnexion",

View file

@ -302,7 +302,7 @@
"pad.importExport.exportworda.title": "Esporta come Microsoft Word",
"pad.importExport.exportpdfa.title": "Esporta in formato PDF",
"pad.importExport.exportopena.title": "Esporta in formato ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "È possibile importare solo file di testo semplice o in formato HTML. Per funzionalità di importazione più avanzate, si prega <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">di installare LibreOffice</a> .",
"pad.importExport.noConverter.innerHTML": "È possibile importare direttamente file di testo semplice, HTML, Microsoft Word (.docx) e file Etherpad. Per importare altri formati come PDF, ODT, DOC o RTF, l'amministratore del server deve installare LibreOffice: consulta la <a href=\"https://docs.etherpad.org/\">documentazione di Etherpad</a>.",
"pad.modals.connected": "Connesso.",
"pad.modals.reconnecting": "Riconnessione al pad in corso…",
"pad.modals.forcereconnect": "Forza la riconnessione",

View file

@ -6,6 +6,7 @@
"Chqaz",
"Omotecho",
"Shirayuki",
"Tensama0415",
"Torinky"
]
},
@ -30,7 +31,7 @@
"admin_plugins_info.hooks_client": "クライアント側のフック",
"admin_plugins_info.hooks_server": "サーバー側のフック",
"index.newPad": "新規作成",
"index.createOpenPad": "または作成/編集するパッド名を入力:",
"index.createOpenPad": "編集するパッド名を入力",
"index.openPad": "次の名称の既存の Pad を開く:",
"pad.toolbar.bold.title": "太字 (Ctrl+B)",
"pad.toolbar.italic.title": "斜体 (Ctrl+I)",
@ -39,7 +40,7 @@
"pad.toolbar.ol.title": "番号付きリスト (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "番号なしリスト (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "インデント (Tab)",
"pad.toolbar.unindent.title": "インデント解除 (Shift+Tab)",
"pad.toolbar.unindent.title": "アウトデント (Shift+Tab)",
"pad.toolbar.undo.title": "元に戻す (Ctrl+Z)",
"pad.toolbar.redo.title": "やり直し (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "作者の色分けを消去(Ctrl+Shift+C)",
@ -54,7 +55,7 @@
"pad.loading": "読み込み中...",
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. \n\nクッキーが見つかりません。ブラウザの設定でクッキーの使用を許可するまで、アクセスの記録や設定は引き継がれません。原因はブラウザによって Etherpad が iFrame に組み込まれたからと考えられます。親ドメインの iFrame と同じドメイン/サブドメインに置かれているかどうか、Etherpad の設定を確認してください。",
"pad.permissionDenied": "あなたにはこのパッドへのアクセス許可がありません",
"pad.settings.padSettings": "パッドの設定",
"pad.settings.padSettings": "パッドの設定",
"pad.settings.myView": "個人設定",
"pad.settings.stickychat": "画面にチャットを常に表示",
"pad.settings.chatandusers": "チャットとユーザーを表示",
@ -77,7 +78,7 @@
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.modals.connected": "接続されました。",
"pad.modals.reconnecting": "パッドに再接続中...",
"pad.modals.reconnecting": "パッドに再接続中",
"pad.modals.forcereconnect": "強制的に再接続",
"pad.modals.reconnecttimer": "再接続を試行中",
"pad.modals.cancel": "中止",

View file

@ -145,14 +145,14 @@
"admin_settings.current_save.value": "설정 저장",
"admin_settings.invalid_json": "잘못된 JSON",
"admin_settings.current_test.value": "JSON 검증",
"admin_settings.current_prettify.value": "JSON 정리",
"admin_settings.current_prettify.value": "JSON를 보기 좋게 정리",
"admin_settings.toast.saved": "설정을 성공적으로 저장했습니다.",
"admin_settings.toast.save_failed": "저장 실패: settings.json에 쓰기 할 수 없습니다.",
"admin_settings.toast.json_invalid": "구문 오류: 쉼표, 중괄호, 따옴표를 확인하세요.",
"admin_settings.toast.disconnected": "저장할 수 없음: 서버에 연결되어 있지 않습니다.",
"admin_settings.toast.validation_ok": "JSON이 유효합니다.",
"admin_settings.toast.validation_failed": "JSON이 유효하지 않습니다. 구문 오류를 수정하세요.",
"admin_settings.toast.prettify_failed": "정리할 수 없습니다. 먼저 구문 오류를 수정하세요.",
"admin_settings.toast.prettify_failed": "보기 좋게 정리할 수 없습니다. 먼저 구문 오류를 수정하세요.",
"admin_settings.prettify_confirm": "정리하면 모든 의견이 제거됩니다. 계속하시겠습니까?",
"admin_settings.mode.form": "형태",
"admin_settings.mode.raw": "원본",
@ -325,7 +325,7 @@
"pad.importExport.exportworda.title": "Microsoft Word 파일로 내보내기",
"pad.importExport.exportpdfa.title": "PDF로 내보내기",
"pad.importExport.exportopena.title": "ODF(Open Document Format) 형식으로 내보내기",
"pad.importExport.noConverter.innerHTML": "일반 텍스트나 HTML 형식으로만 가져올 수 있습니다. 고급 가져오기 기능에 대해서는 <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">리브레오피스를 설치</a>하세요.",
"pad.importExport.noConverter.innerHTML": "일반 텍스트, HTML, 마이크로소프트 워드 (.docx) 또는 이더패드 파일을 직접 가져올 수 있습니다. PDF, ODT, DOC 또는 RTF와 같은 다른 형식을 가져오려면 서버 관리자가 리브레오피스를 설치해야 합니다. 자세한 내용은 <a href=\"https://docs.etherpad.org/\">이더패드 설명서</a>를 참고하세요.",
"pad.modals.connected": "연결함.",
"pad.modals.reconnecting": "내 패드에 다시 연결하는 중...",
"pad.modals.forcereconnect": "강제로 다시 연결",

View file

@ -187,6 +187,39 @@
"update.page.policy.rollback-failed-terminal": "Претходната поднова не успеа и не можеше да се отповика. Стиснете на „Прифати“ откако воспоставката ќе стане задрава за да отклучите.",
"update.page.policy.up-to-date": "Ја користите најновата верзија.",
"update.page.policy.tier-off": "Подновие се оневозможени (updates.tier = „off“).",
"update.page.policy.maintenance-window-missing": "Ниво 4 (автономно) бара период на одржување. Задајте го updates.maintenanceWindow во settings.json за да овозможите автономни поднови.",
"update.page.policy.maintenance-window-invalid": "Ниво 4 (автономно) е оневозможено бидејќи updates.maintenanceWindow е погрешно срочен. Се очекуваше {start, end, tz} со времиња ЧЧ:ММ и tz да биде „local“ или „utc“.",
"update.page.last_result.verified": "Потврдена последната поднова на {{tag}}.",
"update.page.last_result.rolled-back": "Отповикан последниот обид за поднова на {{tag}}: {{reason}}.",
"update.page.last_result.rollback-failed": "Последниот обид за поднова не успеа И отповикувањето не успеа: {{reason}}. Потребна е рачна интервенција.",
"update.page.last_result.preflight-failed": "Не успеа последниот обид за поднова на {{tag}} во подготовката: {{reason}}.",
"update.page.last_result.cancelled": "Последниот обид за поднова на {{tag}} е откажан од администратор.",
"update.execution.idle": "Неактивно",
"update.execution.scheduled": "Подновата е закажана",
"update.execution.preflight": "Подготвителни проверки",
"update.execution.preflight-failed": "Проверките не успеаја",
"update.execution.draining": "Исцрпувачки седници",
"update.execution.executing": "Подновувам...",
"update.execution.pending-verification": "Чека на потврда",
"update.execution.verified": "Потврдено",
"update.execution.rolling-back": "Отповикување",
"update.execution.rolled-back": "Отповикано",
"update.execution.rollback-failed": "Отповикувањето не успеа",
"update.banner.terminal.rollback-failed": "Не успеа обидот за поднова и не можеше да биде отповикан. Потребна е рачна интервенција.",
"update.banner.scheduled": "Автоподновата на {{tag}} е закажана — на сила за {{remaining}}.",
"update.banner.maintenance-window-missing": "Автономните поднови се оневозможени додека не се постави период на одржување.",
"update.banner.maintenance-window-invalid": "Автономните поднови се оневозможени бидејќи периодот на одржување е погрешно срочен.",
"update.page.scheduled.title": "Подновата е закажана",
"update.page.scheduled.countdown": "Etherpad ќе почне со подновување на {{tag}} за {{remaining}}.",
"update.page.scheduled.deferred_until": "Вон периодот на одржување. Подновата ќе почне кога периодот ќе започне во {{at}}.",
"update.page.scheduled.apply_now": "Примени сега",
"update.window.title": "Период на одржување",
"update.window.summary": "{{start}}{{end}} ({{tz}})",
"update.window.unset": "Не е наместено.",
"update.window.next_opens_at": "Следниот период почнува на {{at}}.",
"update.drain.t60": "Etherpad ќе се превклучи за 60 секунди за да направи примена и поднова.",
"update.drain.t30": "Etherpad ќе се превклучи за 30 секунди за да направи примена и поднова.",
"update.drain.t10": "Etherpad ќе се превклучи за 10 секунди за да направи примена и поднова.",
"index.newPad": "Нова тетратка",
"index.settings": "Нагодувања",
"index.transferSessionTitle": "Префрли седница",
@ -199,6 +232,7 @@
"index.copyLinkButton": "Копирај врска во меѓускладот",
"index.transferToSystem": "3. Копирај седница во нов систем",
"index.transferToSystemDescription": "Отворете ја ископираната врска во целниот прелистувач или уред за да ја префрлите вашата седница.",
"index.code": "Код",
"index.transferSessionDescription": "Префрлете ја вашата тековна седница на прелистувач или уред стискајќи на копчето подолу. Ова ќе ја прекопира врската во страница која ќе ви ја префрли седницата кога ќе се отвори во целниот прелистувач или уред.",
"index.createOpenPad": "Отвори тетратка по име",
"index.openPad": "отвори постоечка тетратка наречена:",
@ -273,6 +307,12 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.exportetherpada.title": "Извези како Etherpad",
"pad.importExport.exporthtmla.title": "Извези како HTML",
"pad.importExport.exportplaina.title": "Извези како прост текст",
"pad.importExport.exportworda.title": "Извези како Microsoft Word",
"pad.importExport.exportpdfa.title": "Извези како PDF",
"pad.importExport.exportopena.title": "Извези како ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "Можете да увезувате само од прост текст или HTML-форматти. Понапредни можности за увоз ќе добиете ако <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">го воспоставите LibreOffice</a>.",
"pad.modals.connected": "Поврзано.",
"pad.modals.reconnecting": "Ве преповрзувам со тетратката...",
@ -318,13 +358,31 @@
"timeslider.followContents": "Следи ги подновите во содржината на тетратката",
"timeslider.pageTitle": "{{appTitle}} Историски преглед",
"timeslider.toolbar.returnbutton": "Назад на тетратката",
"pad.historyMode.banner": "Историја на посети",
"pad.historyMode.return": "Назад во живо",
"pad.historyMode.revisionLabel": "Преработка {{rev}}",
"pad.historyMode.controlsLabel": "Контроли за историја на тетратката",
"pad.historyMode.sliderLabel": "Преработка на тетратката",
"pad.historyMode.settings.title": "Преглед на историјата",
"pad.historyMode.settings.follow": "Следи поднови на содржини тетратки",
"pad.historyMode.settings.followShort": "Следи",
"pad.historyMode.followOn": "Следите промени во тетратката — стиснете за да престанете со следење",
"pad.historyMode.followOff": "Не следите промени во тетратката — стиснете за да следите",
"pad.historyMode.settings.playbackSpeed": "Брзина на прегледувањето:",
"pad.historyMode.chat.replayHeader": "Разговор од {{time}}",
"pad.historyMode.users.authorsHeader": "Авроти на оваа преработка",
"pad.editor.skipToContent": "Прејди на уредувачот",
"pad.editor.keyboardHint": "Стиснете Escape за да излезете од уредувачот. Стиснете Alt+F9 за алатникот.",
"pad.editor.toolbar.formatting": "Алатник за форматирање",
"pad.editor.toolbar.actions": "Алатник за дејства врз тетратка",
"pad.editor.toolbar.showMore": "Покажи повеќе копчиња на алатникот",
"timeslider.toolbar.authors": "Автори:",
"timeslider.toolbar.authorsList": "Нема автори",
"timeslider.toolbar.exportlink.title": "Извоз",
"timeslider.exportCurrent": "Извези ја тековната верзија како:",
"timeslider.version": "Верзија {{version}}",
"timeslider.saved": "Зачувано на {{day}} {{month}} {{year}} г.",
"timeslider.settings.playbackSpeed": "Брзина на репродукција:",
"timeslider.settings.playbackSpeed": "Брзина на прегледување:",
"timeslider.settings.playbackSpeed.original": "Изворна брзина",
"timeslider.settings.playbackSpeed.realtime": "Реално време",
"timeslider.settings.playbackSpeed.200ms": "200 мс",
@ -351,6 +409,7 @@
"pad.savedrevs.timeslider": "Можете да ги погледате зачуваните преработки посетувајќи го времеследниот лизгач",
"pad.userlist.entername": "Внесете го вашето име",
"pad.userlist.unnamed": "без име",
"pad.userlist.onlineCount": "{[ plural(count) one: {{count}} поврзан корисник, other: {{count}} поврзани корисници ]}",
"pad.editbar.clearcolors": "Да ги отстранам авторските бои од целиот документ? Ова е неповратно",
"pad.impexp.importbutton": "Увези сега",
"pad.impexp.importing": "Увезувам...",

View file

@ -28,17 +28,17 @@
"pad.modals.cancel": "Xikxolewa",
"pad.modals.deleted": "Omopohpoloh.",
"pad.modals.deleted.explanation": "Ōmopoloh inīn Pad.",
"timeslider.version": "Inīc {{version}} Cuepaliztli",
"timeslider.month.january": "Eneroh",
"timeslider.month.february": "Febreroh",
"timeslider.month.march": "Marsoh",
"timeslider.month.april": "April",
"timeslider.month.may": "Mayoh",
"timeslider.month.june": "Honioh",
"timeslider.month.july": "Holioh",
"timeslider.month.august": "Ahostoh",
"timeslider.month.september": "Septiempreh",
"timeslider.month.october": "Oktopreh",
"timeslider.month.november": "Noviempreh",
"timeslider.month.december": "Tisiempreh"
"timeslider.version": "Inic {{version}} Tlacepaliztli",
"timeslider.month.january": "Enero",
"timeslider.month.february": "Febrero",
"timeslider.month.march": "Marzo",
"timeslider.month.april": "Abril",
"timeslider.month.may": "Mayo",
"timeslider.month.june": "Junio",
"timeslider.month.july": "Julio",
"timeslider.month.august": "Agosto",
"timeslider.month.september": "Septiembre",
"timeslider.month.october": "Octubre",
"timeslider.month.november": "Noviembre",
"timeslider.month.december": "Diciembre"
}

View file

@ -7,17 +7,85 @@
"Mark",
"Rudko",
"Teslaton",
"Wizzard",
"Yardom78"
]
},
"admin.page-title": "Ovládací panel správu - Etherpad",
"admin.loading": "Načítava sa…",
"admin.loading_description": "Počkajte, prosím, kým sa stránka načíta.",
"admin.toggle_sidebar": "Prepnúť bočný panel",
"admin.shout": "Komunikácia",
"admin_shout.online_one": "Momentálne je online {{count}} používateľ",
"admin_shout.online_other": "Momentálne je online {{count}} používateľov",
"admin_shout.sticky_toggle": "Zmeniť pripnutú správu",
"admin_login.title": "Etherpad",
"admin_login.username": "Používateľské meno",
"admin_login.password": "Heslo",
"admin_login.submit": "Prihlásiť sa",
"admin_login.failed": "Prihlásenie zlyhalo",
"admin_pads.all_pads": "Všetky pady",
"admin_pads.bulk.cleanup_history": "Vyčistiť históriu",
"admin_pads.bulk.clear_selection": "Zrušiť výber",
"admin_pads.bulk.delete": "Zmazať",
"admin_pads.cancel": "Zrušiť",
"admin_pads.col.pad": "Pad",
"admin_pads.col.revisions": "Revízie",
"admin_pads.col.users": "Používatelia",
"admin_pads.confirm_button": "OK",
"admin_pads.create_pad_dialog_description": "Zvoľte názov nového padu.",
"admin_pads.delete_pad_dialog_description": "Potvrďte alebo zrušte zmazanie padu.",
"admin_pads.delete_pad_dialog_title": "Zmazať pad",
"admin_pads.empty_never_edited": "prázdny · nikdy neupravovaný",
"admin_pads.error_dialog_description": "Vyskytla sa chyba.",
"admin_pads.error_prefix": "Chyba",
"admin_pads.filter.active": "Aktívne",
"admin_pads.filter.all": "Všetky",
"admin_pads.filter.empty": "Prázdne",
"admin_pads.filter.recent": "Tento týždeň",
"admin_pads.filter.stale": "Neaktívne (>1 r.)",
"admin_pads.open": "Otvoriť",
"admin_pads.pagination.next": "Ďalej",
"admin_pads.pagination.previous": "Späť",
"admin_pads.refresh": "Obnoviť",
"admin_pads.relative.days": "pred {{count}} d",
"admin_pads.relative.hours": "pred {{count}} h",
"admin_pads.relative.just_now": "práve teraz",
"admin_pads.relative.minutes": "pred {{count}} min",
"admin_pads.relative.months": "pred {{count}} mes.",
"admin_pads.relative.weeks": "pred {{count}} týž.",
"admin_pads.relative.years": "pred {{count}} r.",
"admin_pads.revisions_count": "{{count}} revízií",
"admin_pads.selected_count": "Vybraté: {{count}}",
"admin_pads.show": "Zobraziť",
"admin_pads.sort.name": "Názov (A Z)",
"admin_pads.sort.revision_number": "Revízie",
"admin_pads.sort.user_count": "Používatelia",
"admin_pads.stats.across_pads": "vo všetkých padoch",
"admin_pads.stats.active_users": "Aktívni používatelia",
"admin_pads.stats.empty_pads": "Prázdne pady",
"admin_pads.stats.last_activity": "Posledná aktivita",
"admin_pads.stats.no_active_users": "Žiadni aktívni používatelia",
"admin_pads.stats.revisions_zero": "0 revízií",
"admin_pads.stats.total": "Pady spolu",
"admin_pads.stats.users_active": "Momentálne aktívnych: {{count}}",
"admin_pads.subtitle": "Prehľad všetkých padov na tejto inštancii Etherpadu. Vyhľadávajte, čistite, otvárajte.",
"admin_plugins": "Správca doplnkov",
"admin_plugins.available": "Dostupné doplnky",
"admin_plugins.available_not-found": "Doplnky neboli nájdené.",
"admin_plugins.available_fetching": "Načítavanie...",
"admin_plugins.available_install.value": "Inštalovať",
"admin_plugins.available_search.placeholder": "Vyhľadať doplnky na inštaláciu",
"admin_plugins.check_updates": "Skontrolovať aktualizácie",
"admin_plugins.core_count": "{{count}} základných",
"admin_plugins.catalog_disabled": "Katalóg zásuvných modulov je vypnutý vaším prevádzkovateľom (privacy.pluginCatalog=false). Zásuvný modul nainštalujete spustením `pnpm run plugins i ep_<name>` na serveri.",
"admin_plugins.crumbs": "Zásuvné moduly",
"admin_plugins.description": "Popis",
"admin_plugins.disables.label": "Vypína:",
"admin_plugins.disables.warning_title": "Tento zásuvný modul zámerne odstraňuje uvedené funkcie Etherpadu.",
"admin_plugins.error_retrieving": "Chyba pri získavaní zásuvných modulov",
"admin_plugins.install_error": "Nepodarilo sa nainštalovať {{plugin}}: {{error}}",
"admin_plugins.install_error_requires_newer_etherpad": "Nemožno nainštalovať {{plugin}}: vyžaduje novšiu verziu Etherpadu. Aktualizujte Etherpad a skúste to znova.",
"admin_plugins.installed": "Nainštalované doplnky",
"admin_plugins.installed_fetching": "Načítavanie nainštalovaných doplnkov...",
"admin_plugins.installed_nothing": "Ešte ste nenainštalovali žiadne doplnky.",
@ -25,27 +93,161 @@
"admin_plugins.last-update": "Posledná aktualizácia",
"admin_plugins.name": "Názov",
"admin_plugins.page-title": "Správca doplnkov - Etherpad",
"admin_plugins.reload_catalog": "Znovu načítať katalóg",
"admin_plugins.search_npm": "Hľadať na npm",
"admin_plugins.sort_ascending": "Zoradiť vzostupne",
"admin_plugins.sort_descending": "Zoradiť zostupne",
"admin_plugins.sort.last_updated": "Naposledy aktualizované",
"admin_plugins.sort.name": "Názov (A Z)",
"admin_plugins.sort.version": "Verzia",
"admin_plugins.source": "Zdroj zásuvného modulu",
"admin_plugins.subtitle": "Inštalujte, aktualizujte a odstraňujte zásuvné moduly Etherpadu. Zmeny si vyžadujú reštart servera.",
"admin_plugins.tag_core": "Základné",
"admin_plugins.update_tooltip": "Aktualizovať",
"admin_plugins.updates_available": "Dostupné aktualizácie",
"admin_plugins.update_now": "Aktualizovať",
"admin_plugins.version": "Verzia",
"admin_plugins_info": "Informácie k riešeniu problémov",
"admin_plugins_info.bindings_label": "{{count}} väzieb",
"admin_plugins_info.copy_diagnostics": "Kopírovať diagnostiku",
"admin_plugins_info.copy_value": "Kopírovať {{label}}",
"admin_plugins_info.git_sha": "Git SHA",
"admin_plugins_info.hook_bindings": "Väzby hookov",
"admin_plugins_info.hooks": "Nainštalované súčasti",
"admin_plugins_info.hooks_client": "Súčasti na strane klienta",
"admin_plugins_info.hooks_server": "Súčasti na strane servera",
"admin_plugins_info.no_hooks": "Nenašli sa žiadne hooky",
"admin_plugins_info.parts": "Nainštalované súčasti",
"admin_plugins_info.plugins": "Nainštalované doplnky",
"admin_plugins_info.page-title": "Informácie o doplnkoch - Etherpad",
"admin_plugins_info.search_placeholder": "Hľadať hook alebo časť…",
"admin_plugins_info.subtitle": "Diagnostika systému: nainštalovaná verzia, registrované časti a hooky.",
"admin_plugins_info.tab_client": "Klient",
"admin_plugins_info.tab_server": "Server",
"admin_plugins_info.up_to_date": "Aktuálne",
"admin_plugins_info.update_available": "Dostupná aktualizácia: {{version}}",
"admin_plugins_info.version": "Verzia Etherpadu",
"admin_plugins_info.version_latest": "Posledná dostupná verzia",
"admin_plugins_info.version_number": "Číslo verzie",
"admin_settings": "Nastavenia",
"admin_settings.create_pad": "Vytvoriť pad",
"admin_settings.current": "Aktuálne nastavenia",
"admin_settings.current_example-devel": "Príklad šablóny vývojárskeho nastavenia",
"admin_settings.current_example-prod": "Príklad šablóny výrobného nastavenia",
"admin_settings.current_restart.value": "Reštartovať Etherpad",
"admin_settings.current_save.value": "Uložiť nastavenia",
"admin_settings.invalid_json": "Neplatný JSON",
"admin_settings.current_test.value": "Overiť JSON",
"admin_settings.current_prettify.value": "Sformátovať JSON",
"admin_settings.toast.saved": "Nastavenia boli úspešne uložené.",
"admin_settings.toast.save_failed": "Uloženie zlyhalo: súbor settings.json sa nepodarilo zapísať.",
"admin_settings.toast.json_invalid": "Syntaktická chyba: skontrolujte čiarky, zátvorky a úvodzovky.",
"admin_settings.toast.disconnected": "Nemožno uložiť: bez pripojenia k serveru.",
"admin_settings.toast.validation_ok": "JSON je platný.",
"admin_settings.toast.validation_failed": "JSON je neplatný: opravte syntaktické chyby.",
"admin_settings.toast.prettify_failed": "Nemožno sformátovať: najprv opravte syntaktické chyby.",
"admin_settings.prettify_confirm": "Formátovaním sa odstránia všetky komentáre. Pokračovať?",
"admin_settings.mode.form": "Formulár",
"admin_settings.mode.raw": "Surový",
"admin_settings.mode.effective": "Efektívny",
"admin_settings.mode.effective_tooltip": "Zobrazenie hodnôt, ktoré Etherpad práve teraz skutočne používa (po dosadení premenných prostredia), iba na čítanie. Tajné hodnoty sú skryté.",
"admin_settings.mode.aria_label": "Režim editora",
"admin_settings.envvar_banner.title": "Tento súbor je šablóna, nie živá konfigurácia.",
"admin_settings.envvar_banner.body": "Zástupné výrazy ako ${VAR:default} sa pri štarte dosadia do pamäte; nikdy sa nezapisujú späť do tohto súboru. Ak chcete zmeniť výslednú hodnotu, upravte premenné prostredia vo svojom prostredí (Docker compose, systemd, .env), alebo tu nahraďte zástupný výraz konkrétnou hodnotou. Prepnutím na kartu Efektívny zistíte, čo Etherpad práve používa.",
"admin_settings.toast.auth_error": "Nie ste overený ako správca. Prihláste sa, prosím, znova.",
"admin_settings.section.general": "Všeobecné",
"admin_settings.parse_error.title": "Nemožno spracovať settings.json",
"admin_settings.parse_error.cta": "Prepnite na surový režim na úpravu",
"admin_settings.env_pill.tooltip": "Číta sa z premennej prostredia {{variable}}. Hodnota nižšie sa použije, keď {{variable}} nie je nastavená.",
"admin_settings.env_pill.default_label": "predvolené",
"admin_settings.env_pill.input_aria": "Predvolená hodnota pre {{variable}}",
"admin_settings.env_pill.runtime_label": "aktívna hodnota",
"admin_settings.env_pill.runtime_tooltip": "Etherpad práve používa túto hodnotu, určenú z {{variable}} alebo jej predvolenej hodnoty.",
"admin_settings.env_pill.redacted_tooltip": "Etherpad používa hodnotu pre {{variable}}, je však skrytá, pretože ide o tajnú hodnotu.",
"admin_settings.page-title": "Nastavenia - Etherpad",
"admin_settings.save_error": "Chyba pri ukladaní nastavení",
"admin_settings.saved_success": "Nastavenia boli úspešne uložené",
"update.banner.title": "Dostupná aktualizácia",
"update.banner.body": "Je dostupný Etherpad {{latest}} (používate {{current}}).",
"update.banner.cta": "Zobraziť aktualizáciu",
"update.page.title": "Aktualizácie Etherpadu",
"update.page.current": "Aktuálna verzia",
"update.page.latest": "Najnovšia verzia",
"update.page.last_check": "Naposledy skontrolované",
"update.page.install_method": "Spôsob inštalácie",
"update.page.tier": "Úroveň aktualizácií",
"update.page.changelog": "Zoznam zmien",
"update.page.up_to_date": "Používate najnovšiu verziu.",
"update.page.disabled": "Kontroly aktualizácií sú vypnuté (updates.tier = „off“).",
"update.page.unauthorized": "Nemáte oprávnenie zobraziť stav aktualizácií.",
"update.page.error": "Nepodarilo sa načítať stav aktualizácií (stav {{status}}).",
"update.badge.severe": "Etherpad na tomto serveri je vážne zastaraný. Upozornite správcu.",
"update.badge.vulnerable": "Etherpad na tomto serveri používa verziu so známymi bezpečnostnými problémami. Upozornite správcu.",
"update.page.apply": "Použiť aktualizáciu",
"update.page.cancel": "Zrušiť",
"update.page.acknowledge": "Potvrdiť",
"update.page.log": "Záznam aktualizácie (posledných 200 riadkov)",
"update.page.execution": "Stav",
"update.page.policy.install-method-not-writable": "Aktualizácie zo správcovského rozhrania vyžadujú inštaláciu cez git. Aktualizujte cez svojho správcu balíkov.",
"update.page.policy.rollback-failed-terminal": "Predchádzajúca aktualizácia zlyhala a nepodarilo sa ju vrátiť späť. Po uvedení inštalácie do poriadku stlačte Potvrdiť, čím sa zámok zruší.",
"update.page.policy.up-to-date": "Používate najnovšiu verziu.",
"update.page.policy.tier-off": "Aktualizácie sú vypnuté (updates.tier = „off“).",
"update.page.policy.maintenance-window-missing": "Úroveň 4 (autonómna) vyžaduje okno údržby. Autonómne aktualizácie zapnete nastavením updates.maintenanceWindow v settings.json.",
"update.page.policy.maintenance-window-invalid": "Úroveň 4 (autonómna) je vypnutá, pretože updates.maintenanceWindow má nesprávny formát. Očakáva sa {start, end, tz} s časmi vo formáte HH:MM a tz „local“ alebo „utc“.",
"update.page.last_result.verified": "Posledná aktualizácia na {{tag}} bola overená.",
"update.page.last_result.rolled-back": "Posledná pokusná aktualizácia na {{tag}} bola vrátená späť: {{reason}}.",
"update.page.last_result.rollback-failed": "Posledný pokus o aktualizáciu zlyhal A zlyhalo aj vrátenie späť: {{reason}}. Vyžaduje sa ručný zásah.",
"update.page.last_result.preflight-failed": "Posledná pokusná aktualizácia na {{tag}} zlyhala pri kontrole pred spustením: {{reason}}.",
"update.page.last_result.cancelled": "Poslednú pokusnú aktualizáciu na {{tag}} zrušil správca.",
"update.execution.idle": "Nečinné",
"update.execution.scheduled": "Aktualizácia naplánovaná",
"update.execution.preflight": "Kontroly pred spustením",
"update.execution.preflight-failed": "Kontrola pred spustením zlyhala",
"update.execution.draining": "Ukončovanie relácií",
"update.execution.executing": "Aktualizuje sa…",
"update.execution.pending-verification": "Čaká sa na overenie",
"update.execution.verified": "Overené",
"update.execution.rolling-back": "Vracia sa späť",
"update.execution.rolled-back": "Vrátené späť",
"update.execution.rollback-failed": "Vrátenie späť zlyhalo",
"update.banner.terminal.rollback-failed": "Pokus o aktualizáciu zlyhal a nepodarilo sa ho vrátiť späť. Vyžaduje sa ručný zásah.",
"update.banner.scheduled": "Automatická aktualizácia na {{tag}} je naplánovaná — použije sa o {{remaining}}.",
"update.banner.maintenance-window-missing": "Autonómne aktualizácie sú vypnuté, kým sa nenastaví okno údržby.",
"update.banner.maintenance-window-invalid": "Autonómne aktualizácie sú vypnuté, pretože okno údržby má nesprávny formát.",
"update.page.scheduled.title": "Aktualizácia naplánovaná",
"update.page.scheduled.countdown": "Etherpad sa začne aktualizovať na {{tag}} o {{remaining}}.",
"update.page.scheduled.deferred_until": "Mimo okna údržby. Aktualizácia sa spustí, keď sa okno otvorí o {{at}}.",
"update.page.scheduled.apply_now": "Použiť teraz",
"update.window.title": "Okno údržby",
"update.window.summary": "{{start}} {{end}} ({{tz}})",
"update.window.unset": "Nie je nastavené.",
"update.window.next_opens_at": "Ďalšie okno sa otvorí o {{at}}.",
"update.drain.t60": "Etherpad sa o 60 sekúnd reštartuje, aby použil aktualizáciu.",
"update.drain.t30": "Etherpad sa o 30 sekúnd reštartuje, aby použil aktualizáciu.",
"update.drain.t10": "Etherpad sa o 10 sekúnd reštartuje, aby použil aktualizáciu.",
"index.newPad": "Nový poznámkový blok",
"index.createOpenPad": "alebo vytvoriť/otvoriť poznámkový blok s názvom:",
"index.settings": "Nastavenia",
"index.transferSessionTitle": "Preniesť reláciu",
"index.receiveSessionTitle": "Prijať reláciu",
"index.receiveSessionDescription": "Tu môžete prijať reláciu Etherpadu z iného prehliadača alebo zariadenia. Upozorňujeme však, že tým sa odstráni vaša súčasná relácia, ak nejakú máte.",
"index.transferSession": "1. Preniesť reláciu",
"index.transferSessionNow": "Preniesť reláciu teraz",
"index.copyLink": "2. Kopírovať odkaz",
"index.copyLinkDescription": "Kliknutím na tlačidlo nižšie skopírujete odkaz do schránky.",
"index.copyLinkButton": "Kopírovať odkaz do schránky",
"index.transferToSystem": "3. Skopírovať reláciu do nového systému",
"index.transferToSystemDescription": "Otvorením skopírovaného odkazu v cieľovom prehliadači alebo zariadení prenesiete svoju reláciu.",
"index.code": "Kód",
"index.transferSessionDescription": "Kliknutím na tlačidlo nižšie prenesiete svoju súčasnú reláciu do prehliadača alebo zariadenia. Skopíruje sa odkaz na stránku, ktorá po otvorení v cieľovom prehliadači alebo zariadení prenesie vašu reláciu.",
"index.createOpenPad": "Otvoriť pad podľa názvu",
"index.openPad": "otvoriť poznámkový blok s názvom:",
"index.recentPads": "Nedávne pady",
"index.recentPadsEmpty": "Nenašli sa žiadne nedávne pady.",
"index.generateNewPad": "Vygenerovať náhodný názov padu",
"index.labelPad": "Názov padu (nepovinné)",
"index.placeholderPadEnter": "Zadajte názov padu…",
"index.createAndShareDocuments": "Vytvárajte a zdieľajte dokumenty v reálnom čase",
"index.createAndShareDocumentsDescription": "Etherpad vám umožňuje spoločne upravovať dokumenty v reálnom čase, podobne ako živý viacpoužívateľský editor, ktorý beží vo vašom prehliadači.",
"pad.toolbar.bold.title": "Tučné (Ctrl+B)",
"pad.toolbar.italic.title": "Kurzíva (Ctrl+I)",
"pad.toolbar.underline.title": "Podčiarknuté (Ctrl+U)",
@ -62,22 +264,42 @@
"pad.toolbar.savedRevision.title": "Uložiť revíziu",
"pad.toolbar.settings.title": "Nastavenia",
"pad.toolbar.embed.title": "Zdieľať alebo vložiť tento poznámkový blok",
"pad.toolbar.home.title": "Späť na domovskú stránku",
"pad.toolbar.showusers.title": "Zobraziť používateľov tohoto poznámkového bloku",
"pad.colorpicker.save": "Uložiť",
"pad.colorpicker.cancel": "Zrušiť",
"pad.loading": "Načítava sa...",
"pad.noCookie": "Cookie nebolo možné nájsť. Povoľte prosím cookies vo vašom prehliadači. Vaše sedenie a nastavenia sa medzi návštevami stránky neuložia. To môže byť spôsobené tým že Etherpad je zahrnutý do iFrame v niektorých prehliadačoch. Prosím uistite sa, že Etherpad sa nachádza na tej istej doméne ako hlavný iFrame",
"pad.permissionDenied": "Ľutujeme, nemáte oprávnenie pristupovať k tomuto poznámkovému bloku",
"pad.settings.padSettings": "Nastavenia poznámkového bloku",
"pad.settings.title": "Nastavenia",
"pad.settings.padSettings": "Nastavenia pre celý pad",
"pad.settings.userSettings": "Používateľské nastavenia",
"pad.settings.myView": "Vlastný pohľad",
"pad.settings.disablechat": "Vypnúť chat",
"pad.settings.darkMode": "Tmavý režim",
"pad.settings.stickychat": "Rozhovor stále na obrazovke",
"pad.settings.chatandusers": "Zobraziť rozhovor a používateľov",
"pad.settings.colorcheck": "Farby autorov",
"pad.settings.fadeInactiveAuthorColors": "Stlmiť farby neaktívnych autorov",
"pad.settings.linenocheck": "Čísla riadkov",
"pad.settings.rtlcheck": "Čítať obsah sprava doľava?",
"pad.settings.enforceSettings": "Vynútiť nastavenia pre ostatných používateľov",
"pad.settings.enforcedNotice": "Tieto nastavenia vám uzamkol tvorca tohto padu. Ak ich potrebujete zmeniť, požiadajte tvorcu padu.",
"pad.settings.fontType": "Typ písma:",
"pad.settings.fontType.normal": "Normálne",
"pad.settings.language": "Jazyk:",
"pad.settings.deletePad": "Zmazať pad",
"pad.delete.confirm": "Naozaj chcete zmazať tento pad?",
"pad.deletionToken.modalTitle": "Uložte si token na zmazanie padu",
"pad.deletionToken.modalBody": "Tento token je jediný spôsob, ako zmazať tento pad, ak stratíte reláciu prehliadača alebo zmeníte zariadenie. Uložte si ho na bezpečné miesto — zobrazí sa tu iba raz.",
"pad.deletionToken.copy": "Kopírovať",
"pad.deletionToken.copied": "Skopírované",
"pad.deletionToken.acknowledge": "Uložil som si ho",
"pad.deletionToken.deleteWithToken": "Zmazať pad pomocou tokenu",
"pad.deletionToken.tokenFieldLabel": "Token na zmazanie padu",
"pad.deletionToken.tokenValueLabel": "Váš token na zmazanie padu (iba na čítanie)",
"pad.deletionToken.invalid": "Tento token nie je platný pre tento pad.",
"pad.deletionToken.notCreator": "Nie ste tvorcom tohto padu, takže ho nemôžete zmazať.",
"pad.settings.about": "O Etherpade",
"pad.settings.poweredBy": "Poháňané cez",
"pad.importExport.import_export": "Import/Export",
@ -90,6 +312,13 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.exportetherpada.title": "Exportovať ako Etherpad",
"pad.importExport.exporthtmla.title": "Exportovať ako HTML",
"pad.importExport.exportplaina.title": "Exportovať ako čistý text",
"pad.importExport.exportworda.title": "Exportovať ako Microsoft Word",
"pad.importExport.exportpdfa.title": "Exportovať ako PDF",
"pad.importExport.exportopena.title": "Exportovať ako ODF (Open Document Format)",
"pad.importExport.noConverter.innerHTML": "Importovať môžete iba z formátov čistého textu alebo HTML. Pre pokročilejšie funkcie importu si <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">nainštalujte LibreOffice</a>.",
"pad.modals.connected": "Pripojené.",
"pad.modals.reconnecting": "Opätovné pripájanie k vášmu poznámkovému bloku...",
"pad.modals.forcereconnect": "Vynútiť znovupripojenie",
@ -120,6 +349,8 @@
"pad.modals.disconnected": "Boli ste odpojení.",
"pad.modals.disconnected.explanation": "Spojenie so serverom sa prerušilo",
"pad.modals.disconnected.cause": "Server môže byť nedostupný. Ak by problém pretrvával, informujte správcu služby.",
"pad.gritter.unacceptedCommit.title": "Neuložená úprava",
"pad.gritter.unacceptedCommit.text": "Vaša nedávna úprava stále nie je uložená. Znova sa pripojte a skúste to ešte raz.",
"pad.share": "Zdieľať tento poznámkový blok",
"pad.share.readonly": "Len na čítanie",
"pad.share.link": "Odkaz",
@ -132,12 +363,36 @@
"timeslider.followContents": "Sledovať aktualizácie obsahu poznámkového bloku",
"timeslider.pageTitle": "Časová os {{appTitle}}",
"timeslider.toolbar.returnbutton": "Späť do poznámkového bloku",
"pad.historyMode.banner": "Prezeranie histórie",
"pad.historyMode.return": "Návrat k živej verzii",
"pad.historyMode.revisionLabel": "Revízia {{rev}}",
"pad.historyMode.controlsLabel": "Ovládanie histórie padu",
"pad.historyMode.sliderLabel": "Revízia padu",
"pad.historyMode.settings.title": "Prehrávanie histórie",
"pad.historyMode.settings.follow": "Sledovať zmeny obsahu padu",
"pad.historyMode.settings.followShort": "Sledovať",
"pad.historyMode.followOn": "Sledujú sa zmeny padu — kliknutím sledovanie ukončíte",
"pad.historyMode.followOff": "Zmeny padu sa nesledujú — kliknutím začnete sledovať",
"pad.historyMode.settings.playbackSpeed": "Rýchlosť prehrávania:",
"pad.historyMode.chat.replayHeader": "Chat k {{time}}",
"pad.historyMode.users.authorsHeader": "Autori v tejto revízii",
"pad.editor.skipToContent": "Preskočiť na editor",
"pad.editor.keyboardHint": "Stlačením Escape opustíte editor. Stlačením Alt+F9 sa dostanete na panel nástrojov.",
"pad.editor.toolbar.formatting": "Panel nástrojov formátovania",
"pad.editor.toolbar.actions": "Panel akcií padu",
"pad.editor.toolbar.showMore": "Zobraziť viac tlačidiel panela",
"timeslider.toolbar.authors": "Autori:",
"timeslider.toolbar.authorsList": "Bez autorov",
"timeslider.toolbar.exportlink.title": "Export",
"timeslider.exportCurrent": "Exportovať aktuálnu verziu ako:",
"timeslider.version": "Verzia {{version}}",
"timeslider.saved": "Uložené {{day}}. {{month}} {{year}}",
"timeslider.settings.playbackSpeed": "Rýchlosť prehrávania:",
"timeslider.settings.playbackSpeed.original": "Pôvodná rýchlosť",
"timeslider.settings.playbackSpeed.realtime": "Reálny čas",
"timeslider.settings.playbackSpeed.200ms": "200 ms",
"timeslider.settings.playbackSpeed.500ms": "500 ms",
"timeslider.settings.playbackSpeed.1000ms": "1000 ms",
"timeslider.playPause": "Pustiť / Pozastaviť obsah poznámkového bloku",
"timeslider.backRevision": "Ísť v tomto poznámkovom bloku o jednu revíziu späť",
"timeslider.forwardRevision": "Ísť v tomto poznámkovom bloku o jednu revíziu vpred",
@ -159,6 +414,7 @@
"pad.savedrevs.timeslider": "Návštevou časovej osi môžete zobraziť uložené revízie",
"pad.userlist.entername": "Zadajte svoje meno",
"pad.userlist.unnamed": "nemenovaný",
"pad.userlist.onlineCount": "{[ plural(count) one: {{count}} pripojený používateľ, few: {{count}} pripojení používatelia, many: {{count}} pripojených používateľov, other: {{count}} pripojených používateľov ]}",
"pad.editbar.clearcolors": "Odstrániť farby autorov z celého dokumentu? Táto akcia sa nedá vrátiť",
"pad.impexp.importbutton": "Importovať teraz",
"pad.impexp.importing": "Prebieha import...",
@ -169,5 +425,6 @@
"pad.impexp.importfailed": "Import zlyhal",
"pad.impexp.copypaste": "Vložte prosím kópiu cez schránku",
"pad.impexp.exportdisabled": "Export do formátu {{type}} nie je povolený. Kontaktujte prosím administrátora pre zistenie detailov.",
"pad.impexp.maxFileSize": "Súbor je príliš veľký. Kontaktujte správcu pre zväčšenie povolenej veľkosti súborov pre import"
"pad.impexp.maxFileSize": "Súbor je príliš veľký. Kontaktujte správcu pre zväčšenie povolenej veľkosti súborov pre import",
"pad.social.description": "Kolaboratívny dokument, ktorý môže každý upravovať v reálnom čase."
}

View file

@ -18,6 +18,7 @@
"Stang",
"TFX202X",
"VulpesVulpes825",
"XtexChooser",
"Yfdyh000",
"乌拉跨氪",
"列维劳德",
@ -26,6 +27,30 @@
]
},
"admin.page-title": "管理员面板 - Etherpad",
"admin.loading": "正在加载…",
"admin.loading_description": "页面加载中,请稍安勿躁。",
"admin.toggle_sidebar": "开关侧边栏",
"admin.shout": "通讯",
"admin_shout.online_one": "目前有{{count}}位用户在线",
"admin_shout.online_other": "目前有{{count}}位用户在线",
"admin_shout.sticky_toggle": "更改置顶消息",
"admin_login.title": "Etherpad",
"admin_login.username": "用户名",
"admin_login.password": "密码",
"admin_login.submit": "登录",
"admin_login.failed": "登录失败",
"admin_pads.all_pads": "所有记事本",
"admin_pads.bulk.cleanup_history": "清理历史",
"admin_pads.bulk.clear_selection": "清除选择",
"admin_pads.bulk.delete": "删除",
"admin_pads.cancel": "取消",
"admin_pads.col.users": "用户",
"admin_pads.confirm_button": "确定",
"admin_pads.error_prefix": "错误",
"admin_pads.pagination.next": "下一页",
"admin_pads.pagination.previous": "上一页",
"admin_pads.refresh": "刷新",
"admin_pads.relative.just_now": "刚刚",
"admin_plugins": "插件管理器",
"admin_plugins.available": "可用插件",
"admin_plugins.available_not-found": "找不到插件。",

View file

@ -15,6 +15,16 @@
]
},
"admin.page-title": "管理員面板 - Etherpad",
"admin.loading": "載入中…",
"admin_login.title": "Etherpad",
"admin_login.username": "使用者名稱",
"admin_login.password": "密碼",
"admin_login.submit": "登入",
"admin_login.failed": "登入失敗",
"admin_pads.bulk.delete": "刪除",
"admin_pads.cancel": "取消",
"admin_pads.col.users": "使用者",
"admin_pads.error_prefix": "錯誤",
"admin_plugins": "外掛程式管理器",
"admin_plugins.available": "可用套件",
"admin_plugins.available_not-found": "沒有找到套件。",
@ -29,6 +39,9 @@
"admin_plugins.last-update": "最後更新",
"admin_plugins.name": "名稱",
"admin_plugins.page-title": "套件管理 - Etherpad",
"admin_plugins.update_tooltip": "更新",
"admin_plugins.updates_available": "有可用更新",
"admin_plugins.update_now": "更新",
"admin_plugins.version": "版本",
"admin_plugins_info": "問題排除資訊",
"admin_plugins_info.hooks": "已安裝的掛勾",
@ -37,6 +50,8 @@
"admin_plugins_info.parts": "已安裝部分",
"admin_plugins_info.plugins": "已安裝的套件",
"admin_plugins_info.page-title": "套件資訊 - Etherpad",
"admin_plugins_info.tab_client": "客戶端",
"admin_plugins_info.tab_server": "伺服器",
"admin_plugins_info.version": "Etherpad 版本",
"admin_plugins_info.version_latest": "最新可用版本",
"admin_plugins_info.version_number": "版本號",
@ -46,6 +61,9 @@
"admin_settings.current_example-prod": "生產設定模板範例",
"admin_settings.current_restart.value": "重新啟動 Etherpad",
"admin_settings.current_save.value": "儲存設定",
"admin_settings.invalid_json": "無效 JSON",
"admin_settings.current_test.value": "驗證 JSON",
"admin_settings.current_prettify.value": "美化 JSON 格式",
"admin_settings.page-title": "設定 - Etherpad",
"index.newPad": "新記事本",
"index.settings": "設定",
@ -116,6 +134,11 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF開放文件格式",
"pad.importExport.exporthtmla.title": "匯出成 HTML",
"pad.importExport.exportplaina.title": "匯出成純文字",
"pad.importExport.exportworda.title": "匯出成 Microsoft Word",
"pad.importExport.exportpdfa.title": "匯出成 PDF",
"pad.importExport.exportopena.title": "匯出成 ODF開放文件格式",
"pad.modals.connected": "已連線。",
"pad.modals.reconnecting": "重新連線到您的記事本…",
"pad.modals.forcereconnect": "強制重新連線",

View file

@ -551,10 +551,11 @@ exports.createPad = async (padID: string, text: string, authorId = '') => {
// create pad
await getPadSafe(padID, false, text, authorId);
// When requireAuthentication is on, every creator has a stable identity, so
// the cookie/identity path covers recovery and the one-time token is just
// an extra surface to leak.
const deletionToken = settings.requireAuthentication
// No recovery token when it cannot help: requireAuthentication gives every
// creator a stable identity, and allowPadDeletionByAllUsers lets anyone delete
// the pad with no token at all (issue #7926). Either way the token is just an
// extra surface to leak.
const deletionToken = settings.requireAuthentication || settings.allowPadDeletionByAllUsers
? null
: await padDeletionManager.createDeletionTokenIfAbsent(padID);
return {deletionToken};

View file

@ -868,6 +868,8 @@ class Pad {
await this.saveToDatabase();
}
// Returns the newly created saved revision, or undefined if this revision
// was already saved (so callers can broadcast only genuine additions).
async addSavedRevision(revNum: string, savedById: string, label: string) {
// if this revision is already saved, return silently
for (const i in this.savedRevisions) {
@ -887,6 +889,7 @@ class Pad {
// save this new saved revision
this.savedRevisions.push(savedRevision);
await this.saveToDatabase();
return savedRevision;
}
getSavedRevisions() {

View file

@ -192,7 +192,15 @@ exports.sanitizePadId = async (padId: string) => {
return padId;
};
exports.isValidPadId = (padId: string) => /^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId);
// Pad IDs consisting only of URL "dot-segments" ('.' or '..') are unreachable:
// per the WHATWG URL standard a browser normalises "/p/." to "/p/" and "/p/.."
// to "/", so the pad can never be opened or exported — the request arrives as
// "/p/" and Etherpad answers "Cannot GET /p/". Reject them so such (broken) pads
// can never be created.
const dotSegmentPadId = /^\.{1,2}$/;
exports.isValidPadId = (padId: string) =>
/^(g.[a-zA-Z0-9]{16}\$)?[^$]{1,50}$/.test(padId) && !dotSegmentPadId.test(padId);
/**
* Removes the pad from database and unloads it.

View file

@ -305,8 +305,13 @@ const handlePadDelete = async (socket: any, padDeleteMessage: PadDeleteMessage)
// back to the creator-cookie path, otherwise a creator pasting a wrong
// recovery token into the disclosure field would still succeed — masking a
// typo and contradicting the UI.
const creatorOk = !tokenSupplied && isCreator;
const flagOk = !tokenSupplied && !isCreator && settings.allowPadDeletionByAllUsers;
// Readonly sessions can never delete via the token-less paths: they cannot
// edit the pad, so they must not be able to destroy it just because
// allowPadDeletionByAllUsers is on (issue #7959). A valid recovery token
// (tokenOk) remains a sufficient credential regardless of session mode.
const writable = !session.readonly;
const creatorOk = !tokenSupplied && isCreator && writable;
const flagOk = !tokenSupplied && !isCreator && settings.allowPadDeletionByAllUsers && writable;
if (creatorOk || tokenOk || flagOk) {
await retrievedPad.remove();
@ -622,7 +627,18 @@ exports.handleMessage = async (socket:any, message: ClientVarMessage) => {
const handleSaveRevisionMessage = async (socket:any, message: ClientSaveRevisionMessage) => {
const {padId, author: authorId} = sessioninfos[socket.id];
const pad = await padManager.getPad(padId, null, authorId);
await pad.addSavedRevision(pad.head, authorId);
const savedRevision = await pad.addSavedRevision(pad.head, authorId);
// Notify every client in the pad room — including any open timeslider —
// so saved-revision markers appear live instead of only on the next
// timeslider load (#7946). The client's NEW_SAVEDREV handler existed but
// was never reached because this broadcast was missing; live editors that
// don't handle the type ignore it. Skip the emit for duplicate saves.
if (savedRevision) {
socketio.sockets.in(padId).emit('message', {
type: 'COLLABROOM',
data: {type: 'NEW_SAVEDREV', savedRev: savedRevision},
});
}
};
/**
@ -1287,15 +1303,41 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
// once. Readonly sessions never see it.
const isCreator =
!sessionInfo.readonly && sessionInfo.author === await pad.getRevisionAuthor(0);
// Skip token issuance — and so the client never shows the "Save your pad
// deletion token" modal (issue #7926) — when the token cannot help:
// - requireAuthentication: every creator already has a stable identity, so
// the cookie/identity path is sufficient.
// The deletion token is a recovery handle for the one class of creator that
// can otherwise lose the ability to delete their pad: a user whose creator
// status lives only in a per-browser author-token cookie. It is pointless —
// and the "Save your pad deletion token" modal only overwhelms users who
// will never need it (issue #7926) — when either of these holds:
//
// - allowPadDeletionByAllUsers: anyone can delete the pad with no token at
// all (see handlePadDelete's flagOk branch), so a recovery token is noise
// and the modal only overwhelms users who will never need it.
// all (see handlePadDelete's flagOk branch).
// - the creator has a *durable* identity: authenticated (req.session.user
// with a username) AND the deployment maps that identity to a stable
// authorID via a getAuthorId hook. Only then does `isCreator`
// (author === revision-0 author) survive a cookie clear or a different
// device, so the creator path replaces the token on any device.
//
// Note we deliberately do NOT treat requireAuthentication alone as durable:
// without a getAuthorId hook the authorID still comes from the per-browser
// token cookie (AuthorManager.getAuthorId -> getAuthor4Token), so an
// authenticated user on a second device is NOT the creator and would be
// stranded if we also withheld the token. The getAuthorId hook is the
// documented way (doc/api/hooks_server-side) to pin authorID to username.
const hasGetAuthorIdHook = (plugins.hooks.getAuthorId || []).length > 0;
const hasDurableIdentity = hasGetAuthorIdHook && !!(user && user.username);
const canDeleteWithoutToken = settings.allowPadDeletionByAllUsers || hasDurableIdentity;
// Whether this session may delete the pad with no token at all: the creator
// on this device (creator-cookie still present), or any user when the
// instance opted everyone in. Drives the plain "Delete pad" button, which is
// independent of enablePadWideSettings (issue #7959) — deletion is not a
// pad-wide setting and must stay reachable when that section is disabled.
// Readonly viewers are excluded: they cannot edit, let alone delete, so
// allowPadDeletionByAllUsers must not hand them a delete button (the server
// enforces the same in handlePadDelete).
const canDeletePad =
!sessionInfo.readonly && (isCreator || settings.allowPadDeletionByAllUsers);
const padDeletionToken =
isCreator && !settings.requireAuthentication && !settings.allowPadDeletionByAllUsers
isCreator && !canDeleteWithoutToken
? await padDeletionManager.createDeletionTokenIfAbsent(sessionInfo.padId)
: null;
@ -1314,6 +1356,12 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
enablePadWideSettings: settings.enablePadWideSettings,
enablePluginPadOptions: settings.enablePluginPadOptions,
padDeletionToken,
// Drives the deletion-button label/visibility in pad settings: when the
// user can already delete without a token the recovery-token disclosure is
// redundant, so the client labels the action "Delete Pad" instead of
// "Delete with token" (issue #7926). See showDeletionTokenModalIfPresent.
canDeleteWithoutToken,
canDeletePad,
// Allow-listed copy — settings.privacyBanner could carry extra nested
// keys from a hand-edited settings.json; sending those by reference
// would leak them to every browser. See getPublicPrivacyBanner().

View file

@ -305,13 +305,22 @@ exports.socketio = (hookName: string, {io}: any) => {
socket.on('deletePad', async (padId: string) => {
try {
if (await padManager.doesPadExists(padId)) {
// Healthy pad — full relational cleanup (revs, chat, readonly,
// authors, deletion token, hooks).
logger.info(`Deleting pad: ${padId}`);
const pad = await padManager.getPad(padId);
await pad.remove();
socket.emit('results:deletePad', padId);
return;
try {
// Healthy pad — full relational cleanup (revs, chat, readonly,
// authors, deletion token, hooks).
logger.info(`Deleting pad: ${padId}`);
const pad = await padManager.getPad(padId);
await pad.remove();
socket.emit('results:deletePad', padId);
return;
} catch (err) {
// getPad() runs isValidPadId() and rejects ids that are no longer
// valid — e.g. legacy '.'/'..' pads created before that validation
// was tightened. Don't give up: fall through to the raw key purge
// below so the orphan can still be deleted from the admin UI.
logger.warn(`Relational cleanup failed for "${padId}" ` +
`(${safeErr(err)}); falling back to raw key purge`);
}
}
// doesPadExists() is false either because nothing is stored under

View file

@ -19,6 +19,7 @@
* limitations under the License.
*/
import log4js from 'log4js';
import fs from 'fs';
import path from 'path';
import _ from 'underscore';
@ -30,6 +31,25 @@ const absPathLogger = log4js.getLogger('AbsolutePaths');
*/
let etherpadRoot: string|null = null;
/**
* Walks up the directory tree from `start`, returning the closest ancestor
* directory (including `start` itself) that contains a package.json. Replaces
* the unmaintained `find-root` package, mirroring its semantics: it throws if
* no package.json is found before reaching the filesystem root.
*
* @param {string} start - The directory to start searching from.
* @return {string} The closest ancestor directory containing a package.json.
*/
const findRoot = (start: string): string => {
let dir = start;
for (;;) {
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
const parent = path.dirname(dir);
if (parent === dir) throw new Error('package.json not found in path');
dir = parent;
}
};
/**
* If stringArray's last elements are exactly equal to lastDesiredElements,
* returns a copy in which those last elements are popped, or false otherwise.
@ -79,7 +99,6 @@ export const findEtherpadRoot = () => {
return etherpadRoot;
}
const findRoot = require('find-root');
const foundRoot = findRoot(__dirname);
const splitFoundRoot = foundRoot.split(path.sep);

View file

@ -39,11 +39,9 @@ const ROOT_DIR = path.join(settings.root, 'src/static/');
const LIBRARY_WHITELIST = [
'async',
'js-cookie',
'security',
'split-grid',
'tinycon',
'underscore',
'unorm',
];
// What follows is a terrible hack to avoid loop-back within the server.

View file

@ -35,7 +35,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {argv} from './Cli'
import jsonminify from 'jsonminify';
import {parse as parseJsonc, printParseErrorCode, ParseError} from 'jsonc-parser';
import log4js from 'log4js';
import {createHash} from 'node:crypto';
import randomString from './randomstring';
@ -116,9 +116,18 @@ const parseSettings = (settingsFilename: string, isSettings: boolean) => {
}
try {
settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}');
// jsonc-parser tolerates comments and trailing commas, so settings files
// can stay annotated. Unlike the old jsonminify + naive ',]'/',}' string
// replace, it fixes *every* trailing comma (not just the first of each
// kind) and never mangles those sequences when they appear inside strings.
const errors: ParseError[] = [];
const settings = parseJsonc(settingsStr, errors, {allowTrailingComma: true});
const settings = JSON.parse(settingsStr);
if (errors.length > 0) {
const {error, offset} = errors[0];
throw new Error(`${printParseErrorCode(error)} at offset ${offset}`);
}
if (settings === undefined) throw new Error('file is empty or not valid JSON');
logger.info(`${settingsType} loaded from: ${settingsFilename}`);

View file

@ -67,7 +67,6 @@
, "skiplist.js"
, "colorutils.js"
, "undomodule.js"
, "$unorm/lib/unorm.js"
, "contentcollector.js"
, "changesettracker.js"
, "linestylefilter.js"

View file

@ -38,11 +38,10 @@
"cross-spawn": "^7.0.6",
"dirty-ts": "^1.1.8",
"ejs": "^6.0.1",
"esbuild": "^0.28.0",
"esbuild": "^0.28.1",
"express": "^5.2.1",
"express-rate-limit": "^8.5.1",
"express-session": "^1.19.0",
"find-root": "1.1.0",
"formidable": "^3.5.4",
"html-to-docx": "^1.8.0",
"htmlparser2": "^12.0.0",
@ -50,47 +49,45 @@
"jose": "^6.2.3",
"js-cookie": "^3.0.8",
"jsdom": "^29.1.1",
"jsonminify": "0.4.2",
"jsonc-parser": "^3.3.1",
"jsonwebtoken": "^9.0.3",
"jwt-decode": "^4.0.0",
"languages4translatewiki": "0.1.3",
"live-plugin-manager": "^1.1.0",
"lodash.clonedeep": "4.5.0",
"log4js": "^6.9.1",
"lru-cache": "^11.5.1",
"lru-cache": "^11.5.2",
"mammoth": "^1.12.0",
"measured-core": "^2.0.0",
"mime-types": "^3.0.2",
"mongodb": "^7.1.1",
"mssql": "^12.5.5",
"mysql2": "^3.22.5",
"nano": "^11.0.5",
"nodemailer": "^8.0.10",
"oidc-provider": "9.8.4",
"openapi-backend": "^5.17.0",
"pdfkit": "^0.19.0",
"pg": "^8.21.0",
"mongodb": "^7.4.0",
"mssql": "^12.6.0",
"mysql2": "^3.22.6",
"nano": "^11.0.6",
"nodemailer": "^9.0.3",
"oidc-provider": "9.9.1",
"openapi-backend": "^5.18.0",
"pdfkit": "^0.19.1",
"pg": "^8.22.0",
"prom-client": "^15.1.3",
"proxy-addr": "^2.0.7",
"rate-limiter-flexible": "^11.2.0",
"redis": "^6.0.0",
"redis": "^6.1.0",
"rehype": "^13.0.2",
"rehype-minify-whitespace": "^6.0.2",
"resolve": "1.22.12",
"rethinkdb": "^2.4.2",
"rusty-store-kv": "^1.3.1",
"security": "1.0.0",
"semver": "^7.8.3",
"semver": "^7.8.5",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"superagent": "10.3.0",
"surrealdb": "^2.0.3",
"surrealdb": "^2.0.4",
"tinycon": "0.6.8",
"tsx": "4.22.4",
"ueberdb2": "^6.1.9",
"tsx": "4.23.0",
"ueberdb2": "6.1.16",
"underscore": "1.13.8",
"undici": "^8.4.1",
"unorm": "1.6.0",
"undici": "^8.7.0",
"wtfnode": "^0.10.1"
},
"bin": {
@ -98,7 +95,7 @@
"etherpad-lite": "node/server.ts"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@playwright/test": "^1.61.1",
"@types/async": "^3.2.25",
"@types/cookie-parser": "^1.4.10",
"@types/cross-spawn": "^6.0.6",
@ -110,33 +107,32 @@
"@types/jquery": "^4.0.1",
"@types/js-cookie": "^3.0.6",
"@types/jsdom": "^28.0.3",
"@types/jsonminify": "^0.4.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/mime-types": "^3.0.1",
"@types/mocha": "^10.0.9",
"@types/node": "^25.9.2",
"@types/nodemailer": "^8.0.0",
"@types/node": "^26.1.1",
"@types/nodemailer": "^8.0.1",
"@types/oidc-provider": "^9.5.0",
"@types/pdfkit": "^0.17.6",
"@types/semver": "^7.7.1",
"@types/sinon": "^21.0.1",
"@types/sinon": "^22.0.0",
"@types/supertest": "^7.2.0",
"@types/underscore": "^1.13.0",
"@types/whatwg-mimetype": "^5.0.0",
"chokidar": "^5.0.0",
"eslint": "^10.4.1",
"eslint": "^10.6.0",
"eslint-config-etherpad": "^4.0.5",
"etherpad-cli-client": "^4.0.3",
"mocha": "^11.7.6",
"mocha-froth": "^0.2.10",
"nodeify": "^1.0.1",
"openapi-schema-validation": "^0.4.2",
"set-cookie-parser": "^3.1.0",
"set-cookie-parser": "^3.1.1",
"sinon": "^22.0.0",
"split-grid": "^1.0.11",
"supertest": "^7.2.2",
"typescript": "^6.0.3",
"vitest": "^4.1.8"
"vitest": "^4.1.10"
},
"engines": {
"node": ">=24.0.0",
@ -164,6 +160,6 @@
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts",
"test:vitest": "vitest"
},
"version": "3.3.1",
"version": "3.3.2",
"license": "Apache-2.0"
}

View file

@ -214,12 +214,52 @@ body.history-mode #history-controls { display: flex; }
.history-controls button.buttonicon.buttonicon-play.pause::before {
content: "\e829";
}
.history-slider-input {
.history-slider-wrap {
position: relative;
flex: 1 1 auto;
min-width: 80px;
margin: 0 6px;
display: flex;
align-items: center;
}
.history-slider-input {
flex: 1 1 auto;
min-width: 0;
width: 100%;
cursor: pointer;
}
/* Saved-revision markers overlaid on the slider track (issue #7946). Inset
* left/right by ~half the native range thumb so a marker lines up with the
* thumb centre at the track extremes. pointer-events:none on the layer keeps
* slider dragging unobstructed; the stars themselves re-enable clicks to seek. */
.history-slider-stars {
position: absolute;
left: 8px;
right: 8px;
top: 0;
bottom: 0;
pointer-events: none;
}
.history-slider-stars .history-star {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
padding: 0;
margin: 0;
border: 0;
background: none;
line-height: 1;
cursor: pointer;
pointer-events: auto;
}
.history-slider-stars .history-star::before {
font-family: fontawesome-etherpad;
content: "\e856";
color: #da9700;
font-size: 14px;
}
.history-timer {
flex: 0 0 auto;
font-size: 12px;
@ -282,7 +322,7 @@ body.history-mode #history-controls { display: flex; }
@media (max-width: 800px) {
.history-controls { padding: 0 6px; gap: 4px; min-height: 36px; }
.history-controls button.buttonicon { padding: 4px 6px; min-width: 32px; }
.history-slider-input { min-width: 60px; margin: 0 2px; }
.history-slider-wrap { min-width: 60px; margin: 0 2px; }
}
@media (max-width: 480px) {
.history-controls #history-leftstep,

View file

@ -31,12 +31,13 @@ import Op from "./Op";
const _MAX_LIST_LEVEL = 16;
import AttributeMap from './AttributeMap';
import UNorm from 'unorm';
import {subattribution} from './Changeset';
import {SmartOpAssembler} from "./SmartOpAssembler";
const hooks = require('./pluginfw/hooks');
const sanitizeUnicode = (s) => UNorm.nfc(s);
// NFC-normalize via the native String API (replaces the unmaintained `unorm`
// polyfill; String.prototype.normalize is available in every supported runtime).
const sanitizeUnicode = (s: string) => s.normalize('NFC');
const tagName = (n) => n.tagName && n.tagName.toLowerCase();
// supportedElems are Supported natively within Etherpad and don't require a plugin
const supportedElems = new Set([

View file

@ -149,6 +149,23 @@ const padeditor = (() => {
}
});
// The recovery-token disclosure (#delete-pad-with-token) is rendered for
// every session because a token may now be issued under requireAuthentication
// too (when no getAuthorId hook pins a durable authorID — issue #7926).
// Show it only when the user actually needs a token: when they can already
// delete without one (everyone may delete, or they have a durable
// authenticated identity) the plain "Delete Pad" button suffices, so hide
// the disclosure and all its token wording entirely.
$('#delete-pad-with-token').prop(
'hidden', !!(window as any).clientVars?.canDeleteWithoutToken);
// The plain "Delete pad" button is shown whenever this session can delete
// without a token (creator on this device, or allowPadDeletionByAllUsers).
// It is independent of pad-wide settings so it stays reachable when that
// section is disabled (issue #7959).
$('#delete-pad').prop(
'hidden', !(window as any).clientVars?.canDeletePad);
// delete pad using a recovery token (second device / no creator cookie)
$('#delete-pad-token-submit').on('click', () => {
const token = String($('#delete-pad-token-input').val() || '').trim();

View file

@ -55,6 +55,10 @@ class PadModeController {
private padId: string;
private innerHashChangeHandler: (() => void) | null = null;
private revObserver: MutationObserver | null = null;
// Watches the embedded slider's #ui-slider-bar so saved revisions added live
// (NEW_SAVEDREV from a collaborator while we're in history mode) get mirrored
// onto the outer slider — clientVars only carries the entry-time snapshot.
private savedRevObserver: MutationObserver | null = null;
private syncingHash = false;
// History-mode bridges — populated on enter, torn down on exit.
@ -194,6 +198,11 @@ class PadModeController {
}
this.revLabel.textContent = '';
this.dateLabel.textContent = '';
const stars = document.getElementById('history-slider-stars');
if (stars) {
stars.replaceChildren();
stars.dataset.sig = '';
}
}
// Restore everything entry-time we stashed: chat message visibility, the
@ -248,6 +257,10 @@ class PadModeController {
this.revObserver.disconnect();
this.revObserver = null;
}
if (this.savedRevObserver) {
this.savedRevObserver.disconnect();
this.savedRevObserver = null;
}
if (this.iframe) {
try {
if (this.innerHashChangeHandler && this.iframe.contentWindow) {
@ -374,6 +387,10 @@ class PadModeController {
playBtn.classList.toggle('pause', playing);
playBtn.setAttribute('aria-pressed', playing ? 'true' : 'false');
}
// Saved-revision markers depend on the slider max, which is only known
// once the inner slider has reported its length — render them here so we
// pick up the correct positions on first sync and on any max change.
this.renderSavedRevisionStars(innerWin);
};
// The hook registered earlier in attachInnerBridges already calls
// onRevChange — piggyback on it for slider input/timer updates by
@ -386,10 +403,87 @@ class PadModeController {
}
BS.onSlider(sync);
sync(BS.getSliderPosition?.() ?? 0);
// Now that the inner slider exists, watch it for live NEW_SAVEDREV stars.
this.observeInnerSavedRevisions(innerWin);
};
registerSync();
}
// Mirror the embedded timeslider's saved revisions onto the outer slider as
// clickable star markers (issue #7946). The inner slider draws its own stars
// on #ui-slider-bar, but that DOM is hidden in embed mode, so users only see
// the outer #history-slider-input — which had no markers.
//
// The inner #ui-slider-bar .star elements are the live source of truth: the
// timeslider keeps them current as NEW_SAVEDREV messages arrive (each carries
// a `pos` attribute = revNum), whereas clientVars.savedRevisions is only the
// entry-time snapshot. We read positions from those stars and pull labels
// from the snapshot where available. A signature guard keeps this cheap when
// sync() fires on every scrub; positions are percentage-based so they reflow
// on resize for free.
private renderSavedRevisionStars(innerWin: Window): void {
const inner: any = innerWin as any;
const layer = document.getElementById('history-slider-stars');
const sliderInput = document.getElementById('history-slider-input') as HTMLInputElement | null;
if (!layer || !sliderInput || !innerWin.document) return;
const max = Number(sliderInput.max) || 0;
const revNums = Array.from(innerWin.document.querySelectorAll('#ui-slider-bar .star'))
.map((el) => Number(el.getAttribute('pos')))
// max === 0 is a valid single-revision pad: only rev 0 belongs there.
.filter((n) => Number.isFinite(n) && n >= 0 && (max === 0 ? n === 0 : n <= max));
if (revNums.length === 0 || max < 0) {
if (layer.childElementCount) layer.replaceChildren();
layer.dataset.sig = '';
return;
}
// Labels live in the clientVars snapshot, keyed by revNum.
const labels = new Map<number, string>();
const snapshot = inner.clientVars?.savedRevisions;
if (Array.isArray(snapshot)) {
for (const r of snapshot) {
const n = Number(r && r.revNum);
if (Number.isFinite(n) && r && typeof r.label === 'string' && r.label) labels.set(n, r.label);
}
}
const sig = `${max}:${[...revNums].sort((a, b) => a - b).join(',')}`;
if (layer.dataset.sig === sig) return;
layer.dataset.sig = sig;
layer.replaceChildren();
for (const revNum of revNums) {
const frac = max === 0 ? 0 : revNum / max;
// A purely visual marker (the layer is aria-hidden): keyboard/screen
// reader users already reach any revision via the slider and step
// buttons, so we mirror the legacy timeslider's mouse-only stars rather
// than inject extra tab stops. The hover title aids mouse users; the
// click is a convenience to jump straight to the saved point.
const star = document.createElement('span');
star.className = 'history-star';
star.style.left = `${(frac * 100).toFixed(4)}%`;
star.title = labels.get(revNum) || `Revision ${revNum}`;
star.addEventListener('click', () => {
try { inner.BroadcastSlider?.setSliderPosition?.(revNum); } catch (_e) { /* inner gone */ }
});
layer.appendChild(star);
}
}
// Re-render the outer markers whenever the embedded slider adds a star
// (NEW_SAVEDREV). Observing the inner #ui-slider-bar covers saved revisions
// created live while history mode is open, which sync()'s scrub-driven
// callback would otherwise miss until the next slider move.
private observeInnerSavedRevisions(innerWin: Window): void {
if (this.savedRevObserver) return;
const bar = innerWin.document && innerWin.document.getElementById('ui-slider-bar');
if (!bar) return;
this.savedRevObserver = new MutationObserver(() => { this.renderSavedRevisionStars(innerWin); });
this.savedRevObserver.observe(bar, {childList: true});
}
// Capture the live state we'll restore on exit: live chat message
// visibility (just the timestamps — actual messages stay), live users
// panel HTML, and current Export hrefs.

View file

@ -24,7 +24,7 @@ import {binarySearch} from "./ace2_common";
* limitations under the License.
*/
const Security = require('security');
import * as Security from './security';
import jsCookie, {CookiesStatic} from 'js-cookie'
/**

View file

@ -1,20 +1,73 @@
// @ts-nocheck
'use strict';
/**
* Copyright 2009 Google Inc.
* OWASP-style output-escaping helpers.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* Vendored from the `security` npm package (v1.0.0), which has been
* unmaintained since 2012. The implementation below is reproduced verbatim
* (behaviour is byte-identical) so the dependency can be dropped from core.
*
* http://www.apache.org/licenses/LICENSE-2.0
* Original work Copyright (c) 2011 Chad Weider, MIT licensed:
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
module.exports = require('security');
const HTML_ENTITY_MAP: {[c: string]: string} = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
};
// OWASP Guidelines: &, <, >, ", ' plus forward slash.
const HTML_CHARACTERS_EXPRESSION = /[&"'<>/]/gm;
export const escapeHTML = (text: string) => text && text.replace(HTML_CHARACTERS_EXPRESSION,
(c: string) => HTML_ENTITY_MAP[c] || c);
// OWASP Guidelines: escape all non alphanumeric characters in ASCII space.
const HTML_ATTRIBUTE_CHARACTERS_EXPRESSION = /[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\xFF]/gm;
export const escapeHTMLAttribute = (text: string) => text && text.replace(HTML_ATTRIBUTE_CHARACTERS_EXPRESSION,
(c: string) => HTML_ENTITY_MAP[c] || `&#x${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)};`);
// OWASP Guidelines: escape all non alphanumeric characters in ASCII space.
// Also include line breaks (for literal).
const JAVASCRIPT_CHARACTERS_EXPRESSION = /[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\xFF\u2028\u2029]/gm;
export const encodeJavaScriptIdentifier = (text: string) => text && text.replace(JAVASCRIPT_CHARACTERS_EXPRESSION,
(c: string) => `\\u${(`0000${c.charCodeAt(0).toString(16)}`).slice(-4)}`);
export const encodeJavaScriptString = (text: string) => text && `"${encodeJavaScriptIdentifier(text)}"`;
// This is not great, but it is useful.
// NB: the original `security` package used /"(?:\\.|[^"])*"/, where `[^"]` also
// matches a backslash and so overlaps with `\\.`, causing exponential
// backtracking (ReDoS) on adversarial input. We exclude the backslash from the
// character class so the two alternatives are mutually exclusive — this matches
// exactly the same well-formed JSON string literals but in linear time.
const JSON_STRING_LITERAL_EXPRESSION = /"(?:[^"\\]|\\.)*"/gm;
export const encodeJavaScriptData = (object: any) => JSON.stringify(object).replace(JSON_STRING_LITERAL_EXPRESSION,
(string: string) => encodeJavaScriptString(JSON.parse(string)));
// OWASP Guidelines: escape all non alphanumeric characters in ASCII space.
const CSS_CHARACTERS_EXPRESSION = /[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\xFF]/gm;
export const encodeCSSIdentifier = (text: string) => text && text.replace(CSS_CHARACTERS_EXPRESSION,
(c: string) => `\\${(`000000${c.charCodeAt(0).toString(16)}`).slice(-6)}`);
export const encodeCSSString = (text: string) => text && `"${encodeCSSIdentifier(text)}"`;

View file

@ -159,8 +159,15 @@
</button>
<button type="button" id="history-rightstep" class="buttonicon buttonicon-step-forward">
</button>
<input type="range" id="history-slider-input" class="history-slider-input"
min="0" max="0" value="0" step="1">
<!-- Slider + saved-revision markers. The stars overlay sits above the
range track; pad_mode.ts fills it from the embedded timeslider's
savedRevisions so explicit "Save Revision" points stay visible in
in-pad history mode (issue #7946). -->
<span class="history-slider-wrap">
<input type="range" id="history-slider-input" class="history-slider-input"
min="0" max="0" value="0" step="1">
<span id="history-slider-stars" class="history-slider-stars" aria-hidden="true"></span>
</span>
<span id="history-timer" class="history-timer hide-for-mobile" aria-live="polite"></span>
<!-- Follow toggle: an icon-only button. The eye is always rendered;
the diagonal slash overlay is shown only when aria-pressed is
@ -384,18 +391,24 @@
</p>
<% e.end_block(); %>
</div>
<button class="btn btn-danger" data-l10n-id="pad.settings.deletePad" id="delete-pad">Delete pad</button>
</div><% } %>
</div>
<% if (!settings.requireAuthentication) { %>
<details id="delete-pad-with-token">
<!-- Pad deletion is independent of pad-wide settings (issue #7959):
both controls are always rendered and pad_editor.ts shows exactly
the one that applies. #delete-pad (token-less) is shown when
clientVars.canDeletePad — the creator on this device, or any user
when allowPadDeletionByAllUsers is on. The recovery-token
disclosure is shown when clientVars.canDeleteWithoutToken is false
(issue #7926): a creator on a second device, or under
requireAuthentication without a durable cross-device identity. -->
<button class="btn btn-danger" data-l10n-id="pad.settings.deletePad" id="delete-pad" hidden>Delete pad</button>
<details id="delete-pad-with-token" hidden>
<summary data-l10n-id="pad.deletionToken.deleteWithToken">Delete with token</summary>
<label for="delete-pad-token-input" data-l10n-id="pad.deletionToken.tokenFieldLabel">Pad deletion token</label>
<input type="password" id="delete-pad-token-input" autocomplete="off" spellcheck="false">
<button id="delete-pad-token-submit" type="button" class="btn btn-danger"
data-l10n-id="pad.settings.deletePad">Delete pad</button>
</details>
<% } %>
<h2 data-l10n-id="pad.settings.about">About</h2>
<span data-l10n-id="pad.settings.poweredBy">Powered by</span>
<a href="https://etherpad.org" target="_blank" referrerpolicy="no-referrer" rel="noopener">Etherpad</a>

View file

@ -0,0 +1,48 @@
'use strict';
// Unit coverage for PadManager.isValidPadId.
//
// isValidPadId is a pure function (a regex test), so this spec just requires
// PadManager and exercises it directly — no running database is needed.
// PadManager's import-time `require`s (DB, Pad, customError) only *define*
// things; the database connection happens lazily in DB.init(), so loading the
// module here has no side effects. This runs under the mocha backend suite
// (`--import=tsx`), where `require()` resolves the `.ts` sources natively.
import {strict as assert} from 'assert';
const padManager = require('../../../node/db/PadManager');
describe(__filename, function () {
describe('isValidPadId', function () {
it('accepts ordinary pad ids', async function () {
for (const id of [
'foo',
'TF-EVC',
'TF-LEC_IP03-EMS-CSM',
'a.b',
'.foo',
'foo.',
"a'b",
'g.s8oes9dhwrvt0zif$bar', // group pad
]) {
assert.equal(padManager.isValidPadId(id), true, `expected "${id}" to be valid`);
}
});
// Regression test for "Cannot GET /p/": a pad id that is a URL dot-segment
// ('.' or '..') is normalised away by the browser per the WHATWG URL
// standard ('/p/.' -> '/p/', '/p/..' -> '/'), so the pad can never be
// opened or exported. Such ids must be rejected. Before the fix
// isValidPadId returned true for both, so this test would fail.
it('rejects URL dot-segment pad ids that would be unreachable', async function () {
assert.equal(padManager.isValidPadId('.'), false);
assert.equal(padManager.isValidPadId('..'), false);
});
it('still rejects empty ids and ids containing "$"', async function () {
assert.equal(padManager.isValidPadId(''), false);
assert.equal(padManager.isValidPadId('a$b'), false);
});
});
});

View file

@ -177,4 +177,27 @@ describe(__filename, function () {
assert.ok(!names.includes(corruptId), `corrupt pad still listed: ${JSON.stringify(names)}`);
assert.ok(names.includes(goodId), `good pad missing after delete: ${JSON.stringify(names)}`);
});
// Regression for the isValidPadId tightening that rejects '.' and '..': a
// legacy pad with such an id predates the validation, so it still exists in
// the DB (doesPadExists is true → deletePad takes the "healthy" branch) but
// getPad() now throws on it. deletePad must fall back to the raw key purge
// instead of failing silently, otherwise the orphan is undeletable from the
// admin UI. Before the fallback this `ask()` never gets `results:deletePad`
// and times out.
it("a legacy '.' pad (now an invalid id) can still be deleted", async function () {
this.timeout(30000);
const dotId = '.';
// getPad('.') would now throw, so seed the record directly with a truthy
// `atext` so doesPadExists() returns true and the handler enters the branch
// where getPad() throws.
await db.set(`pad:${dotId}`, {atext: {text: '\n', attribs: ''}, pool: {}, head: -1, savedRevisions: []});
try {
const ack = await ask(socket, 'deletePad', dotId, 'results:deletePad');
assert.equal(ack, dotId, `expected deletePad to ack "${dotId}", got ${JSON.stringify(ack)}`);
} finally {
try { await db.remove(`pad:${dotId}`); } catch { /* ignore */ }
try { padManager.unloadPad(dotId); } catch { /* ignore */ }
}
});
});

View file

@ -80,6 +80,17 @@ describe(__filename, function () {
await callApi('deletePad', {padID: padId});
});
it('createPad returns null deletionToken when allowPadDeletionByAllUsers is on', async function () {
// Anyone can delete the pad with no token at all, so the recovery token is
// pointless — matches the socket/UI path (issue #7926).
settings.allowPadDeletionByAllUsers = true;
const padId = makeId();
const res = await callApi('createPad', {padID: padId});
assert.equal(res.body.code, 0, JSON.stringify(res.body));
assert.equal(res.body.data.deletionToken, null);
await callApi('deletePad', {padID: padId});
});
it('JWT admin call (no deletionToken) still works — admins stay trusted', async function () {
const padId = makeId();
await callApi('createPad', {padID: padId});

View file

@ -0,0 +1,44 @@
'use strict';
import {MapArrayType} from '../../../node/types/MapType';
import settings from '../../../node/utils/Settings';
const assert = require('assert').strict;
const common = require('../common');
// Regression coverage for issue #7959. The token-less "Delete pad" button
// (#delete-pad) used to be nested inside the `enablePadWideSettings`-gated
// pad-settings section, so disabling pad-wide settings removed the only way to
// delete a pad without a recovery token. Pad deletion is unrelated to pad-wide
// settings, so the button must be rendered regardless of that flag (its
// visibility is then driven at runtime by clientVars.canDeletePad).
describe(__filename, function () {
this.timeout(30000);
let agent: any;
const backup: MapArrayType<any> = {};
before(async function () { agent = await common.init(); });
beforeEach(async function () {
backup.enablePadWideSettings = settings.enablePadWideSettings;
});
afterEach(async function () {
settings.enablePadWideSettings = backup.enablePadWideSettings;
});
const hasDeletePadButton = (html: string): boolean =>
/id="delete-pad"/.test(html);
it('renders the Delete pad button with pad-wide settings enabled', async function () {
settings.enablePadWideSettings = true;
const res = await agent.get('/p/deleteUiPlacementOn').expect(200);
assert.equal(hasDeletePadButton(res.text), true);
});
it('renders the Delete pad button with pad-wide settings disabled (#7959)', async function () {
settings.enablePadWideSettings = false;
const res = await agent.get('/p/deleteUiPlacementOff').expect(200);
assert.equal(hasDeletePadButton(res.text), true);
});
});

View file

@ -0,0 +1,78 @@
'use strict';
const assert = require('assert').strict;
// The escaping helpers are a client module, but they are pure (no browser
// globals) so they can be exercised directly from a backend spec. This locks
// the byte-for-byte output of the helpers vendored from the (now removed)
// `security` npm package, so the vendoring can never silently drift.
const Security = require('../../../static/js/security');
describe(__filename, function () {
describe('public API', function () {
it('exposes the full set of helpers plugins may rely on', function () {
for (const fn of [
'escapeHTML', 'escapeHTMLAttribute',
'encodeJavaScriptIdentifier', 'encodeJavaScriptString', 'encodeJavaScriptData',
'encodeCSSIdentifier', 'encodeCSSString',
]) {
assert.equal(typeof Security[fn], 'function', `Security.${fn} must be a function`);
}
});
});
describe('escapeHTML', function () {
it('escapes &, <, >, ", \' and / per OWASP', function () {
assert.equal(Security.escapeHTML('<a href="x">/&\''),
'&lt;a href=&quot;x&quot;&gt;&#x2F;&amp;&#x27;');
});
it('neutralises a script tag', function () {
assert.equal(Security.escapeHTML('<script>alert(1)</script>'),
'&lt;script&gt;alert(1)&lt;&#x2F;script&gt;');
});
it('leaves plain alphanumerics untouched', function () {
assert.equal(Security.escapeHTML('Hello World 123'), 'Hello World 123');
});
it('passes falsy input straight through', function () {
assert.equal(Security.escapeHTML(''), '');
});
});
describe('escapeHTMLAttribute', function () {
it('hex-encodes non-alphanumeric ASCII not covered by named entities', function () {
assert.equal(Security.escapeHTMLAttribute('a b'), 'a&#x20;b');
// hex is lowercased, matching the original `security` package output.
assert.equal(Security.escapeHTMLAttribute('javascript:alert(1)'),
'javascript&#x3a;alert&#x28;1&#x29;');
});
it('prefers named entities for &, <, >, ", \', /', function () {
assert.equal(Security.escapeHTMLAttribute('<>&"\'/'),
'&lt;&gt;&amp;&quot;&#x27;&#x2F;');
});
it('leaves alphanumerics untouched', function () {
assert.equal(Security.escapeHTMLAttribute('abcXYZ0189'), 'abcXYZ0189');
});
});
describe('javascript / css encoders', function () {
it('encodeJavaScriptString quotes and backslash-u-escapes specials', function () {
assert.equal(Security.encodeJavaScriptString('a<b'), '"a\\u003cb"');
});
it('encodeCSSString quotes and backslash-escapes specials', function () {
assert.equal(Security.encodeCSSString('a;b'), '"a\\00003bb"');
});
it('encodeJavaScriptData escapes specials inside JSON string literals', function () {
assert.equal(
Security.encodeJavaScriptData({a: '<b>', c: 'x"y', d: 'a\\b'}),
'{"a":"\\u003cb\\u003e","c":"x\\u0022y","d":"a\\u005cb"}');
});
it('encodeJavaScriptData regex is linear (ReDoS guard)', function () {
// The JSON-string-literal regex used to be /"(?:\\.|[^"])*"/, which
// backtracks exponentially on an unterminated string of `\!` repeats.
// Run the regex directly on adversarial input and assert it returns fast.
const evil = `"${'\\!'.repeat(50000)}`; // no closing quote
const start = Date.now();
/"(?:[^"\\]|\\.)*"/gm.test(evil);
assert.ok(Date.now() - start < 1000, 'regex must not backtrack exponentially');
});
});
});

View file

@ -197,4 +197,143 @@ describe(__filename, function () {
assert.strictEqual(settings.padOptions.fadeInactiveAuthorColors, true);
});
});
// Regression test for ether/etherpad#7911.
// Air-gapped / firewalled deployments must be able to disable Etherpad's
// outbound calls (version check + plugin catalogue + self-updater) purely
// via environment variables, without editing settings.json inside the image.
// These assertions parse the *shipped* settings.json.docker so a future edit
// that drops the ${ENV} placeholders fails loudly here.
describe('offline / air-gapped env overrides (issue #7911)', function () {
const dockerSettings = path.join(__dirname, '../../../../settings.json.docker');
const templateSettings = path.join(__dirname, '../../../../settings.json.template');
const envVars = [
'PRIVACY_UPDATE_CHECK', 'PRIVACY_PLUGIN_CATALOG', 'UPDATES_TIER',
'UPDATES_SOURCE', 'UPDATES_CHANNEL', 'UPDATES_CHECK_INTERVAL_HOURS',
'UPDATES_GITHUB_REPO', 'UPDATES_REQUIRE_ADMIN_FOR_STATUS', 'UPDATE_SERVER',
];
const saved: {[k: string]: string | undefined} = {};
before(function () { for (const v of envVars) saved[v] = process.env[v]; });
afterEach(function () {
for (const v of envVars) {
if (saved[v] == null) delete process.env[v];
else process.env[v] = saved[v];
}
});
it('keeps shipped defaults when no env vars are set', function () {
for (const v of envVars) delete process.env[v];
const s = exportedForTestingOnly.parseSettings(dockerSettings, true);
assert.strictEqual(s!.privacy.updateCheck, true);
assert.strictEqual(s!.privacy.pluginCatalog, true);
assert.strictEqual(s!.updates.tier, 'notify');
assert.strictEqual(s!.updates.checkIntervalHours, 6);
assert.strictEqual(s!.updateServer, 'https://etherpad.org/ep_infos');
});
it('disables all outbound calls when the offline env vars are set', function () {
process.env.PRIVACY_UPDATE_CHECK = 'false';
process.env.PRIVACY_PLUGIN_CATALOG = 'false';
process.env.UPDATES_TIER = 'off';
const s = exportedForTestingOnly.parseSettings(dockerSettings, true);
// Coerced to real booleans, not the strings "false".
assert.strictEqual(s!.privacy.updateCheck, false);
assert.strictEqual(s!.privacy.pluginCatalog, false);
assert.strictEqual(s!.updates.tier, 'off');
});
it('honours the remaining updates.* and updateServer overrides', function () {
process.env.UPDATES_SOURCE = 'gitlab';
process.env.UPDATES_CHANNEL = 'beta';
process.env.UPDATES_CHECK_INTERVAL_HOURS = '24';
process.env.UPDATES_GITHUB_REPO = 'acme/etherpad-fork';
process.env.UPDATES_REQUIRE_ADMIN_FOR_STATUS = 'true';
process.env.UPDATE_SERVER = 'https://mirror.internal/ep_infos';
const s = exportedForTestingOnly.parseSettings(dockerSettings, true);
assert.strictEqual(s!.updates.source, 'gitlab');
assert.strictEqual(s!.updates.channel, 'beta');
assert.strictEqual(s!.updates.checkIntervalHours, 24); // numeric coercion
assert.strictEqual(s!.updates.githubRepo, 'acme/etherpad-fork');
assert.strictEqual(s!.updates.requireAdminForStatus, true); // boolean coercion
assert.strictEqual(s!.updateServer, 'https://mirror.internal/ep_infos');
});
// The source-install template carries the same placeholders so non-Docker
// deployments get the offline knobs too.
it('settings.json.template exposes the same offline overrides', function () {
for (const v of envVars) delete process.env[v];
const dflt = exportedForTestingOnly.parseSettings(templateSettings, true);
assert.strictEqual(dflt!.privacy.updateCheck, true);
assert.strictEqual(dflt!.privacy.pluginCatalog, true);
assert.strictEqual(dflt!.updates.tier, 'notify');
assert.strictEqual(dflt!.updateServer, 'https://etherpad.org/ep_infos');
process.env.UPDATES_TIER = 'off';
process.env.PRIVACY_UPDATE_CHECK = 'false';
process.env.PRIVACY_PLUGIN_CATALOG = 'false';
const over = exportedForTestingOnly.parseSettings(templateSettings, true);
assert.strictEqual(over!.updates.tier, 'off');
assert.strictEqual(over!.privacy.updateCheck, false);
assert.strictEqual(over!.privacy.pluginCatalog, false);
});
});
// Regression test for the jsonminify -> jsonc-parser migration.
// The old parser was `jsonminify(str).replace(',]', ']').replace(',}', '}')`,
// which had two correctness bugs that jsonc-parser (allowTrailingComma) fixes:
// 1. String#replace with a string needle only swaps the FIRST match, so a
// file with more than one trailing comma of the same kind stayed invalid.
// 2. The blind ',]' / ',}' replace also corrupted those byte sequences when
// they appeared *inside* a string value (e.g. a URL or literal text).
describe('JSONC settings parsing (jsonminify -> jsonc-parser)', function () {
const fs = require('fs');
const os = require('os');
let tmpFile: string;
const writeTmp = (contents: string) => {
tmpFile = path.join(os.tmpdir(), `ep-settings-jsonc-${process.pid}.json`);
fs.writeFileSync(tmpFile, contents);
return tmpFile;
};
afterEach(function () {
if (tmpFile) { try { fs.unlinkSync(tmpFile); } catch (e) { /* ignore */ } }
});
it('strips comments and tolerates multiple trailing commas', function () {
const file = writeTmp(`// leading line comment
/* block comment */
{
"list": [
"a",
"b",
],
"nested": [
[1, 2,],
[3, 4,],
],
"obj": {
"x": 1,
"y": 2,
},
}`);
const s: any = exportedForTestingOnly.parseSettings(file, true);
assert.deepEqual(s.list, ['a', 'b']);
assert.deepEqual(s.nested, [[1, 2], [3, 4]]);
assert.deepEqual(s.obj, {x: 1, y: 2});
});
it('does not corrupt ",]" / ",}" sequences inside string values', function () {
const file = writeTmp(`{
"url": "http://example.com/a,]b,}c",
"text": "trailing-comma-like ,] and ,} must survive"
}`);
const s: any = exportedForTestingOnly.parseSettings(file, true);
// The old replace() would have stripped the comma and produced
// "http://example.com/a]b}c" here.
assert.strictEqual(s.url, 'http://example.com/a,]b,}c');
assert.strictEqual(s.text, 'trailing-comma-like ,] and ,} must survive');
});
});
});

View file

@ -493,6 +493,8 @@ describe(__filename, function () {
});
describe('Pad deletion token issuance (#7926)', function () {
let getAuthorIdBackup: any;
const removeIfExists = async (padId: string) => {
if (await padManager.doesPadExist(padId)) {
const p = await padManager.getPad(padId);
@ -500,14 +502,32 @@ describe(__filename, function () {
}
};
// A getAuthorId hook that pins authorID to the authenticated username — the
// documented way (doc/api/hooks_server-side) to give a user a stable
// identity across cookie clears and devices. Its mere presence is what makes
// an authenticated session "durable" for token-suppression purposes.
const installStableIdentityHook = () => {
plugins.hooks.getAuthorId = [{hook_fn: async (hookName: string, context: any) => {
const username = context.user && context.user.username;
if (!username) return;
context.dbKey = `username=${username}`;
return '';
}}];
};
beforeEach(async function () {
// @ts-ignore - public setting toggled per test
settings.allowPadDeletionByAllUsers = false;
// The outer harness only backs up preAuthorize/authenticate/authorize, so
// manage getAuthorId ourselves to avoid leaking it into later specs.
getAuthorIdBackup = plugins.hooks.getAuthorId;
plugins.hooks.getAuthorId = [];
await removeIfExists('pad');
});
afterEach(async function () {
if (socket) socket.close();
socket = null;
plugins.hooks.getAuthorId = getAuthorIdBackup;
await removeIfExists('pad');
});
@ -519,6 +539,10 @@ describe(__filename, function () {
assert.equal(typeof cv.data.padDeletionToken, 'string',
'creator should get a token so the client can show the save-token modal');
assert.ok(cv.data.padDeletionToken.length >= 32);
assert.equal(cv.data.canDeleteWithoutToken, false);
// The creator can always delete without a token on this device, so the
// plain "Delete pad" button is offered (issue #7959).
assert.equal(cv.data.canDeletePad, true);
});
it('no token (and so no modal) when allowPadDeletionByAllUsers is true', async function () {
@ -532,7 +556,104 @@ describe(__filename, function () {
// client, so the "Save your pad deletion token" modal never appears. Anyone
// can already delete the pad without a token in this configuration.
assert.equal(cv.data.padDeletionToken, null);
assert.equal(cv.data.canDeleteWithoutToken, true);
assert.equal(cv.data.canDeletePad, true);
});
it('non-creator gets canDeletePad=false by default, true under allowPadDeletionByAllUsers (#7959)',
async function () {
const supertest = require('supertest');
// The creator (default cookie jar) establishes the pad's rev-0 author.
const resCreator = await agent.get('/p/pad').expect(200);
socket = await common.connect(resCreator);
const cvCreator: any = await common.handshake(socket, 'pad');
assert.equal(cvCreator.data.canDeletePad, true, 'creator can always delete');
// A different browser (separate cookie jar) is NOT the creator, so with
// allowPadDeletionByAllUsers off it must not be offered the token-less
// Delete pad button.
const otherBrowser = supertest(common.baseUrl);
const resOther = await otherBrowser.get('/p/pad').expect(200);
const otherSocket = await common.connect(resOther);
try {
const cvOther: any = await common.handshake(otherSocket, 'pad');
assert.equal(cvOther.data.canDeletePad, false,
'non-creator must not see Delete pad by default');
} finally {
otherSocket.close();
}
// With everyone opted in, the same non-creator CAN delete, so the
// button must be offered — independent of enablePadWideSettings (#7959).
// @ts-ignore - public setting toggled per test
settings.allowPadDeletionByAllUsers = true;
const otherBrowser2 = supertest(common.baseUrl);
const resOther2 = await otherBrowser2.get('/p/pad').expect(200);
const otherSocket2 = await common.connect(resOther2);
try {
const cvOther2: any = await common.handshake(otherSocket2, 'pad');
assert.equal(cvOther2.data.canDeletePad, true,
'allowPadDeletionByAllUsers must offer Delete pad to everyone');
} finally {
otherSocket2.close();
}
});
it('readonly viewer is denied canDeletePad and token-less deletion under allowPadDeletionByAllUsers (#7959)',
async function () {
// @ts-ignore - public setting toggled per test
settings.allowPadDeletionByAllUsers = true;
// Creator establishes the pad (rev-0 author) and yields its read-only id.
const resCreator = await agent.get('/p/pad').expect(200);
const creatorSocket = await common.connect(resCreator);
const cvCreator: any = await common.handshake(creatorSocket, 'pad');
const readOnlyId = cvCreator.data.readOnlyId;
assert.ok(readOnlyManager.isReadOnlyId(readOnlyId));
creatorSocket.close();
// A read-only viewer must NOT be offered the token-less delete button,
// even with deletion opened to all users — readonly viewers cannot edit,
// let alone delete (issue #7959).
const resRo = await agent.get(`/p/${readOnlyId}`).expect(200);
socket = await common.connect(resRo);
const cvRo: any = await common.handshake(socket, readOnlyId);
assert.equal(cvRo.data.readonly, true);
assert.equal(cvRo.data.canDeletePad, false,
'readonly viewers must not get the token-less Delete pad button');
// ...and the server must refuse a token-less PAD_DELETE from a readonly
// session, or allowPadDeletionByAllUsers becomes a data-loss hole.
await common.sendPadDelete(socket, {padId: 'pad'}).catch(() => {});
assert.ok(await padManager.doesPadExist('pad'),
'readonly session must not be able to delete the pad without a token');
});
it('authenticated creator WITHOUT a getAuthorId hook still gets a token', async function () {
// requireAuthentication alone is NOT durable: the authorID still comes from
// the per-browser token cookie, so this user would be stranded on a second
// device if the token were withheld. They must keep getting one.
settings.requireAuthentication = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await common.connect(res);
const cv: any = await common.handshake(socket, 'pad');
assert.equal(cv.type, 'CLIENT_VARS');
assert.equal(typeof cv.data.padDeletionToken, 'string');
assert.equal(cv.data.canDeleteWithoutToken, false);
assert.equal(cv.data.canDeletePad, true);
});
it('authenticated creator WITH a getAuthorId hook gets no token (durable identity)',
async function () {
settings.requireAuthentication = true;
installStableIdentityHook();
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
socket = await common.connect(res);
const cv: any = await common.handshake(socket, 'pad');
assert.equal(cv.type, 'CLIENT_VARS');
assert.equal(cv.data.padDeletionToken, null);
assert.equal(cv.data.canDeleteWithoutToken, true);
assert.equal(cv.data.canDeletePad, true);
});
});
describe('SocketIORouter.js', function () {

View file

@ -13,15 +13,16 @@
*/
const fs = require('fs');
const jsonminify = require('jsonminify');
const {parse: parseJsonc} = require('jsonc-parser');
function loadSettings() {
let settingsStr = fs.readFileSync(`${__dirname}/../../../settings.json.docker`).toString();
// try to parse the settings
try {
if (settingsStr) {
settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}');
const settings = JSON.parse(settingsStr);
// jsonc-parser tolerates the comments and trailing commas in the docker
// settings file, matching node/utils/Settings.ts.
const settings = parseJsonc(settingsStr, [], {allowTrailingComma: true});
// custom settings for running in a container
settings.ip = 'localhost';

View file

@ -3,7 +3,7 @@ import {goToNewPad, goToPad, sendChatMessage, showChat} from "../helper/padHelpe
import {showSettings} from "../helper/settingsHelper";
test.describe('creator-owned pad settings', () => {
test('shows pad settings only to the creator and keeps delete pad there', async ({page, browser}) => {
test('shows pad settings only to the creator; delete pad is creator-gated but separate', async ({page, browser}) => {
const padId = await goToNewPad(page);
const context2 = await browser.newContext();
@ -19,6 +19,9 @@ test.describe('creator-owned pad settings', () => {
await expect(page.locator('#pad-settings-section')).toBeVisible();
await expect(page.locator('#delete-pad')).toBeVisible();
await expect(page.locator('#padsettings-enforcecheck')).toBeVisible();
// The delete-pad button is no longer nested inside the pad-wide settings
// section: deletion is independent of enablePadWideSettings (issue #7959).
await expect(page.locator('#pad-settings-section #delete-pad')).toHaveCount(0);
await expect(page2.locator('#user-settings-section > h2')).toHaveText('User Settings');
await expect(page2.locator('#theme-toggle-row')).toBeVisible();

View file

@ -0,0 +1,53 @@
import {expect, Page, test} from "@playwright/test";
import {clearPadContent, goToNewPad, writeToPad} from "../helper/padHelper";
// Ported from the "jumps to a revision given in the url" case of the legacy
// timeslider_revisions.js (which no CI workflow ran). Re-targeted at the in-pad
// history model (#7659): a #rev/N hash on the pad URL boots straight into
// history mode at that revision (pad_mode.bootstrapFromHash), and the legacy
// #N shortlink form is still accepted for old bookmarks.
test.describe('timeslider deep link', function () {
test.describe.configure({mode: 'serial'});
test.beforeEach(async ({context}) => {
await context.clearCookies();
});
const expectHistoryAtRev0 = async (page: Page) => {
await expect(page.locator('body.history-mode')).toBeVisible({timeout: 15000});
await expect(page.locator('#history-controls')).toBeVisible();
// bootstrapFromHash canonicalizes any accepted hash form to #rev/0.
await expect.poll(() => new URL(page.url()).hash, {timeout: 15000}).toBe('#rev/0');
// The slider lands on revision 0 once pad_mode syncs from the embedded
// BroadcastSlider — the signal that we're actually viewing that revision.
await expect.poll(
async () => await page.locator('#history-slider-input').evaluate(
(el) => Number((el as HTMLInputElement).value)),
{timeout: 15000}).toBe(0);
};
test('#rev/N hash boots into history mode at that revision', async function ({page}) {
const padId = await goToNewPad(page);
await clearPadContent(page);
await writeToPad(page, 'One ');
await page.waitForTimeout(400);
await writeToPad(page, 'Two ');
await page.waitForTimeout(800);
// Deep-link to revision 0 of the same pad.
await page.goto(`http://localhost:9001/p/${padId}#rev/0`);
await expectHistoryAtRev0(page);
});
test('legacy #N shortlink hash still enters history mode', async function ({page}) {
const padId = await goToNewPad(page);
await clearPadContent(page);
await writeToPad(page, 'One ');
await page.waitForTimeout(400);
await writeToPad(page, 'Two ');
await page.waitForTimeout(800);
await page.goto(`http://localhost:9001/p/${padId}#0`);
await expectHistoryAtRev0(page);
});
});

View file

@ -0,0 +1,88 @@
import {expect, Page, test} from "@playwright/test";
import {clearPadContent, goToPad, writeToPad} from "../helper/padHelper";
// Ported from the legacy mocha suite (timeslider_numeric_padID.js and the
// "checks the export url" case in timeslider_revisions.js), neither of which
// ran in CI. Re-targeted at the in-pad history UI (#7659): the export links
// live in the outer #exportColumn and pad_mode.ts rewrites their hrefs to
// /p/<pad>/<rev>/export/<type> for the revision currently being viewed.
test.describe('timeslider export links', function () {
test.describe.configure({mode: 'serial'});
test.beforeEach(async ({context}) => {
await context.clearCookies();
});
// Suppress the one-time pad-deletion-token modal (same trick goToNewPad uses)
// so it can't steal focus mid-test on a creator session.
const suppressDeletionTokenModal = async (page: Page) => {
await page.addInitScript(() => {
let stored: unknown;
Object.defineProperty(window, 'clientVars', {
configurable: true,
get() { return stored; },
set(v) {
if (v != null && typeof v === 'object') {
(v as {padDeletionToken?: string | null}).padDeletionToken = null;
}
stored = v;
},
});
});
};
const enterHistoryMode = async (page: Page) => {
await page.click('.buttonicon-history');
await page.waitForSelector('#history-controls:not([hidden])', {state: 'visible'});
await page.waitForSelector('#history-frame');
};
const goToRevision = async (page: Page, rev: number) => {
await page.locator('#history-slider-input').evaluate((el, value) => {
(el as HTMLInputElement).value = String(value);
el.dispatchEvent(new Event('input', {bubbles: true}));
}, rev);
await expect(page.locator('#history-banner-rev')).toHaveText(`Version ${rev}`, {timeout: 15000});
};
const exportHref = (page: Page, id: string) =>
page.locator(`#${id}`).getAttribute('href');
test('export hrefs target the viewed revision, including a numeric pad id', async function ({page}) {
// A numeric pad id is the specific case the legacy test guarded — the
// href rewriter must not confuse it with the revision segment. Use a
// high-entropy numeric id (timestamp + random) so reruns against a
// persistent DB can't collide on the same pad.
const padId = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
await suppressDeletionTokenModal(page);
await goToPad(page, padId); // navigates and waits for the editor to be ready
await clearPadContent(page);
await writeToPad(page, 'One ');
await page.waitForTimeout(400);
await writeToPad(page, 'Two ');
await page.waitForTimeout(800);
await enterHistoryMode(page);
// Wait for pad_mode to sync the slider max from the embedded BroadcastSlider.
await expect.poll(
async () => await page.locator('#history-slider-input').evaluate(
(el) => Number((el as HTMLInputElement).max)),
{timeout: 15000}).toBeGreaterThan(0);
const maxRev = await page.locator('#history-slider-input').evaluate(
(el) => Number((el as HTMLInputElement).max));
expect(maxRev).toBeGreaterThan(0);
// On entry the slider is at the latest revision; hrefs point there.
await expect.poll(() => exportHref(page, 'exporthtmla'), {timeout: 15000})
.toContain(`/${padId}/${maxRev}/export/html`);
expect(await exportHref(page, 'exportplaina')).toContain(`/${padId}/${maxRev}/export/txt`);
// Scrub to revision 0 — the export targets must follow.
await goToRevision(page, 0);
await expect.poll(() => exportHref(page, 'exporthtmla'), {timeout: 15000})
.toContain(`/${padId}/0/export/html`);
expect(await exportHref(page, 'exportplaina')).toContain(`/${padId}/0/export/txt`);
});
});

View file

@ -0,0 +1,81 @@
import {expect, Page, test} from "@playwright/test";
import {clearPadContent, goToNewPad, writeToPad} from "../helper/padHelper";
// Ported from the legacy mocha suite (src/tests/frontend/specs/timeslider_labels.js),
// which no CI workflow ran. Re-targeted at the in-pad history UI (#7659): the
// revision label and date are shown in the outer #history-banner, populated by
// pad_mode.ts mirroring the embedded timeslider's #revision_label / #revision_date.
// This guards the banner bridge against silently breaking again (cf. #7946).
test.describe('timeslider revision labels', function () {
test.describe.configure({mode: 'serial'});
// The "Version N" label and "Saved <Month> <day>, <year>" date are localized
// (timeslider.version / timeslider.saved). Pin the locale so the assertions
// are deterministic and the date string stays Date-parseable.
test.use({locale: 'en-US'});
test.beforeEach(async ({context}) => {
await context.clearCookies();
});
const enterHistoryMode = async (page: Page) => {
await page.click('.buttonicon-history');
await page.waitForSelector('#history-controls:not([hidden])', {state: 'visible'});
await page.waitForSelector('#history-frame');
};
// Drive the outer slider (a remote control for the embedded BroadcastSlider)
// to a specific revision and wait for the banner to reflect it.
const goToRevision = async (page: Page, rev: number) => {
await page.locator('#history-slider-input').evaluate((el, value) => {
(el as HTMLInputElement).value = String(value);
el.dispatchEvent(new Event('input', {bubbles: true}));
}, rev);
await expect(page.locator('#history-banner-rev')).toHaveText(`Version ${rev}`, {timeout: 15000});
};
// "Saved June 12, 2026" -> a parseable Date (the banner mirrors the
// timeslider.saved l10n string "Saved {{month}} {{day}}, {{year}}").
const parsedBannerDate = async (page: Page) => await page.locator('#history-banner-date').evaluate(
(el) => new Date((el.textContent || '').replace(/^Saved\s+/i, '')).getTime());
test('shows Version label and a valid date that update while scrubbing', async function ({page}) {
await goToNewPad(page);
await clearPadContent(page);
// Produce a few revisions.
await writeToPad(page, 'Alpha ');
await page.waitForTimeout(400);
await writeToPad(page, 'Beta ');
await page.waitForTimeout(400);
await writeToPad(page, 'Gamma ');
await page.waitForTimeout(800);
await enterHistoryMode(page);
// On entry the slider sits at the latest revision; the banner must show a
// non-empty "Version N" label and a non-NaN date. Wait for pad_mode to sync
// the slider max from the embedded BroadcastSlider before reading it.
await expect.poll(
async () => await page.locator('#history-slider-input').evaluate(
(el) => Number((el as HTMLInputElement).max)),
{timeout: 15000}).toBeGreaterThan(0);
const maxRev = await page.locator('#history-slider-input').evaluate(
(el) => Number((el as HTMLInputElement).max));
expect(maxRev).toBeGreaterThan(0);
await expect(page.locator('#history-banner-rev')).toHaveText(`Version ${maxRev}`);
const dateLast = await parsedBannerDate(page);
expect(Number.isNaN(dateLast)).toBe(false);
// The mirrored timer must also be a real, non-NaN datetime.
const timerLast = await page.locator('#history-timer').textContent();
expect(Number.isNaN(new Date(timerLast || '').getTime())).toBe(false);
// Scrub back to revision 0 — label and date must update.
await goToRevision(page, 0);
await expect(page.locator('#history-banner-rev')).toHaveText('Version 0');
const dateFirst = await parsedBannerDate(page);
expect(Number.isNaN(dateFirst)).toBe(false);
// The latest revision is never older than revision 0.
expect(dateLast).toBeGreaterThanOrEqual(dateFirst);
});
});

View file

@ -0,0 +1,97 @@
import {expect, Page, test} from "@playwright/test";
import {clearPadContent, goToNewPad, goToPad, writeToPad} from "../helper/padHelper";
// Regression coverage for #7946: after #7659 moved the timeslider into the pad
// as an embedded iframe, the user-facing control became the outer
// #history-slider-input range input. Saved revisions ("Save Revision" button)
// are still drawn as stars inside the now-hidden iframe slider, so they
// stopped appearing for users. pad_mode.ts must bridge the saved revisions out
// onto the outer slider as markers.
test.describe('timeslider saved-revision markers', function () {
test.describe.configure({mode: 'serial'});
test.beforeEach(async ({context}) => {
await context.clearCookies();
});
const enterHistoryMode = async (page: Page) => {
await page.click('.buttonicon-history');
await page.waitForSelector('#history-controls:not([hidden])', {state: 'visible'});
await page.waitForSelector('#history-frame');
};
test('a saved revision shows a marker on the outer history slider', async function ({page}) {
await goToNewPad(page);
await clearPadContent(page);
// Build a few revisions, save one partway through, then add more so the
// saved marker lands in the middle of the slider (not under the thumb).
await writeToPad(page, 'One ');
await page.waitForTimeout(400);
await writeToPad(page, 'Two ');
await page.waitForTimeout(600);
// Save a revision at the current head.
await page.click('.buttonicon-savedRevision');
// The save confirmation gritter carries class `saved-revision`.
await page.waitForSelector('.saved-revision', {state: 'visible'});
// Add more edits so head advances past the saved revision.
await writeToPad(page, 'Three ');
await page.waitForTimeout(400);
await writeToPad(page, 'Four ');
await page.waitForTimeout(800);
await enterHistoryMode(page);
// The outer slider must show at least one visible saved-revision marker.
const marker = page.locator('.history-star');
await expect(marker.first()).toBeVisible({timeout: 15000});
expect(await marker.count()).toBeGreaterThanOrEqual(1);
// The marker must be positioned within the slider track, not collapsed to
// the origin. Assert on the inline left percentage directly (markers are
// always positioned via style.left = "N%"); a fallback to a layout-derived
// coordinate would let a degenerate left:0% render still pass.
const leftPct = await marker.first().evaluate(
(el) => parseFloat((el as HTMLElement).style.left));
expect(leftPct).toBeGreaterThan(0);
expect(leftPct).toBeLessThan(100);
});
test('a revision saved live appears on an already-open history slider', async function ({browser}) {
// Reviewer (history viewer).
const ctxA = await browser.newContext();
const pageA = await ctxA.newPage();
const padId = await goToNewPad(pageA);
await clearPadContent(pageA);
await writeToPad(pageA, 'Alpha Beta ');
await pageA.waitForTimeout(600);
await pageA.click('.buttonicon-savedRevision');
await pageA.waitForSelector('.saved-revision', {state: 'visible'});
await writeToPad(pageA, 'Gamma Delta ');
await pageA.waitForTimeout(800);
await enterHistoryMode(pageA);
await expect(pageA.locator('.history-star').first()).toBeVisible({timeout: 15000});
const before = await pageA.locator('.history-star').count();
// A second collaborator keeps editing the live pad and saves a revision
// while pageA is sitting in history mode. The server must broadcast
// NEW_SAVEDREV so pageA's open timeslider gains a marker without reloading.
const ctxB = await browser.newContext();
const pageB = await ctxB.newPage();
await goToPad(pageB, padId);
await writeToPad(pageB, 'Epsilon Zeta Eta ');
await pageB.waitForTimeout(800);
await pageB.click('.buttonicon-savedRevision');
await pageB.waitForSelector('.saved-revision', {state: 'visible'});
await expect.poll(
async () => await pageA.locator('.history-star').count(),
{timeout: 20000})
.toBeGreaterThan(before);
await ctxA.close();
await ctxB.close();
});
});

View file

@ -1,65 +0,0 @@
'use strict';
describe('timeslider', function () {
// create a new pad before each test run
beforeEach(async function () {
await helper.aNewPad();
});
/**
* @todo test authorsList
*/
it("Shows a date/time in the timeslider and make sure it doesn't include NaN", async function () {
this.timeout(12000);
// make some changes to produce 3 revisions
const revs = 3;
for (let i = 0; i < revs; i++) {
await helper.edit('a\n');
}
await helper.gotoTimeslider(revs);
await helper.waitForPromise(() => helper.contentWindow().location.hash === `#${revs}`);
// the datetime of last edit
const timerTimeLast = new Date(helper.timesliderTimerTime()).getTime();
// the day of this revision, e.g. August 12, 2020 (stripped the string "Saved")
const dateLast = new Date(helper.revisionDateElem().substr(6)).getTime();
// the label/revision, ie Version 3
const labelLast = helper.revisionLabelElem().text();
// the datetime should be a date
expect(Number.isNaN(timerTimeLast)).to.eql(false);
// the Date object of the day should not be NaN
expect(Number.isNaN(dateLast)).to.eql(false);
// the label should be Version `Number`
expect(labelLast).to.be(`Version ${revs}`);
// Click somewhere left on the timeslider to go to revision 0
helper.sliderClick(1);
// the datetime of last edit
const timerTime = new Date(helper.timesliderTimerTime()).getTime();
// the day of this revision, e.g. August 12, 2020
const date = new Date(helper.revisionDateElem().substr(6)).getTime();
// the label/revision, e.g. Version 0
const label = helper.revisionLabelElem().text();
// the datetime should be a date
expect(Number.isNaN(timerTime)).to.eql(false);
// the last revision should be newer or have the same time
expect(timerTimeLast).to.not.be.lessThan(timerTime);
// the Date object of the day should not be NaN
expect(Number.isNaN(date)).to.eql(false);
// the label should be Version 0
expect(label).to.be('Version 0');
});
});

View file

@ -1,31 +0,0 @@
'use strict';
describe('timeslider', function () {
const padId = 735773577357 + (Math.round(Math.random() * 1000));
// create a new pad before each test run
beforeEach(async function () {
await helper.aNewPad({id: padId});
});
it('Makes sure the export URIs are as expected when the padID is numeric', async function () {
await helper.edit('a\n');
await helper.gotoTimeslider(1);
// ensure we are on revision 1
await helper.waitForPromise(() => helper.contentWindow().location.hash === '#1');
// expect URI to be similar to
// http://192.168.1.48:9001/p/2/1/export/html
// http://192.168.1.48:9001/p/735773577399/1/export/html
const rev1ExportLink = helper.contentWindow().$('#exporthtmla').attr('href');
expect(rev1ExportLink).to.contain('/1/export/html');
// Click somewhere left on the timeslider to go to revision 0
helper.sliderClick(30);
const rev0ExportLink = helper.contentWindow().$('#exporthtmla').attr('href');
expect(rev0ExportLink).to.contain('/0/export/html');
});
});

View file

@ -1,150 +0,0 @@
'use strict';
describe('timeslider', function () {
// create a new pad before each test run
beforeEach(async function () {
await helper.aNewPad();
});
it('loads adds a hundred revisions', async function () {
this.timeout(100000);
const chrome$ = helper.padChrome$;
// Create a bunch of revisions.
for (let i = 0; i < 99; i++) await helper.edit('a');
chrome$('.buttonicon-savedRevision').trigger('click');
await helper.waitForPromise(() => helper.padChrome$('.saved-revision').length > 0);
// Give some time to send the SAVE_REVISION message to the server before navigating away.
await new Promise((resolve) => setTimeout(resolve, 100));
// go to timeslider
$('#iframe-container iframe')
.attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`);
await new Promise((resolve) => setTimeout(resolve, 6000));
const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
const $sliderBar = timeslider$('#ui-slider-bar');
const latestContents = timeslider$('#innerdocbody').text();
// Click somewhere on the timeslider
let e = new jQuery.Event('mousedown');
// sets y co-ordinate of the pad slider modal.
const base = (timeslider$('#ui-slider-bar').offset().top - 24);
e.clientX = e.pageX = 150;
e.clientY = e.pageY = base + 5;
$sliderBar.trigger(e);
e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = base;
$sliderBar.trigger(e);
e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = base - 5;
$sliderBar.trigger(e);
$sliderBar.trigger('mouseup');
await new Promise((resolve) => setTimeout(resolve, 1000));
// make sure the text has changed
expect(timeslider$('#innerdocbody').text()).not.to.eql(latestContents);
const starIsVisible = timeslider$('.star').is(':visible');
expect(starIsVisible).to.eql(true);
});
// Disabled as jquery trigger no longer works properly
xit('changes the url when clicking on the timeslider', async function () {
// Create some revisions.
for (let i = 0; i < 20; i++) await helper.edit('a');
// go to timeslider
$('#iframe-container iframe')
.attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`);
await new Promise((resolve) => setTimeout(resolve, 6000));
const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
const $sliderBar = timeslider$('#ui-slider-bar');
const oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash;
// Click somewhere on the timeslider
const e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = 60;
$sliderBar.trigger(e);
await helper.waitForPromise(
() => $('#iframe-container iframe')[0].contentWindow.location.hash !== oldUrl, 6000);
});
it('jumps to a revision given in the url', async function () {
const inner$ = helper.padInner$;
this.timeout(40000);
// wait for the text to be loaded
await helper.waitForPromise(() => inner$('body').text().length !== 0, 10000);
const newLines = inner$('body div').length;
const oldLength = inner$('body').text().length + newLines / 2;
expect(oldLength).to.not.eql(0);
inner$('div').first().sendkeys('a');
let timeslider$;
// wait for our additional revision to be added
await helper.waitForPromise(() => {
// newLines takes the new lines into account which are strippen when using
// inner$('body').text(), one <div> is used for one line in ACE.
const lenOkay = inner$('body').text().length + newLines / 2 !== oldLength;
// this waits for the color to be added to our <span>, which means that the revision
// was accepted by the server.
const colorOkay = inner$('span').first().attr('class').indexOf('author-') === 0;
return lenOkay && colorOkay;
}, 10000);
// go to timeslider with a specific revision set
$('#iframe-container iframe')
.attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`);
// wait for the timeslider to be loaded
await helper.waitForPromise(() => {
try {
timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
} catch (e) {
// Empty catch block <3
}
return timeslider$ && timeslider$('#innerdocbody').text().length === oldLength;
}, 10000);
});
it('checks the export url', async function () {
const inner$ = helper.padInner$;
this.timeout(11000);
inner$('div').first().sendkeys('a');
await new Promise((resolve) => setTimeout(resolve, 2500));
// go to timeslider
$('#iframe-container iframe')
.attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`);
let timeslider$;
let exportLink;
await helper.waitForPromise(() => {
try {
timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
} catch (e) {
// Empty catch block <3
}
if (!timeslider$) return false;
exportLink = timeslider$('#exportplaina').attr('href');
if (!exportLink) return false;
return exportLink.substr(exportLink.length - 12) === '0/export/txt';
}, 6000);
});
});

View file

@ -12,6 +12,6 @@
"devDependencies": {
"ep_etherpad-lite": "workspace:../src",
"typescript": "^6.0.3",
"vite": "^8.0.16"
"vite": "^8.1.4"
}
}