Compare commits

...

49 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
48 changed files with 3544 additions and 1857 deletions

View file

@ -17,4 +17,11 @@ updates:
open-pull-requests-limit: 30 open-pull-requests-limit: 30
groups: groups:
dev-dependencies: 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

@ -33,7 +33,7 @@ jobs:
- -
name: Checkout repository name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}
@ -51,7 +51,7 @@ jobs:
cache: pnpm cache: pnpm
- -
name: Install libreoffice name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.6.1 uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with: with:
packages: libreoffice libreoffice-pdfimport packages: libreoffice libreoffice-pdfimport
version: 1.0 version: 1.0
@ -89,7 +89,7 @@ jobs:
- -
name: Checkout repository name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}
@ -107,7 +107,7 @@ jobs:
cache: pnpm cache: pnpm
- -
name: Install libreoffice name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.6.1 uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with: with:
packages: libreoffice libreoffice-pdfimport packages: libreoffice libreoffice-pdfimport
version: 1.0 version: 1.0
@ -169,7 +169,7 @@ jobs:
- -
name: Checkout repository name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}
@ -235,7 +235,7 @@ jobs:
- -
name: Checkout repository name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}

View file

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

View file

@ -42,7 +42,7 @@ jobs:
tags: ${{ env.TEST_TAG }} tags: ${{ env.TEST_TAG }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}

View file

@ -28,7 +28,7 @@ jobs:
- name: Checkout core (PR) - name: Checkout core (PR)
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}

View file

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

View file

@ -21,14 +21,14 @@ jobs:
- -
name: Checkout repository name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm-store- ${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache Playwright browsers name: Cache Playwright browsers
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright
@ -93,14 +93,14 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm-store- ${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache Playwright browsers name: Cache Playwright browsers
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright
@ -169,14 +169,14 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm-store- ${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache Playwright browsers name: Cache Playwright browsers
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright
@ -270,14 +270,14 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pnpm-store- ${{ runner.os }}-pnpm-store-
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache Playwright browsers name: Cache Playwright browsers
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright

View file

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

View file

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

View file

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

View file

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

View file

@ -47,7 +47,7 @@ jobs:
repository: ether/ether.github.com repository: ether/ether.github.com
path: ether.github.com path: ether.github.com
token: '${{ secrets.ETHER_RELEASE_TOKEN }}' token: '${{ secrets.ETHER_RELEASE_TOKEN }}'
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}
@ -82,7 +82,7 @@ jobs:
with: with:
ruby-version: 2.7 ruby-version: 2.7
- uses: reitzig/actions-asciidoctor@v2.0.4 - uses: reitzig/actions-asciidoctor@v2.0.5
with: with:
version: 2.0.18 version: 2.0.18
- name: Prepare release - name: Prepare release

View file

@ -54,7 +54,7 @@ jobs:
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- name: Upgrade npm to >=11.5.1 (required for trusted publishing) - name: Upgrade npm to >=11.5.1 (required for trusted publishing)
run: npm install -g npm@latest run: npm install -g npm@latest
- uses: actions/cache@v5 - uses: actions/cache@v6
name: Cache pnpm store name: Cache pnpm store
with: with:
path: ${{ env.PNPM_HOME }} path: ${{ env.PNPM_HOME }}

View file

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

View file

@ -14,39 +14,39 @@
"test": "pnpm gen:api && tsx --test 'src/**/__tests__/*.test.ts' 'src/**/__tests__/*.test.tsx'" "test": "pnpm gen:api && tsx --test 'src/**/__tests__/*.test.ts' 'src/**/__tests__/*.test.tsx'"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-switch": "^1.3.1", "@radix-ui/react-switch": "^1.3.3",
"@tanstack/react-query": "^5.101.0", "@tanstack/react-query": "^5.101.2",
"@tanstack/react-query-devtools": "^5.101.0", "@tanstack/react-query-devtools": "^5.101.2",
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",
"openapi-react-query": "^0.5.4" "openapi-react-query": "^0.5.4"
}, },
"devDependencies": { "devDependencies": {
"@radix-ui/react-dialog": "^1.1.17", "@radix-ui/react-dialog": "^1.1.19",
"@radix-ui/react-toast": "^1.2.17", "@radix-ui/react-toast": "^1.2.19",
"@radix-ui/react-visually-hidden": "^1.2.6", "@radix-ui/react-visually-hidden": "^1.2.7",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.61.1", "@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.61.1", "@typescript-eslint/parser": "^8.63.0",
"@vitejs/plugin-react": "^6.0.2", "@vitejs/plugin-react": "^6.0.3",
"babel-plugin-react-compiler": "19.1.0-rc.3", "babel-plugin-react-compiler": "19.1.0-rc.3",
"eslint": "^10.5.0", "eslint": "^10.6.0",
"eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3", "eslint-plugin-react-refresh": "^0.5.3",
"i18next": "^26.3.1", "i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.21.0", "lucide-react": "^1.23.0",
"openapi-typescript": "^7.13.0", "openapi-typescript": "^7.13.0",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-hook-form": "^7.80.0", "react-hook-form": "^7.81.0",
"react-i18next": "^17.0.8", "react-i18next": "^17.0.9",
"react-router-dom": "^7.18.0", "react-router-dom": "^7.18.1",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"tsx": "^4.22.4", "tsx": "^4.23.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.0.16", "vite": "^8.1.4",
"vite-plugin-babel": "^1.7.3", "vite-plugin-babel": "^1.7.3",
"zustand": "^5.0.14" "zustand": "^5.0.14"
} }

View file

@ -9,12 +9,12 @@
"dependencies": { "dependencies": {
"ep_etherpad-lite": "workspace:../src", "ep_etherpad-lite": "workspace:../src",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"semver": "^7.8.4", "semver": "^7.8.5",
"tsx": "^4.22.4", "tsx": "^4.23.0",
"ueberdb2": "6.1.13" "ueberdb2": "6.1.16"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^26.0.0", "@types/node": "^26.1.1",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"typescript": "^6.0.3" "typescript": "^6.0.3"
}, },

View file

@ -27,13 +27,18 @@ export default defineConfig({
items: [ items: [
{ text: 'Docker', link: '/docker.md' }, { text: 'Docker', link: '/docker.md' },
{ text: 'Configuration', link: '/configuration.md' }, { text: 'Configuration', link: '/configuration.md' },
{ text: 'Deployment', link: '/deployment.md' },
{ text: 'Database', link: '/database.md' },
{ text: 'Localization', link: '/localization.md' }, { text: 'Localization', link: '/localization.md' },
{ text: 'Cookies', link: '/cookies.md' }, { text: 'Cookies', link: '/cookies.md' },
{ text: 'Plugins', link: '/plugins.md' }, { text: 'Plugins', link: '/plugins.md' },
{ text: 'Stats', link: '/stats.md' }, { text: 'Stats', link: '/stats.md' },
{text: 'Skins', link: '/skins.md' }, {text: 'Skins', link: '/skins.md' },
{ text: 'Accessibility', link: '/accessibility.md' },
{text: 'Demo', link: '/demo.md' }, {text: 'Demo', link: '/demo.md' },
{text: 'CLI', link: '/cli.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

@ -1,7 +1,7 @@
# Changeset Library # Changeset Library
The [changeset 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. provides tools to create, read, and apply changesets.
## Changeset ## Changeset
@ -21,6 +21,42 @@ A transmitted changeset looks like this:
'Z:z>1|2=m=b*0|1+1$\n' '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 ## Attribute Pool
```javascript ```javascript
@ -36,6 +72,59 @@ are used many times.
There is one attribute pool per pad, and it includes every current and There is one attribute pool per pad, and it includes every current and
historical attribute used in the pad. 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 ## Further Reading
Detailed information about the changesets & Easysync protocol: 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.

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": { "devDependencies": {
"oxc-minify": "^0.137.0", "oxc-minify": "^0.139.0",
"vitepress": "^2.0.0-alpha.17" "vitepress": "^2.0.0-alpha.18"
}, },
"scripts": { "scripts": {
"docs:dev": "vitepress dev", "docs:dev": "vitepress dev",

View file

@ -45,7 +45,7 @@
"node": ">=24.0.0", "node": ">=24.0.0",
"pnpm": ">=11.1.2" "pnpm": ">=11.1.2"
}, },
"packageManager": "pnpm@11.1.2", "packageManager": "pnpm@11.10.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/ether/etherpad.git" "url": "https://github.com/ether/etherpad.git"

3336
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -44,3 +44,15 @@ overrides:
uuid@<14.0.0: '>=14.0.0' uuid@<14.0.0: '>=14.0.0'
vite@>=7.0.0 <7.3.2: '>=7.3.2' vite@>=7.0.0 <7.3.2: '>=7.3.2'
ws@>=8.0.0 <8.21.0: '>=8.21.0' 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

@ -124,7 +124,7 @@
"pad.importExport.exportword": "مايكروسوفت وورد", "pad.importExport.exportword": "مايكروسوفت وورد",
"pad.importExport.exportpdf": "صيغة المستندات المحمولة", "pad.importExport.exportpdf": "صيغة المستندات المحمولة",
"pad.importExport.exportopen": "ODF (نسق المستند المفتوح)", "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.connected": "متصل.",
"pad.modals.reconnecting": "إعادة الاتصال ببادك..", "pad.modals.reconnecting": "إعادة الاتصال ببادك..",
"pad.modals.forcereconnect": "فرض إعادة الاتصال", "pad.modals.forcereconnect": "فرض إعادة الاتصال",

View file

@ -16,13 +16,80 @@
] ]
}, },
"admin.page-title": "Ovládací panel Správce - Etherpad", "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": "Správce zásuvných moodulů",
"admin_plugins.available": "Dostupné zásuvné moduly", "admin_plugins.available": "Dostupné zásuvné moduly",
"admin_plugins.available_not-found": "Nejsou žádné zásuvné moduly", "admin_plugins.available_not-found": "Nejsou žádné zásuvné moduly",
"admin_plugins.available_fetching": "Načítání...", "admin_plugins.available_fetching": "Načítání...",
"admin_plugins.available_install.value": "Instalovat", "admin_plugins.available_install.value": "Instalovat",
"admin_plugins.available_search.placeholder": "Vyhledat zásuvné moduly k instalaci", "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.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": "Nainstalované zásuvné moduly",
"admin_plugins.installed_fetching": "Načítání instalovaných zásuvných modulů...", "admin_plugins.installed_fetching": "Načítání instalovaných zásuvných modulů...",
"admin_plugins.installed_nothing": "Dosud jste nenainstalovali žádné zásuvné moduly.", "admin_plugins.installed_nothing": "Dosud jste nenainstalovali žádné zásuvné moduly.",
@ -30,23 +97,61 @@
"admin_plugins.last-update": "Poslední aktualizace", "admin_plugins.last-update": "Poslední aktualizace",
"admin_plugins.name": "Název", "admin_plugins.name": "Název",
"admin_plugins.page-title": "Správce zásuvných modulů - Etherpad", "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.version": "Verze",
"admin_plugins_info": "Informace o řešení problému", "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": "Instalované hooks",
"admin_plugins_info.hooks_client": "hooks na straně klienta", "admin_plugins_info.hooks_client": "hooks na straně klienta",
"admin_plugins_info.hooks_server": "hooks na straně serveru", "admin_plugins_info.hooks_server": "hooks na straně serveru",
"admin_plugins_info.parts": "Nainstalované součásti", "admin_plugins_info.parts": "Nainstalované součásti",
"admin_plugins_info.plugins": "Nainstalované zásuvné moduly", "admin_plugins_info.plugins": "Nainstalované zásuvné moduly",
"admin_plugins_info.page-title": "Informace o zásuvných modulech - Etherpad", "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": "Verze Etherpad",
"admin_plugins_info.version_latest": "Poslední dostupná verze", "admin_plugins_info.version_latest": "Poslední dostupná verze",
"admin_plugins_info.version_number": "Číslo verze", "admin_plugins_info.version_number": "Číslo verze",
"admin_settings": "Nastavení", "admin_settings": "Nastavení",
"admin_settings.create_pad": "Vytvořit pad",
"admin_settings.current": "Aktuální konfugurace", "admin_settings.current": "Aktuální konfugurace",
"admin_settings.current_example-devel": "Příklad ukázkové vývojové šablony", "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_example-prod": "Příklad šablony nastavení výroby",
"admin_settings.current_restart.value": "Restartovat Etherpad", "admin_settings.current_restart.value": "Restartovat Etherpad",
"admin_settings.current_save.value": "Uložit nastavení", "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", "admin_settings.page-title": "Nastavení - Etherpad",
"index.newPad": "Založ nový Pad", "index.newPad": "Založ nový Pad",
"index.settings": "Nastavení", "index.settings": "Nastavení",

View file

@ -7,6 +7,7 @@
"Chpol", "Chpol",
"Cquoi", "Cquoi",
"Crochet.david", "Crochet.david",
"Crowwhailord",
"Derugon", "Derugon",
"Envlh", "Envlh",
"Framafan", "Framafan",
@ -259,7 +260,7 @@
"pad.importExport.exportetherpada.title": "Exporter au format Etherpad", "pad.importExport.exportetherpada.title": "Exporter au format Etherpad",
"pad.importExport.exporthtmla.title": "Exporter au format HTML", "pad.importExport.exporthtmla.title": "Exporter au format HTML",
"pad.importExport.exportplaina.title": "Exporter en texte brut", "pad.importExport.exportplaina.title": "Exporter en texte brut",
"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.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.connected": "Connecté.",
"pad.modals.reconnecting": "Reconnexion à votre bloc-notes en cours...", "pad.modals.reconnecting": "Reconnexion à votre bloc-notes en cours...",
"pad.modals.forcereconnect": "Forcer la reconnexion", "pad.modals.forcereconnect": "Forcer la reconnexion",

View file

@ -302,7 +302,7 @@
"pad.importExport.exportworda.title": "Esporta come Microsoft Word", "pad.importExport.exportworda.title": "Esporta come Microsoft Word",
"pad.importExport.exportpdfa.title": "Esporta in formato PDF", "pad.importExport.exportpdfa.title": "Esporta in formato PDF",
"pad.importExport.exportopena.title": "Esporta in formato ODF (Open Document Format)", "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.connected": "Connesso.",
"pad.modals.reconnecting": "Riconnessione al pad in corso…", "pad.modals.reconnecting": "Riconnessione al pad in corso…",
"pad.modals.forcereconnect": "Forza la riconnessione", "pad.modals.forcereconnect": "Forza la riconnessione",

View file

@ -6,6 +6,7 @@
"Chqaz", "Chqaz",
"Omotecho", "Omotecho",
"Shirayuki", "Shirayuki",
"Tensama0415",
"Torinky" "Torinky"
] ]
}, },
@ -30,7 +31,7 @@
"admin_plugins_info.hooks_client": "クライアント側のフック", "admin_plugins_info.hooks_client": "クライアント側のフック",
"admin_plugins_info.hooks_server": "サーバー側のフック", "admin_plugins_info.hooks_server": "サーバー側のフック",
"index.newPad": "新規作成", "index.newPad": "新規作成",
"index.createOpenPad": "または作成/編集するパッド名を入力:", "index.createOpenPad": "編集するパッド名を入力",
"index.openPad": "次の名称の既存の Pad を開く:", "index.openPad": "次の名称の既存の Pad を開く:",
"pad.toolbar.bold.title": "太字 (Ctrl+B)", "pad.toolbar.bold.title": "太字 (Ctrl+B)",
"pad.toolbar.italic.title": "斜体 (Ctrl+I)", "pad.toolbar.italic.title": "斜体 (Ctrl+I)",
@ -39,7 +40,7 @@
"pad.toolbar.ol.title": "番号付きリスト (Ctrl+Shift+N)", "pad.toolbar.ol.title": "番号付きリスト (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "番号なしリスト (Ctrl+Shift+L)", "pad.toolbar.ul.title": "番号なしリスト (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "インデント (Tab)", "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.undo.title": "元に戻す (Ctrl+Z)",
"pad.toolbar.redo.title": "やり直し (Ctrl+Y)", "pad.toolbar.redo.title": "やり直し (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "作者の色分けを消去(Ctrl+Shift+C)", "pad.toolbar.clearAuthorship.title": "作者の色分けを消去(Ctrl+Shift+C)",
@ -54,7 +55,7 @@
"pad.loading": "読み込み中...", "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.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.permissionDenied": "あなたにはこのパッドへのアクセス許可がありません",
"pad.settings.padSettings": "パッドの設定", "pad.settings.padSettings": "パッドの設定",
"pad.settings.myView": "個人設定", "pad.settings.myView": "個人設定",
"pad.settings.stickychat": "画面にチャットを常に表示", "pad.settings.stickychat": "画面にチャットを常に表示",
"pad.settings.chatandusers": "チャットとユーザーを表示", "pad.settings.chatandusers": "チャットとユーザーを表示",
@ -77,7 +78,7 @@
"pad.importExport.exportpdf": "PDF", "pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.modals.connected": "接続されました。", "pad.modals.connected": "接続されました。",
"pad.modals.reconnecting": "パッドに再接続中...", "pad.modals.reconnecting": "パッドに再接続中",
"pad.modals.forcereconnect": "強制的に再接続", "pad.modals.forcereconnect": "強制的に再接続",
"pad.modals.reconnecttimer": "再接続を試行中", "pad.modals.reconnecttimer": "再接続を試行中",
"pad.modals.cancel": "中止", "pad.modals.cancel": "中止",

View file

@ -145,14 +145,14 @@
"admin_settings.current_save.value": "설정 저장", "admin_settings.current_save.value": "설정 저장",
"admin_settings.invalid_json": "잘못된 JSON", "admin_settings.invalid_json": "잘못된 JSON",
"admin_settings.current_test.value": "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.saved": "설정을 성공적으로 저장했습니다.",
"admin_settings.toast.save_failed": "저장 실패: settings.json에 쓰기 할 수 없습니다.", "admin_settings.toast.save_failed": "저장 실패: settings.json에 쓰기 할 수 없습니다.",
"admin_settings.toast.json_invalid": "구문 오류: 쉼표, 중괄호, 따옴표를 확인하세요.", "admin_settings.toast.json_invalid": "구문 오류: 쉼표, 중괄호, 따옴표를 확인하세요.",
"admin_settings.toast.disconnected": "저장할 수 없음: 서버에 연결되어 있지 않습니다.", "admin_settings.toast.disconnected": "저장할 수 없음: 서버에 연결되어 있지 않습니다.",
"admin_settings.toast.validation_ok": "JSON이 유효합니다.", "admin_settings.toast.validation_ok": "JSON이 유효합니다.",
"admin_settings.toast.validation_failed": "JSON이 유효하지 않습니다. 구문 오류를 수정하세요.", "admin_settings.toast.validation_failed": "JSON이 유효하지 않습니다. 구문 오류를 수정하세요.",
"admin_settings.toast.prettify_failed": "정리할 수 없습니다. 먼저 구문 오류를 수정하세요.", "admin_settings.toast.prettify_failed": "보기 좋게 정리할 수 없습니다. 먼저 구문 오류를 수정하세요.",
"admin_settings.prettify_confirm": "정리하면 모든 의견이 제거됩니다. 계속하시겠습니까?", "admin_settings.prettify_confirm": "정리하면 모든 의견이 제거됩니다. 계속하시겠습니까?",
"admin_settings.mode.form": "형태", "admin_settings.mode.form": "형태",
"admin_settings.mode.raw": "원본", "admin_settings.mode.raw": "원본",
@ -325,7 +325,7 @@
"pad.importExport.exportworda.title": "Microsoft Word 파일로 내보내기", "pad.importExport.exportworda.title": "Microsoft Word 파일로 내보내기",
"pad.importExport.exportpdfa.title": "PDF로 내보내기", "pad.importExport.exportpdfa.title": "PDF로 내보내기",
"pad.importExport.exportopena.title": "ODF(Open Document Format) 형식으로 내보내기", "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.connected": "연결함.",
"pad.modals.reconnecting": "내 패드에 다시 연결하는 중...", "pad.modals.reconnecting": "내 패드에 다시 연결하는 중...",
"pad.modals.forcereconnect": "강제로 다시 연결", "pad.modals.forcereconnect": "강제로 다시 연결",

View file

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

View file

@ -7,17 +7,85 @@
"Mark", "Mark",
"Rudko", "Rudko",
"Teslaton", "Teslaton",
"Wizzard",
"Yardom78" "Yardom78"
] ]
}, },
"admin.page-title": "Ovládací panel správu - Etherpad", "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": "Správca doplnkov",
"admin_plugins.available": "Dostupné doplnky", "admin_plugins.available": "Dostupné doplnky",
"admin_plugins.available_not-found": "Doplnky neboli nájdené.", "admin_plugins.available_not-found": "Doplnky neboli nájdené.",
"admin_plugins.available_fetching": "Načítavanie...", "admin_plugins.available_fetching": "Načítavanie...",
"admin_plugins.available_install.value": "Inštalovať", "admin_plugins.available_install.value": "Inštalovať",
"admin_plugins.available_search.placeholder": "Vyhľadať doplnky na inštaláciu", "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.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": "Nainštalované doplnky",
"admin_plugins.installed_fetching": "Načítavanie nainštalovaných doplnkov...", "admin_plugins.installed_fetching": "Načítavanie nainštalovaných doplnkov...",
"admin_plugins.installed_nothing": "Ešte ste nenainštalovali žiadne doplnky.", "admin_plugins.installed_nothing": "Ešte ste nenainštalovali žiadne doplnky.",
@ -25,27 +93,161 @@
"admin_plugins.last-update": "Posledná aktualizácia", "admin_plugins.last-update": "Posledná aktualizácia",
"admin_plugins.name": "Názov", "admin_plugins.name": "Názov",
"admin_plugins.page-title": "Správca doplnkov - Etherpad", "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.version": "Verzia",
"admin_plugins_info": "Informácie k riešeniu problémov", "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": "Nainštalované súčasti",
"admin_plugins_info.hooks_client": "Súčasti na strane klienta", "admin_plugins_info.hooks_client": "Súčasti na strane klienta",
"admin_plugins_info.hooks_server": "Súčasti na strane servera", "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.parts": "Nainštalované súčasti",
"admin_plugins_info.plugins": "Nainštalované doplnky", "admin_plugins_info.plugins": "Nainštalované doplnky",
"admin_plugins_info.page-title": "Informácie o doplnkoch - Etherpad", "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": "Verzia Etherpadu",
"admin_plugins_info.version_latest": "Posledná dostupná verzia", "admin_plugins_info.version_latest": "Posledná dostupná verzia",
"admin_plugins_info.version_number": "Číslo verzie", "admin_plugins_info.version_number": "Číslo verzie",
"admin_settings": "Nastavenia", "admin_settings": "Nastavenia",
"admin_settings.create_pad": "Vytvoriť pad",
"admin_settings.current": "Aktuálne nastavenia", "admin_settings.current": "Aktuálne nastavenia",
"admin_settings.current_example-devel": "Príklad šablóny vývojárskeho 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_example-prod": "Príklad šablóny výrobného nastavenia",
"admin_settings.current_restart.value": "Reštartovať Etherpad", "admin_settings.current_restart.value": "Reštartovať Etherpad",
"admin_settings.current_save.value": "Uložiť nastavenia", "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.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.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.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.bold.title": "Tučné (Ctrl+B)",
"pad.toolbar.italic.title": "Kurzíva (Ctrl+I)", "pad.toolbar.italic.title": "Kurzíva (Ctrl+I)",
"pad.toolbar.underline.title": "Podčiarknuté (Ctrl+U)", "pad.toolbar.underline.title": "Podčiarknuté (Ctrl+U)",
@ -62,22 +264,42 @@
"pad.toolbar.savedRevision.title": "Uložiť revíziu", "pad.toolbar.savedRevision.title": "Uložiť revíziu",
"pad.toolbar.settings.title": "Nastavenia", "pad.toolbar.settings.title": "Nastavenia",
"pad.toolbar.embed.title": "Zdieľať alebo vložiť tento poznámkový blok", "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.toolbar.showusers.title": "Zobraziť používateľov tohoto poznámkového bloku",
"pad.colorpicker.save": "Uložiť", "pad.colorpicker.save": "Uložiť",
"pad.colorpicker.cancel": "Zrušiť", "pad.colorpicker.cancel": "Zrušiť",
"pad.loading": "Načítava sa...", "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.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.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.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.stickychat": "Rozhovor stále na obrazovke",
"pad.settings.chatandusers": "Zobraziť rozhovor a používateľov", "pad.settings.chatandusers": "Zobraziť rozhovor a používateľov",
"pad.settings.colorcheck": "Farby autorov", "pad.settings.colorcheck": "Farby autorov",
"pad.settings.fadeInactiveAuthorColors": "Stlmiť farby neaktívnych autorov",
"pad.settings.linenocheck": "Čísla riadkov", "pad.settings.linenocheck": "Čísla riadkov",
"pad.settings.rtlcheck": "Čítať obsah sprava doľava?", "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": "Typ písma:",
"pad.settings.fontType.normal": "Normálne", "pad.settings.fontType.normal": "Normálne",
"pad.settings.language": "Jazyk:", "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.about": "O Etherpade",
"pad.settings.poweredBy": "Poháňané cez", "pad.settings.poweredBy": "Poháňané cez",
"pad.importExport.import_export": "Import/Export", "pad.importExport.import_export": "Import/Export",
@ -90,6 +312,13 @@
"pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF", "pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)", "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.connected": "Pripojené.",
"pad.modals.reconnecting": "Opätovné pripájanie k vášmu poznámkovému bloku...", "pad.modals.reconnecting": "Opätovné pripájanie k vášmu poznámkovému bloku...",
"pad.modals.forcereconnect": "Vynútiť znovupripojenie", "pad.modals.forcereconnect": "Vynútiť znovupripojenie",
@ -120,6 +349,8 @@
"pad.modals.disconnected": "Boli ste odpojení.", "pad.modals.disconnected": "Boli ste odpojení.",
"pad.modals.disconnected.explanation": "Spojenie so serverom sa prerušilo", "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.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": "Zdieľať tento poznámkový blok",
"pad.share.readonly": "Len na čítanie", "pad.share.readonly": "Len na čítanie",
"pad.share.link": "Odkaz", "pad.share.link": "Odkaz",
@ -132,12 +363,36 @@
"timeslider.followContents": "Sledovať aktualizácie obsahu poznámkového bloku", "timeslider.followContents": "Sledovať aktualizácie obsahu poznámkového bloku",
"timeslider.pageTitle": "Časová os {{appTitle}}", "timeslider.pageTitle": "Časová os {{appTitle}}",
"timeslider.toolbar.returnbutton": "Späť do poznámkového bloku", "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.authors": "Autori:",
"timeslider.toolbar.authorsList": "Bez autorov", "timeslider.toolbar.authorsList": "Bez autorov",
"timeslider.toolbar.exportlink.title": "Export", "timeslider.toolbar.exportlink.title": "Export",
"timeslider.exportCurrent": "Exportovať aktuálnu verziu ako:", "timeslider.exportCurrent": "Exportovať aktuálnu verziu ako:",
"timeslider.version": "Verzia {{version}}", "timeslider.version": "Verzia {{version}}",
"timeslider.saved": "Uložené {{day}}. {{month}} {{year}}", "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.playPause": "Pustiť / Pozastaviť obsah poznámkového bloku",
"timeslider.backRevision": "Ísť v tomto poznámkovom bloku o jednu revíziu späť", "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", "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.savedrevs.timeslider": "Návštevou časovej osi môžete zobraziť uložené revízie",
"pad.userlist.entername": "Zadajte svoje meno", "pad.userlist.entername": "Zadajte svoje meno",
"pad.userlist.unnamed": "nemenovaný", "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.editbar.clearcolors": "Odstrániť farby autorov z celého dokumentu? Táto akcia sa nedá vrátiť",
"pad.impexp.importbutton": "Importovať teraz", "pad.impexp.importbutton": "Importovať teraz",
"pad.impexp.importing": "Prebieha import...", "pad.impexp.importing": "Prebieha import...",
@ -169,5 +425,6 @@
"pad.impexp.importfailed": "Import zlyhal", "pad.impexp.importfailed": "Import zlyhal",
"pad.impexp.copypaste": "Vložte prosím kópiu cez schránku", "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.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

@ -29,9 +29,19 @@
"admin.page-title": "管理员面板 - Etherpad", "admin.page-title": "管理员面板 - Etherpad",
"admin.loading": "正在加载…", "admin.loading": "正在加载…",
"admin.loading_description": "页面加载中,请稍安勿躁。", "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.username": "用户名",
"admin_login.password": "密码", "admin_login.password": "密码",
"admin_login.submit": "登录", "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.bulk.delete": "删除",
"admin_pads.cancel": "取消", "admin_pads.cancel": "取消",
"admin_pads.col.users": "用户", "admin_pads.col.users": "用户",

View file

@ -19,6 +19,7 @@
* limitations under the License. * limitations under the License.
*/ */
import log4js from 'log4js'; import log4js from 'log4js';
import fs from 'fs';
import path from 'path'; import path from 'path';
import _ from 'underscore'; import _ from 'underscore';
@ -30,6 +31,25 @@ const absPathLogger = log4js.getLogger('AbsolutePaths');
*/ */
let etherpadRoot: string|null = null; 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, * If stringArray's last elements are exactly equal to lastDesiredElements,
* returns a copy in which those last elements are popped, or false otherwise. * returns a copy in which those last elements are popped, or false otherwise.
@ -79,7 +99,6 @@ export const findEtherpadRoot = () => {
return etherpadRoot; return etherpadRoot;
} }
const findRoot = require('find-root');
const foundRoot = findRoot(__dirname); const foundRoot = findRoot(__dirname);
const splitFoundRoot = foundRoot.split(path.sep); const splitFoundRoot = foundRoot.split(path.sep);

View file

@ -39,11 +39,9 @@ const ROOT_DIR = path.join(settings.root, 'src/static/');
const LIBRARY_WHITELIST = [ const LIBRARY_WHITELIST = [
'async', 'async',
'js-cookie', 'js-cookie',
'security',
'split-grid', 'split-grid',
'tinycon', 'tinycon',
'underscore', 'underscore',
'unorm',
]; ];
// What follows is a terrible hack to avoid loop-back within the server. // 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 os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import {argv} from './Cli' import {argv} from './Cli'
import jsonminify from 'jsonminify'; import {parse as parseJsonc, printParseErrorCode, ParseError} from 'jsonc-parser';
import log4js from 'log4js'; import log4js from 'log4js';
import {createHash} from 'node:crypto'; import {createHash} from 'node:crypto';
import randomString from './randomstring'; import randomString from './randomstring';
@ -116,9 +116,18 @@ const parseSettings = (settingsFilename: string, isSettings: boolean) => {
} }
try { 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}`); logger.info(`${settingsType} loaded from: ${settingsFilename}`);

View file

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

View file

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

View file

@ -31,12 +31,13 @@ import Op from "./Op";
const _MAX_LIST_LEVEL = 16; const _MAX_LIST_LEVEL = 16;
import AttributeMap from './AttributeMap'; import AttributeMap from './AttributeMap';
import UNorm from 'unorm';
import {subattribution} from './Changeset'; import {subattribution} from './Changeset';
import {SmartOpAssembler} from "./SmartOpAssembler"; import {SmartOpAssembler} from "./SmartOpAssembler";
const hooks = require('./pluginfw/hooks'); 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(); const tagName = (n) => n.tagName && n.tagName.toLowerCase();
// supportedElems are Supported natively within Etherpad and don't require a plugin // supportedElems are Supported natively within Etherpad and don't require a plugin
const supportedElems = new Set([ const supportedElems = new Set([

View file

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

View file

@ -1,20 +1,73 @@
// @ts-nocheck
'use strict'; 'use strict';
/** /**
* Copyright 2009 Google Inc. * OWASP-style output-escaping helpers.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Vendored from the `security` npm package (v1.0.0), which has been
* you may not use this file except in compliance with the License. * unmaintained since 2012. The implementation below is reproduced verbatim
* You may obtain a copy of the License at * (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 * Permission is hereby granted, free of charge, to any person obtaining a
* distributed under the License is distributed on an "AS-IS" BASIS, * copy of this software and associated documentation files (the "Software"),
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * to deal in the Software without restriction, including without limitation
* See the License for the specific language governing permissions and * the rights to use, copy, modify, merge, publish, distribute, sublicense,
* limitations under the License. * 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

@ -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

@ -278,4 +278,62 @@ describe(__filename, function () {
assert.strictEqual(over!.privacy.pluginCatalog, 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

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

View file

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