mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-19 09:34:07 +00:00
Compare commits
49 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a6109bd1d | ||
|
|
037f4a25b5 | ||
|
|
4755bd3e17 | ||
|
|
05d0f62403 | ||
|
|
3efcbfd358 | ||
|
|
2b5d521923 | ||
|
|
631caf911c | ||
|
|
0aba424357 | ||
|
|
194f144940 | ||
|
|
c045fcf698 | ||
|
|
451df9f01c | ||
|
|
ce27525d97 | ||
|
|
9c48900092 | ||
|
|
173dbb3448 | ||
|
|
24e0c44d91 | ||
|
|
f65701eaaf | ||
|
|
2f766d075d | ||
|
|
2c94ef9493 | ||
|
|
fdc23c658f | ||
|
|
e5d6142061 | ||
|
|
b7d2477840 | ||
|
|
4bc80d1439 | ||
|
|
8a9c0a6e20 | ||
|
|
df1666cffa | ||
|
|
afdd9b9500 | ||
|
|
3b9acd487c | ||
|
|
82d1fd63a5 | ||
|
|
75be62c801 | ||
|
|
ab4c7beded | ||
|
|
be8120b14e | ||
|
|
33160db454 | ||
|
|
9e850e17f3 | ||
|
|
3ac0b0f0a4 | ||
|
|
cce7cc6cd4 | ||
|
|
bce18df898 | ||
|
|
1442aee756 | ||
|
|
00e1892704 | ||
|
|
2ecd748fca | ||
|
|
0cd01fce41 | ||
|
|
40327d59d6 | ||
|
|
b45f60e20a | ||
|
|
f5b61c3388 | ||
|
|
ff0bd2e819 | ||
|
|
8e04130969 | ||
|
|
c8b544802a | ||
|
|
01500ca70c | ||
|
|
7168f14f0a | ||
|
|
01d0b08a4e | ||
|
|
41dc87edf9 |
48 changed files with 3544 additions and 1857 deletions
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
|
|
@ -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:
|
||||||
|
- "*"
|
||||||
|
|
|
||||||
12
.github/workflows/backend-tests.yml
vendored
12
.github/workflows/backend-tests.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
4
.github/workflows/build-and-deploy-docs.yml
vendored
4
.github/workflows/build-and-deploy-docs.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
2
.github/workflows/downstream-smoke.yml
vendored
2
.github/workflows/downstream-smoke.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
4
.github/workflows/frontend-admin-tests.yml
vendored
4
.github/workflows/frontend-admin-tests.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
16
.github/workflows/frontend-tests.yml
vendored
16
.github/workflows/frontend-tests.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.github/workflows/handleRelease.yml
vendored
2
.github/workflows/handleRelease.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
6
.github/workflows/load-test.yml
vendored
6
.github/workflows/load-test.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
2
.github/workflows/perform-type-check.yml
vendored
2
.github/workflows/perform-type-check.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
2
.github/workflows/rate-limit.yml
vendored
2
.github/workflows/rate-limit.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.github/workflows/releaseEtherpad.yml
vendored
2
.github/workflows/releaseEtherpad.yml
vendored
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
91
doc/accessibility.md
Normal 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.
|
||||||
|
|
@ -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
204
doc/database.md
Normal 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
359
doc/deployment.md
Normal 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
240
doc/development.md
Normal 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
204
doc/faq.md
Normal 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.
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
3336
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": "فرض إعادة الاتصال",
|
||||||
|
|
|
||||||
|
|
@ -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 (A–Z)",
|
||||||
|
"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 (A–Z)",
|
||||||
|
"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í",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": "中止",
|
||||||
|
|
|
||||||
|
|
@ -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": "강제로 다시 연결",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "用户",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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([
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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} = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'/': '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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)}"`;
|
||||||
|
|
|
||||||
78
src/tests/backend/specs/security.ts
Normal file
78
src/tests/backend/specs/security.ts
Normal 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">/&\''),
|
||||||
|
'<a href="x">/&'');
|
||||||
|
});
|
||||||
|
it('neutralises a script tag', function () {
|
||||||
|
assert.equal(Security.escapeHTML('<script>alert(1)</script>'),
|
||||||
|
'<script>alert(1)</script>');
|
||||||
|
});
|
||||||
|
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 b');
|
||||||
|
// hex is lowercased, matching the original `security` package output.
|
||||||
|
assert.equal(Security.escapeHTMLAttribute('javascript:alert(1)'),
|
||||||
|
'javascript:alert(1)');
|
||||||
|
});
|
||||||
|
it('prefers named entities for &, <, >, ", \', /', function () {
|
||||||
|
assert.equal(Security.escapeHTMLAttribute('<>&"\'/'),
|
||||||
|
'<>&"'/');
|
||||||
|
});
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue