mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-05-03 05:39:12 -04:00
Compare commits
241 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f62bf241e6 | ||
![]() |
0a4e7513aa | ||
![]() |
6208a04556 | ||
![]() |
af2d7cc552 | ||
![]() |
050869aecd | ||
![]() |
697c98a89f | ||
![]() |
db16196e46 | ||
![]() |
e86f3ce643 | ||
![]() |
509d4a3397 | ||
![]() |
5e8e42d022 | ||
![]() |
7fde6ee9bd | ||
![]() |
d62d5a0460 | ||
![]() |
77792a5f7f | ||
![]() |
aa25a2f840 | ||
![]() |
65949b60f2 | ||
![]() |
c130715b32 | ||
![]() |
f8239863d8 | ||
![]() |
6a750f191e | ||
![]() |
227cb59751 | ||
![]() |
2b1c275665 | ||
![]() |
ff73af8aad | ||
![]() |
ae15d4ea30 | ||
![]() |
524dd6bc6d | ||
![]() |
9b31d17d04 | ||
![]() |
b6bdbe1a2a | ||
![]() |
49d2860b1e | ||
![]() |
676edddc38 | ||
![]() |
ab5b933fb3 | ||
![]() |
35ddea0c57 | ||
![]() |
b6b6eb528e | ||
![]() |
9276a001d8 | ||
![]() |
b3a0c3d426 | ||
![]() |
d61578d680 | ||
![]() |
9f49ba60fa | ||
![]() |
df60a78348 | ||
![]() |
b6e47d7f0a | ||
![]() |
43de91d994 | ||
![]() |
6321e1cc34 | ||
![]() |
02e53f328d | ||
![]() |
3e95ae2ccd | ||
![]() |
397fc615b9 | ||
![]() |
8eceb730d5 | ||
![]() |
770f8b4aa1 | ||
![]() |
fbc4b5be59 | ||
![]() |
ad15d7652d | ||
![]() |
87d8b87855 | ||
![]() |
8b41eed8a6 | ||
![]() |
705d4ee24a | ||
![]() |
d1df1c14d9 | ||
![]() |
2bbed2e858 | ||
![]() |
f567c2a2b7 | ||
![]() |
155df5257e | ||
![]() |
121c27d361 | ||
![]() |
bb6e47ce9a | ||
![]() |
acf01c779e | ||
![]() |
feaae4957f | ||
![]() |
b03e8167a6 | ||
![]() |
b1f4996f58 | ||
![]() |
42371f3638 | ||
![]() |
84d67575ce | ||
![]() |
a25b57fe50 | ||
![]() |
a985fe181f | ||
![]() |
ccc7c60416 | ||
![]() |
9bc91cb1d0 | ||
![]() |
0995ce1567 | ||
![]() |
85da7cc0f0 | ||
![]() |
954a6b7b40 | ||
![]() |
1ad0d43754 | ||
![]() |
6423bde241 | ||
![]() |
2731e4d8d3 | ||
![]() |
7b86c3f91f | ||
![]() |
f4a8fc9544 | ||
![]() |
b158c6175f | ||
![]() |
13bc06febe | ||
![]() |
0da15a4a60 | ||
![]() |
ff24e1e80e | ||
![]() |
52f3827411 | ||
![]() |
40c3329efa | ||
![]() |
840fc982c4 | ||
![]() |
7545771dbc | ||
![]() |
c137aed7e2 | ||
![]() |
766deb204e | ||
![]() |
cc76866554 | ||
![]() |
1c5fcfc0e7 | ||
![]() |
c106b769cf | ||
![]() |
4dbf1b925d | ||
![]() |
e3300a6e83 | ||
![]() |
708e3e1d66 | ||
![]() |
430b5a26b9 | ||
![]() |
68b6aacc1f | ||
![]() |
3ad34f5180 | ||
![]() |
14ea8462f5 | ||
![]() |
12e8c85095 | ||
![]() |
6c4717f441 | ||
![]() |
6b28a1d532 | ||
![]() |
0248aca5b6 | ||
![]() |
6942d3876f | ||
![]() |
06573608a2 | ||
![]() |
e30bdd844d | ||
![]() |
d79537472b | ||
![]() |
8c1a519465 | ||
![]() |
95f189c5c1 | ||
![]() |
1d211170a3 | ||
![]() |
b04962321c | ||
![]() |
98699464ce | ||
![]() |
b157ec9734 | ||
![]() |
7c4959aff5 | ||
![]() |
41208c0c57 | ||
![]() |
056e777539 | ||
![]() |
ac470eec9a | ||
![]() |
ec8ecc980f | ||
![]() |
e3bb2bde83 | ||
![]() |
2d09749d6c | ||
![]() |
fdb5026653 | ||
![]() |
846f8fd6f1 | ||
![]() |
0c68ddce1e | ||
![]() |
5b8be1cac4 | ||
![]() |
0debef78e8 | ||
![]() |
1e3a61e5fb | ||
![]() |
ac4489875f | ||
![]() |
2e04ab9030 | ||
![]() |
5bda2aa568 | ||
![]() |
c5bacf5ca4 | ||
![]() |
de2586ddcf | ||
![]() |
b7d03010fc | ||
![]() |
fd234e8136 | ||
![]() |
f7fe0b543d | ||
![]() |
89d6c8b727 | ||
![]() |
8aa31713f7 | ||
![]() |
09b235d71d | ||
![]() |
0b26405201 | ||
![]() |
f61a3b6c5a | ||
![]() |
37a8e0b0cb | ||
![]() |
ea31e892ae | ||
![]() |
e029dbf3d3 | ||
![]() |
d452903f86 | ||
![]() |
756a5e9574 | ||
![]() |
28d7d744bb | ||
![]() |
2aba044dfe | ||
![]() |
f7dc3df26b | ||
![]() |
2520c8db3f | ||
![]() |
218cfaa96d | ||
![]() |
e7642dcbc2 | ||
![]() |
bb3fa1f9e4 | ||
![]() |
518256b63e | ||
![]() |
738b5aba40 | ||
![]() |
b06882dc3a | ||
![]() |
477ca33dc9 | ||
![]() |
c8561fe0a4 | ||
![]() |
d6322bb591 | ||
![]() |
75c9d2a03d | ||
![]() |
c6d5d420c2 | ||
![]() |
1c459b3e41 | ||
![]() |
4034ee1e6e | ||
![]() |
60a4f63c52 | ||
![]() |
12f81cfb5e | ||
![]() |
76e52e51d9 | ||
![]() |
51b0c321fd | ||
![]() |
cf701d58ec | ||
![]() |
94b9b821a9 | ||
![]() |
5846ff9c8e | ||
![]() |
45bafe0b94 | ||
![]() |
1ad9418a6f | ||
![]() |
08f199178d | ||
![]() |
178fe7c24d | ||
![]() |
3de6c7caef | ||
![]() |
d30f65ebf2 | ||
![]() |
b1b203ff4f | ||
![]() |
be1a185310 | ||
![]() |
4166544801 | ||
![]() |
1d33f31063 | ||
![]() |
58bb2adb08 | ||
![]() |
645e289d2d | ||
![]() |
113884d071 | ||
![]() |
9c21522bc0 | ||
![]() |
54560b2573 | ||
![]() |
4891243c27 | ||
![]() |
50631475f9 | ||
![]() |
20a635136c | ||
![]() |
a61f634586 | ||
![]() |
97dbe03e2e | ||
![]() |
c7e2b57664 | ||
![]() |
15a9dfc992 | ||
![]() |
96124654f4 | ||
![]() |
f066c6b4a8 | ||
![]() |
4c63449a69 | ||
![]() |
c5752ea934 | ||
![]() |
9096b406d6 | ||
![]() |
993409c8da | ||
![]() |
0aea774af7 | ||
![]() |
dd83164b22 | ||
![]() |
79da35c6a6 | ||
![]() |
84cfbe51e5 | ||
![]() |
504d04d668 | ||
![]() |
d22b1d2a4c | ||
![]() |
eeaa14f309 | ||
![]() |
d4cbbf18e0 | ||
![]() |
d010d53195 | ||
![]() |
c9dd454af9 | ||
![]() |
8463ce5ec0 | ||
![]() |
8285dce8c1 | ||
![]() |
7122b3a04b | ||
![]() |
85fde97645 | ||
![]() |
17a515142b | ||
![]() |
94abebf770 | ||
![]() |
b0b15f988b | ||
![]() |
be1dc84f65 | ||
![]() |
8c91ab280d | ||
![]() |
2ccffa98f6 | ||
![]() |
f8225b3e2a | ||
![]() |
1393f338e0 | ||
![]() |
38f3aca153 | ||
![]() |
42e8e810c2 | ||
![]() |
4870131480 | ||
![]() |
dd5f9cc09f | ||
![]() |
f170e93e75 | ||
![]() |
21380d23cc | ||
![]() |
1e91efc16c | ||
![]() |
852f282b03 | ||
![]() |
4ff00e278a | ||
![]() |
6ed711a4d8 | ||
![]() |
7f05e2b399 | ||
![]() |
28e04bdf71 | ||
![]() |
3dae23a1e5 | ||
![]() |
7e3ad03e2f | ||
![]() |
5ee2c4e7f8 | ||
![]() |
870dbf6ce0 | ||
![]() |
c7a2dea4d1 | ||
![]() |
babfaab4df | ||
![]() |
74a12e04c1 | ||
![]() |
04ba1f53f0 | ||
![]() |
c7b295f8dd | ||
![]() |
221f6f2002 | ||
![]() |
3fdb3f7838 | ||
![]() |
f78cb4a3a8 | ||
![]() |
44024e8a5f | ||
![]() |
dd1231cc55 | ||
![]() |
642aa250ba | ||
![]() |
dafb158de9 | ||
![]() |
07b3597fd3 | ||
![]() |
a69ba6bcf2 |
218 changed files with 11253 additions and 27352 deletions
26
.github/workflows/backend-tests.yml
vendored
26
.github/workflows/backend-tests.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [18, 20, 22]
|
||||
node: [20, 22, 23]
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
|
@ -53,7 +53,7 @@ jobs:
|
|||
run: pnpm config set auto-install-peers false
|
||||
-
|
||||
name: Install libreoffice
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.2
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.5.0
|
||||
with:
|
||||
packages: libreoffice libreoffice-pdfimport
|
||||
version: 1.0
|
||||
|
@ -69,6 +69,9 @@ jobs:
|
|||
-
|
||||
name: Run the backend tests
|
||||
run: pnpm test
|
||||
- name: Run the new vitest tests
|
||||
working-directory: src
|
||||
run: pnpm run test:vitest
|
||||
|
||||
withpluginsLinux:
|
||||
# run on pushes to any branch
|
||||
|
@ -81,7 +84,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [18, 20, 22]
|
||||
node: [20, 22, 23]
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
|
@ -110,7 +113,7 @@ jobs:
|
|||
run: pnpm config set auto-install-peers false
|
||||
-
|
||||
name: Install libreoffice
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.2
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.5.0
|
||||
with:
|
||||
packages: libreoffice libreoffice-pdfimport
|
||||
version: 1.0
|
||||
|
@ -142,6 +145,9 @@ jobs:
|
|||
-
|
||||
name: Run the backend tests
|
||||
run: pnpm test
|
||||
- name: Run the new vitest tests
|
||||
working-directory: src
|
||||
run: pnpm run test:vitest
|
||||
|
||||
withoutpluginsWindows:
|
||||
# run on pushes to any branch
|
||||
|
@ -193,7 +199,11 @@ jobs:
|
|||
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
|
||||
-
|
||||
name: Run the backend tests
|
||||
run: cd src && pnpm test
|
||||
working-directory: src
|
||||
run: pnpm test
|
||||
- name: Run the new vitest tests
|
||||
working-directory: src
|
||||
run: pnpm run test:vitest
|
||||
|
||||
withpluginsWindows:
|
||||
# run on pushes to any branch
|
||||
|
@ -273,4 +283,8 @@ jobs:
|
|||
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
|
||||
-
|
||||
name: Run the backend tests
|
||||
run: cd src && pnpm test
|
||||
working-directory: src
|
||||
run: pnpm test
|
||||
- name: Run the new vitest tests
|
||||
working-directory: src
|
||||
run: pnpm run test:vitest
|
||||
|
|
31
.github/workflows/docker.yml
vendored
31
.github/workflows/docker.yml
vendored
|
@ -22,6 +22,9 @@ jobs:
|
|||
-
|
||||
name: Check out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: etherpad
|
||||
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: github.event_name == 'push'
|
||||
|
@ -33,7 +36,7 @@ jobs:
|
|||
name: Build and export to Docker
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
context: ./etherpad
|
||||
target: production
|
||||
load: true
|
||||
tags: ${{ env.TEST_TAG }}
|
||||
|
@ -62,6 +65,7 @@ jobs:
|
|||
${{ runner.os }}-pnpm-store-
|
||||
-
|
||||
name: Test
|
||||
working-directory: etherpad
|
||||
run: |
|
||||
docker run --rm -d -p 9001:9001 --name test ${{ env.TEST_TAG }}
|
||||
./bin/installDeps.sh
|
||||
|
@ -98,10 +102,11 @@ jobs:
|
|||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
id: build-docker
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
context: ./etherpad
|
||||
target: production
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
|
@ -111,7 +116,29 @@ jobs:
|
|||
uses: peter-evans/dockerhub-description@v4
|
||||
if: github.ref == 'refs/heads/master'
|
||||
with:
|
||||
readme-filepath: ./etherpad/README.md
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: etherpad/etherpad
|
||||
enable-url-completion: true
|
||||
- name: Check out
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: ether-charts
|
||||
repository: ether/ether-charts
|
||||
token: ${{ secrets.ETHER_CHART_TOKEN }}
|
||||
- name: Update tag in values-dev.yaml
|
||||
if: success() && github.ref == 'refs/heads/develop'
|
||||
working-directory: ether-charts
|
||||
run: |
|
||||
sed -i 's/tag: ".*"/tag: "${{ steps.build-docker.outputs.digest }}"/' values-dev.yaml
|
||||
- name: Commit and push changes
|
||||
working-directory: ether-charts
|
||||
if: success() && github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git add values-dev.yaml
|
||||
git commit -m 'Update develop image tag'
|
||||
git push
|
||||
|
|
4
.github/workflows/frontend-admin-tests.yml
vendored
4
.github/workflows/frontend-admin-tests.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [20, 22]
|
||||
node: [20, 22, 23]
|
||||
|
||||
steps:
|
||||
-
|
||||
|
@ -50,7 +50,7 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
|
6
.github/workflows/frontend-tests.yml
vendored
6
.github/workflows/frontend-tests.yml
vendored
|
@ -57,7 +57,7 @@ jobs:
|
|||
name: Create settings.json
|
||||
run: cp ./src/tests/settings.json settings.json
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
@ -127,7 +127,7 @@ jobs:
|
|||
- name: Create settings.json
|
||||
run: cp ./src/tests/settings.json settings.json
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
@ -175,7 +175,7 @@ jobs:
|
|||
with:
|
||||
node-version: 22
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [18, 20, 22]
|
||||
node: [20, 22, 23]
|
||||
steps:
|
||||
-
|
||||
name: Check out latest release
|
||||
|
@ -43,7 +43,7 @@ jobs:
|
|||
- name: Only install direct dependencies
|
||||
run: pnpm config set auto-install-peers false
|
||||
- name: Install libreoffice
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.2
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.5.0
|
||||
with:
|
||||
packages: libreoffice libreoffice-pdfimport
|
||||
version: 1.0
|
||||
|
@ -62,7 +62,7 @@ jobs:
|
|||
run: pnpm config set auto-install-peers false
|
||||
-
|
||||
name: Install libreoffice
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.2
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.5.0
|
||||
with:
|
||||
packages: libreoffice libreoffice-pdfimport
|
||||
version: 1.0
|
||||
|
|
62
CHANGELOG.md
62
CHANGELOG.md
|
@ -1,3 +1,65 @@
|
|||
# 2.3.0
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
- Added possibility to cluster Etherpads behind reverse proxy. There is now a new reverse proxy designed for Etherpads that handles multiple Etherpads and the created pads in them. It will assign the pad assignement to an Etherpad at random but once the choice was made it will always reverse proxy the same backend. This allows to host multiple concurrent Etherpads and benefit from multi core systems even though one Etherpad is singlethreaded.
|
||||
- Added reverse proxy configuration for replacing Nginx. In the past there were some issues with nginx and its configuration. This reverse proxy allows you to handle your configuration with ease.
|
||||
|
||||
If you want to find out more about the reverse proxy method check out the repository https://github.com/ether/etherpad-proxy . It also contains a sample docker-compose file with three Etherpads and one etherpad-proxy. Of course you need to adapt the settings.json.template to your liking and map it into the reverse proxy image before you are ready :).
|
||||
|
||||
|
||||
- Added client authorization to work with Etherpad. Before it would get blocked because it doesn't have the required claim. As this is now fixed etherpad-proxy can also work with your new OAuth2 configuration and retrieve a token via client credentials flow.
|
||||
|
||||
|
||||
|
||||
|
||||
# 2.2.7
|
||||
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
- We migrated all important pages to React 19 and React Router v7
|
||||
|
||||
Besides that only dependency updates.
|
||||
|
||||
|
||||
-> Have a merry Christmas and a happy new year. 🎄 🎁
|
||||
|
||||
|
||||
# 2.2.6
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
- Added option to delete a pad by the creator. This option can be found in the settings menu. When you click on it you get a confirm dialog and after that you have the chance to completely erase the pad.
|
||||
|
||||
|
||||
# 2.2.5
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
- Fixed timeslider not scrolling when the revision count is a multiple of 100
|
||||
- Added new Restful API for version 2 of Etherpad. It is available at /api-docs
|
||||
|
||||
|
||||
# 2.2.4
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
- Switched to new SQLite backend
|
||||
- Fixed rusty-store-kv module not found
|
||||
|
||||
|
||||
# 2.2.3
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
- Introduced a new in process database `rustydb` that represents a fast key value store written in Rust.
|
||||
- Readded window._ as a shortcut for getting text
|
||||
- Added support for migrating any ueberdb database to another. You can now switch as you please. See here: https://docs.etherpad.org/cli.html
|
||||
- Further Typescript movements
|
||||
- A lot of security issues fixed and reviewed in this release. Please update.
|
||||
|
||||
|
||||
# 2.2.2
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
|
63
Dockerfile
63
Dockerfile
|
@ -3,9 +3,10 @@
|
|||
# https://github.com/ether/etherpad-lite
|
||||
#
|
||||
# Author: muxator
|
||||
ARG BUILD_ENV=git
|
||||
|
||||
FROM node:alpine AS adminbuild
|
||||
RUN npm install -g pnpm@9.0.4
|
||||
RUN npm install -g pnpm@latest
|
||||
WORKDIR /opt/etherpad-lite
|
||||
COPY . .
|
||||
RUN pnpm install
|
||||
|
@ -49,6 +50,14 @@ ARG ETHERPAD_PLUGINS=
|
|||
# ETHERPAD_LOCAL_PLUGINS="../ep_my_plugin ../ep_another_plugin"
|
||||
ARG ETHERPAD_LOCAL_PLUGINS=
|
||||
|
||||
# github plugins to install while building the container. By default no plugins are
|
||||
# installed.
|
||||
# If given a value, it has to be a space-separated, quoted list of plugin names.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# ETHERPAD_GITHUB_PLUGINS="ether/ep_plugin"
|
||||
ARG ETHERPAD_GITHUB_PLUGINS=
|
||||
|
||||
# Control whether abiword will be installed, enabling exports to DOC/PDF/ODT formats.
|
||||
# By default, it is not installed.
|
||||
# If given any value, abiword will be installed.
|
||||
|
@ -91,7 +100,7 @@ RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}"
|
|||
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
|
||||
RUN \
|
||||
mkdir -p /usr/share/man/man1 && \
|
||||
npm install pnpm@9.0.4 -g && \
|
||||
npm install pnpm@latest -g && \
|
||||
apk update && apk upgrade && \
|
||||
apk add --no-cache \
|
||||
ca-certificates \
|
||||
|
@ -105,26 +114,49 @@ USER etherpad
|
|||
WORKDIR "${EP_DIR}"
|
||||
|
||||
# etherpads version feature requires this. Only copy what is really needed
|
||||
COPY --chown=etherpad:etherpad ./.git/HEA[D] ./.git/HEAD
|
||||
COPY --chown=etherpad:etherpad ./.git/ref[s] ./.git/refs
|
||||
COPY --chown=etherpad:etherpad ${SETTINGS} ./settings.json
|
||||
COPY --chown=etherpad:etherpad ./var ./var
|
||||
COPY --chown=etherpad:etherpad ./bin ./bin
|
||||
COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./
|
||||
|
||||
FROM build AS development
|
||||
|
||||
COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/
|
||||
|
||||
FROM build AS build_git
|
||||
ONBUILD COPY --chown=etherpad:etherpad ./.git/HEA[D] ./.git/HEAD
|
||||
ONBUILD COPY --chown=etherpad:etherpad ./.git/ref[s] ./.git/refs
|
||||
|
||||
FROM build AS build_copy
|
||||
|
||||
|
||||
|
||||
|
||||
FROM build_${BUILD_ENV} AS development
|
||||
|
||||
ARG ETHERPAD_PLUGINS=
|
||||
ARG ETHERPAD_LOCAL_PLUGINS=
|
||||
ARG ETHERPAD_LOCAL_PLUGINS_ENV=
|
||||
ARG ETHERPAD_GITHUB_PLUGINS=
|
||||
|
||||
COPY --chown=etherpad:etherpad ./src/ ./src/
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/ templates/admin./src/templates/admin
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||
|
||||
COPY --chown=etherpad:etherpad ./local_plugin[s] ./local_plugins/
|
||||
|
||||
RUN bash -c ./bin/installLocalPlugins.sh
|
||||
|
||||
RUN bin/installDeps.sh && \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
||||
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}}; \
|
||||
fi
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \
|
||||
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \
|
||||
fi
|
||||
|
||||
|
||||
FROM build AS production
|
||||
FROM build_${BUILD_ENV} AS production
|
||||
|
||||
ARG ETHERPAD_PLUGINS=
|
||||
ARG ETHERPAD_LOCAL_PLUGINS=
|
||||
ARG ETHERPAD_LOCAL_PLUGINS_ENV=
|
||||
ARG ETHERPAD_GITHUB_PLUGINS=
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV ETHERPAD_PRODUCTION=true
|
||||
|
@ -133,11 +165,14 @@ COPY --chown=etherpad:etherpad ./src ./src
|
|||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/templates/admin ./src/templates/admin
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||
|
||||
RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
||||
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}}; \
|
||||
fi
|
||||
COPY --chown=etherpad:etherpad ./local_plugin[s] ./local_plugins/
|
||||
|
||||
RUN bash -c ./bin/installLocalPlugins.sh
|
||||
|
||||
RUN bin/installDeps.sh && \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \
|
||||
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \
|
||||
fi
|
||||
|
||||
# Copy the configuration file.
|
||||
COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json
|
||||
|
|
27
README.md
27
README.md
|
@ -90,7 +90,7 @@ services:
|
|||
# ports:
|
||||
# - "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data/pgdata
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
@ -174,6 +174,31 @@ following plugins:
|
|||
that each user's chosen color, display name, comment ownership, etc. is
|
||||
strongly linked to their account.
|
||||
|
||||
### Upgrade Etherpad
|
||||
|
||||
Run the following command in your Etherpad folder to upgrade
|
||||
|
||||
1. Stop any running Etherpad (manual, systemd ...)
|
||||
2. Get present version
|
||||
```sh
|
||||
git -P tag --contains
|
||||
```
|
||||
3. List versions available
|
||||
```sh
|
||||
git -P tag --list "v*" --merged
|
||||
```
|
||||
4. Select the version
|
||||
```sh
|
||||
git checkout v2.2.5
|
||||
git switch -c v2.2.5
|
||||
```
|
||||
5. Upgrade Etherpad
|
||||
```sh
|
||||
./bin/run.sh
|
||||
```
|
||||
6. Stop with [CTRL-C]
|
||||
7. Restart your Etherpad service
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Tweak the settings
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "admin",
|
||||
"private": true,
|
||||
"version": "2.2.2",
|
||||
"version": "2.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
@ -11,32 +11,32 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-switch": "^1.1.0"
|
||||
"@radix-ui/react-switch": "^1.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"i18next": "^23.12.2",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"lucide-react": "^0.426.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"zustand": "^4.5.4"
|
||||
"@radix-ui/react-dialog": "^1.1.11",
|
||||
"@radix-ui/react-toast": "^1.2.11",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"i18next": "^25.0.2",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-router-dom": "^7.5.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.3.4",
|
||||
"vite-plugin-static-copy": "^2.3.1",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"zustand": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"ep_adminpads2_autoupdate.title": "Aktiviert oder deaktiviert automatische Aktualisierungen für die aktuelle Abfrage.",
|
||||
"ep_adminpads2_confirm": "Willst du das Pad {{padID}} wirklich löschen?",
|
||||
"ep_adminpads2_delete.value": "Löschen",
|
||||
"ep_adminpads2_cleanup": "Historie aufräumen",
|
||||
"ep_adminpads2_last-edited": "Zuletzt bearbeitet",
|
||||
"ep_adminpads2_loading": "Lädt...",
|
||||
"ep_adminpads2_manage-pads": "Pads verwalten",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"ep_adminpads2_autoupdate.title": "Enables or disables automatic updates for the current query.",
|
||||
"ep_adminpads2_confirm": "Do you really want to delete the pad {{padID}}?",
|
||||
"ep_adminpads2_delete.value": "Delete",
|
||||
"ep_adminpads2_cleanup": "Cleanup revisions",
|
||||
"ep_adminpads2_last-edited": "Last edited",
|
||||
"ep_adminpads2_loading": "Loading…",
|
||||
"ep_adminpads2_manage-pads": "Manage pads",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {useEffect} from 'react'
|
||||
import {useEffect, useState} from 'react'
|
||||
import './App.css'
|
||||
import {connect} from 'socket.io-client'
|
||||
import {isJSONClean} from './utils/utils.ts'
|
||||
|
@ -6,107 +6,115 @@ import {NavLink, Outlet, useNavigate} from "react-router-dom";
|
|||
import {useStore} from "./store/store.ts";
|
||||
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall} from "lucide-react";
|
||||
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall, LucideMenu} from "lucide-react";
|
||||
|
||||
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
|
||||
export const App = ()=> {
|
||||
const setSettings = useStore(state => state.setSettings);
|
||||
const {t} = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const WS_URL = import.meta.env.DEV ? 'http://localhost:9001' : ''
|
||||
export const App = () => {
|
||||
const setSettings = useStore(state => state.setSettings);
|
||||
const {t} = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [sidebarOpen, setSidebarOpen] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/admin-auth/', {
|
||||
method: 'POST'
|
||||
}).then((value)=>{
|
||||
if(!value.ok){
|
||||
navigate('/login')
|
||||
}
|
||||
}).catch(()=>{
|
||||
navigate('/login')
|
||||
})
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fetch('/admin-auth/', {
|
||||
method: 'POST'
|
||||
}).then((value) => {
|
||||
if (!value.ok) {
|
||||
navigate('/login')
|
||||
}
|
||||
}).catch(() => {
|
||||
navigate('/login')
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t('admin.page-title')
|
||||
useEffect(() => {
|
||||
document.title = t('admin.page-title')
|
||||
|
||||
useStore.getState().setShowLoading(true);
|
||||
const settingSocket = connect(`${WS_URL}/settings`, {
|
||||
transports: ['websocket'],
|
||||
});
|
||||
useStore.getState().setShowLoading(true);
|
||||
const settingSocket = connect(`${WS_URL}/settings`, {
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
|
||||
transports: ['websocket'],
|
||||
})
|
||||
const pluginsSocket = connect(`${WS_URL}/pluginfw/installer`, {
|
||||
transports: ['websocket'],
|
||||
})
|
||||
|
||||
pluginsSocket.on('connect', () => {
|
||||
useStore.getState().setPluginsSocket(pluginsSocket);
|
||||
});
|
||||
pluginsSocket.on('connect', () => {
|
||||
useStore.getState().setPluginsSocket(pluginsSocket);
|
||||
});
|
||||
|
||||
|
||||
settingSocket.on('connect', () => {
|
||||
useStore.getState().setSettingsSocket(settingSocket);
|
||||
useStore.getState().setShowLoading(false)
|
||||
settingSocket.emit('load');
|
||||
console.log('connected');
|
||||
});
|
||||
settingSocket.on('connect', () => {
|
||||
useStore.getState().setSettingsSocket(settingSocket);
|
||||
useStore.getState().setShowLoading(false)
|
||||
settingSocket.emit('load');
|
||||
console.log('connected');
|
||||
});
|
||||
|
||||
settingSocket.on('disconnect', (reason) => {
|
||||
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
|
||||
// server disconnect".
|
||||
useStore.getState().setShowLoading(true)
|
||||
if (reason === 'io server disconnect') {
|
||||
settingSocket.connect();
|
||||
}
|
||||
});
|
||||
settingSocket.on('disconnect', (reason) => {
|
||||
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
|
||||
// server disconnect".
|
||||
useStore.getState().setShowLoading(true)
|
||||
if (reason === 'io server disconnect') {
|
||||
settingSocket.connect();
|
||||
}
|
||||
});
|
||||
|
||||
settingSocket.on('settings', (settings) => {
|
||||
/* Check whether the settings.json is authorized to be viewed */
|
||||
if (settings.results === 'NOT_ALLOWED') {
|
||||
console.log('Not allowed to view settings.json')
|
||||
return;
|
||||
}
|
||||
settingSocket.on('settings', (settings) => {
|
||||
/* Check whether the settings.json is authorized to be viewed */
|
||||
if (settings.results === 'NOT_ALLOWED') {
|
||||
console.log('Not allowed to view settings.json')
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check to make sure the JSON is clean before proceeding */
|
||||
if (isJSONClean(settings.results)) {
|
||||
setSettings(settings.results);
|
||||
} else {
|
||||
alert('Invalid JSON');
|
||||
}
|
||||
useStore.getState().setShowLoading(false);
|
||||
});
|
||||
/* Check to make sure the JSON is clean before proceeding */
|
||||
if (isJSONClean(settings.results)) {
|
||||
setSettings(settings.results);
|
||||
} else {
|
||||
alert('Invalid JSON');
|
||||
}
|
||||
useStore.getState().setShowLoading(false);
|
||||
});
|
||||
|
||||
settingSocket.on('saveprogress', (status)=>{
|
||||
console.log(status)
|
||||
})
|
||||
settingSocket.on('saveprogress', (status) => {
|
||||
console.log(status)
|
||||
})
|
||||
|
||||
return () => {
|
||||
settingSocket.disconnect();
|
||||
pluginsSocket.disconnect()
|
||||
}
|
||||
}, []);
|
||||
return () => {
|
||||
settingSocket.disconnect();
|
||||
pluginsSocket.disconnect()
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div id="wrapper">
|
||||
<LoadingScreen/>
|
||||
<div className="menu">
|
||||
<div className="inner-menu">
|
||||
<span>
|
||||
return <div id="wrapper" className={`${sidebarOpen ? '': 'closed' }`}>
|
||||
<LoadingScreen/>
|
||||
<div className="menu">
|
||||
<div className="inner-menu">
|
||||
<span>
|
||||
<Crown width={40} height={40}/>
|
||||
<h1>Etherpad</h1>
|
||||
</span>
|
||||
<ul>
|
||||
<li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
|
||||
<li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li>
|
||||
<li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
|
||||
<li><NavLink to={"/pads"}><NotepadText/><Trans
|
||||
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
|
||||
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="innerwrapper">
|
||||
<Outlet/>
|
||||
</div>
|
||||
<ul onClick={()=>{
|
||||
if (window.innerWidth < 768) {
|
||||
setSidebarOpen(false)
|
||||
}
|
||||
}}>
|
||||
<li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
|
||||
<li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li>
|
||||
<li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
|
||||
<li><NavLink to={"/pads"}><NotepadText/><Trans
|
||||
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
|
||||
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button id="icon-button" onClick={() => {
|
||||
setSidebarOpen(!sidebarOpen)
|
||||
}}><LucideMenu/></button>
|
||||
<div className="innerwrapper">
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {FC, ReactElement} from "react";
|
||||
import {FC, JSX, ReactElement} from "react";
|
||||
|
||||
export type IconButtonProps = {
|
||||
icon: JSX.Element,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,16 +4,54 @@ import {InstalledPlugin, PluginDef, SearchParams} from "./Plugin.ts";
|
|||
import {useDebounce} from "../utils/useDebounce.ts";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import {SearchField} from "../components/SearchField.tsx";
|
||||
import {Download, Trash} from "lucide-react";
|
||||
import {ArrowUpFromDot, Download, Trash} from "lucide-react";
|
||||
import {IconButton} from "../components/IconButton.tsx";
|
||||
import {determineSorting} from "../utils/sorting.ts";
|
||||
|
||||
|
||||
export const HomePage = () => {
|
||||
const pluginsSocket = useStore(state=>state.pluginsSocket)
|
||||
const [plugins,setPlugins] = useState<PluginDef[]>([])
|
||||
const [installedPlugins, setInstalledPlugins] = useState<InstalledPlugin[]>([])
|
||||
const installedPlugins = useStore(state=>state.installedPlugins)
|
||||
const setInstalledPlugins = useStore(state=>state.setInstalledPlugins)
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
offset: 0,
|
||||
limit: 99999,
|
||||
sortBy: 'name',
|
||||
sortDir: 'asc',
|
||||
searchTerm: ''
|
||||
})
|
||||
|
||||
const filteredInstallablePlugins = useMemo(()=>{
|
||||
return plugins.sort((a, b)=>{
|
||||
if(searchParams.sortBy === "version"){
|
||||
if(searchParams.sortDir === "asc"){
|
||||
return a.version.localeCompare(b.version)
|
||||
}
|
||||
return b.version.localeCompare(a.version)
|
||||
}
|
||||
|
||||
if(searchParams.sortBy === "last-updated"){
|
||||
if(searchParams.sortDir === "asc"){
|
||||
return a.time.localeCompare(b.time)
|
||||
}
|
||||
return b.time.localeCompare(a.time)
|
||||
}
|
||||
|
||||
|
||||
if (searchParams.sortBy === "name") {
|
||||
if(searchParams.sortDir === "asc"){
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
return b.name.localeCompare(a.name)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}, [plugins, searchParams])
|
||||
|
||||
const sortedInstalledPlugins = useMemo(()=>{
|
||||
return installedPlugins.sort((a, b)=>{
|
||||
return useStore.getState().installedPlugins.sort((a, b)=>{
|
||||
|
||||
if(a.name < b.name){
|
||||
return -1
|
||||
}
|
||||
|
@ -23,14 +61,8 @@ export const HomePage = () => {
|
|||
return 0
|
||||
})
|
||||
|
||||
} ,[installedPlugins])
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
offset: 0,
|
||||
limit: 99999,
|
||||
sortBy: 'name',
|
||||
sortDir: 'asc',
|
||||
searchTerm: ''
|
||||
})
|
||||
} ,[installedPlugins, searchParams])
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const {t} = useTranslation()
|
||||
|
||||
|
@ -47,17 +79,16 @@ export const HomePage = () => {
|
|||
})
|
||||
|
||||
pluginsSocket.on('results:updatable', (data) => {
|
||||
data.updatable.forEach((pluginName: string) => {
|
||||
setInstalledPlugins(installedPlugins.map(plugin => {
|
||||
if (plugin.name === pluginName) {
|
||||
return {
|
||||
...plugin,
|
||||
updatable: true
|
||||
}
|
||||
}
|
||||
return plugin
|
||||
}))
|
||||
})
|
||||
const newInstalledPlugins = useStore.getState().installedPlugins.map(plugin => {
|
||||
if (data.updatable.includes(plugin.name)) {
|
||||
return {
|
||||
...plugin,
|
||||
updatable: true
|
||||
}
|
||||
}
|
||||
return plugin
|
||||
})
|
||||
setInstalledPlugins(newInstalledPlugins)
|
||||
})
|
||||
|
||||
pluginsSocket.on('finished:install', () => {
|
||||
|
@ -128,6 +159,7 @@ export const HomePage = () => {
|
|||
})
|
||||
}, 500, [searchTerm])
|
||||
|
||||
|
||||
return <div>
|
||||
<h1><Trans i18nKey="admin_plugins"/></h1>
|
||||
|
||||
|
@ -149,7 +181,7 @@ export const HomePage = () => {
|
|||
<td>
|
||||
{
|
||||
plugin.updatable ?
|
||||
<button onClick={() => installPlugin(plugin.name)}>Update</button>
|
||||
<IconButton onClick={() => installPlugin(plugin.name)} icon={<ArrowUpFromDot/>} title="Update"></IconButton>
|
||||
: <IconButton disabled={plugin.name == "ep_etherpad-lite"} icon={<Trash/>} title={<Trans i18nKey="admin_plugins.installed_uninstall.value"/>} onClick={() => uninstallPlugin(plugin.name)}/>
|
||||
}
|
||||
</td>
|
||||
|
@ -162,19 +194,39 @@ export const HomePage = () => {
|
|||
<h2><Trans i18nKey="admin_plugins.available"/></h2>
|
||||
<SearchField onChange={v=>{setSearchTerm(v.target.value)}} placeholder={t('admin_plugins.available_search.placeholder')} value={searchTerm}/>
|
||||
|
||||
<div className="table-container">
|
||||
<table id="available-plugins">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><Trans i18nKey="admin_plugins.name"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'name')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'name',
|
||||
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
|
||||
})
|
||||
}}>
|
||||
<Trans i18nKey="admin_plugins.name" /></th>
|
||||
<th style={{width: '30%'}}><Trans i18nKey="admin_plugins.description"/></th>
|
||||
<th><Trans i18nKey="admin_plugins.version"/></th>
|
||||
<th><Trans i18nKey="admin_plugins.last-update"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'version')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'version',
|
||||
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
|
||||
})
|
||||
}}><Trans i18nKey="admin_plugins.version"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'last-updated')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'last-updated',
|
||||
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
|
||||
})
|
||||
}}><Trans i18nKey="admin_plugins.last-update"/></th>
|
||||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody style={{overflow: 'auto'}}>
|
||||
{(plugins.length > 0) ?
|
||||
plugins.map((plugin) => {
|
||||
{(filteredInstallablePlugins.length > 0) ?
|
||||
filteredInstallablePlugins.map((plugin) => {
|
||||
return <tr key={plugin.name}>
|
||||
<td><a rel="noopener noreferrer" href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
|
||||
<td>{plugin.description}</td>
|
||||
|
@ -190,5 +242,6 @@ export const HomePage = () => {
|
|||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {useDebounce} from "../utils/useDebounce.ts";
|
|||
import {determineSorting} from "../utils/sorting.ts";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import {IconButton} from "../components/IconButton.tsx";
|
||||
import {ChevronLeft, ChevronRight, Eye, Trash2} from "lucide-react";
|
||||
import {ChevronLeft, ChevronRight, Eye, Trash2, FileStack} from "lucide-react";
|
||||
import {SearchField} from "../components/SearchField.tsx";
|
||||
|
||||
export const PadPage = ()=>{
|
||||
|
@ -23,6 +23,7 @@ export const PadPage = ()=>{
|
|||
const pads = useStore(state=>state.pads)
|
||||
const [currentPage, setCurrentPage] = useState<number>(0)
|
||||
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
|
||||
const [errorText, setErrorText] = useState<string|null>(null)
|
||||
const [padToDelete, setPadToDelete] = useState<string>('')
|
||||
const pages = useMemo(()=>{
|
||||
if(!pads){
|
||||
|
@ -68,12 +69,35 @@ export const PadPage = ()=>{
|
|||
results: newPads
|
||||
})
|
||||
})
|
||||
|
||||
settingsSocket.on('results:cleanupPadRevisions', (data)=>{
|
||||
let newPads = useStore.getState().pads?.results ?? []
|
||||
|
||||
if (data.error) {
|
||||
setErrorText(data.error)
|
||||
return
|
||||
}
|
||||
|
||||
newPads.forEach((pad)=>{
|
||||
if (pad.padName === data.padId) {
|
||||
pad.revisionNumber = data.keepRevisions
|
||||
}
|
||||
})
|
||||
|
||||
useStore.getState().setPads({
|
||||
results: newPads,
|
||||
total: useStore.getState().pads!.total
|
||||
})
|
||||
})
|
||||
}, [settingsSocket, pads]);
|
||||
|
||||
const deletePad = (padID: string)=>{
|
||||
settingsSocket?.emit('deletePad', padID)
|
||||
}
|
||||
|
||||
const cleanupPad = (padID: string)=>{
|
||||
settingsSocket?.emit('cleanupPadRevisions', padID)
|
||||
}
|
||||
|
||||
|
||||
return <div>
|
||||
|
@ -100,11 +124,26 @@ export const PadPage = ()=>{
|
|||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<Dialog.Root open={errorText !== null}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay"/>
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<div>
|
||||
<div>Error occured: {errorText}</div>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={() => {
|
||||
setErrorText(null)
|
||||
}}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<h1><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></h1>
|
||||
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<tr className="search-pads">
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'padName')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
|
@ -136,7 +175,7 @@ export const PadPage = ()=>{
|
|||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody className="search-pads-body">
|
||||
{
|
||||
pads?.results?.map((pad)=>{
|
||||
return <tr key={pad.padName}>
|
||||
|
@ -150,6 +189,9 @@ export const PadPage = ()=>{
|
|||
setPadToDelete(pad.padName)
|
||||
setDeleteDialog(true)
|
||||
}}/>
|
||||
<IconButton icon={<FileStack/>} title={<Trans i18nKey="ep_admin_pads:ep_adminpads2_cleanup"/>} onClick={()=>{
|
||||
cleanupPad(pad.padName)
|
||||
}}/>
|
||||
<IconButton icon={<Eye/>} title="view" onClick={()=>window.open(`/p/${pad.padName}`, '_blank')}/>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -20,7 +20,7 @@ export type SearchParams = {
|
|||
searchTerm: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
sortBy: 'name'|'version',
|
||||
sortBy: 'name'|'version'|'last-updated',
|
||||
sortDir: 'asc'|'desc'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {create} from "zustand";
|
||||
import {Socket} from "socket.io-client";
|
||||
import {PadSearchResult} from "../utils/PadSearch.ts";
|
||||
import {InstalledPlugin} from "../pages/Plugin.ts";
|
||||
|
||||
type ToastState = {
|
||||
description?:string,
|
||||
|
@ -22,7 +23,9 @@ type StoreState = {
|
|||
toastState: ToastState,
|
||||
setToastState: (val: ToastState)=>void,
|
||||
pads: PadSearchResult|undefined,
|
||||
setPads: (pads: PadSearchResult)=>void
|
||||
setPads: (pads: PadSearchResult)=>void,
|
||||
installedPlugins: InstalledPlugin[],
|
||||
setInstalledPlugins: (plugins: InstalledPlugin[])=>void
|
||||
}
|
||||
|
||||
|
||||
|
@ -43,5 +46,7 @@ export const useStore = create<StoreState>()((set) => ({
|
|||
success: false
|
||||
},
|
||||
pads: undefined,
|
||||
setPads: (pads)=>set({pads})
|
||||
setPads: (pads)=>set({pads}),
|
||||
installedPlugins: [],
|
||||
setInstalledPlugins: (plugins)=>set({installedPlugins: plugins})
|
||||
}));
|
||||
|
|
51
bin/installLocalPlugins.sh
Executable file
51
bin/installLocalPlugins.sh
Executable file
|
@ -0,0 +1,51 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
trim() {
|
||||
local var="$*"
|
||||
# remove leading whitespace characters
|
||||
var="${var#"${var%%[![:space:]]*}"}"
|
||||
# remove trailing whitespace characters
|
||||
var="${var%"${var##*[![:space:]]}"}"
|
||||
printf '%s' "$var"
|
||||
}
|
||||
|
||||
# Move to the Etherpad base directory.
|
||||
MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
|
||||
cd "${MY_DIR}/.." || exit 1
|
||||
|
||||
# Source constants and useful functions
|
||||
. bin/functions.sh
|
||||
|
||||
PNPM_OPTIONS=
|
||||
if [ ! -z "${NODE_ENV-}" ]; then
|
||||
if [ "$NODE_ENV" == 'production' ]; then
|
||||
PNPM_OPTIONS='--prod'
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -z "${ETHERPAD_LOCAL_PLUGINS_ENV-}" ]; then
|
||||
if [ "$ETHERPAD_LOCAL_PLUGINS_ENV" == 'production' ]; then
|
||||
PNPM_OPTIONS='--prod'
|
||||
elif [ "$ETHERPAD_LOCAL_PLUGINS_ENV" == 'development' ]; then
|
||||
PNPM_OPTIONS='-D'
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then
|
||||
readarray -d ' ' plugins <<< "${ETHERPAD_LOCAL_PLUGINS}"
|
||||
for plugin in "${plugins[@]}"; do
|
||||
plugin=$(trim "$plugin")
|
||||
if [ -d "local_plugins/${plugin}" ]; then
|
||||
echo "Installing plugin: '${plugin}'"
|
||||
pnpm install -w ${PNPM_OPTIONS:-} "local_plugins/${plugin}/"
|
||||
else
|
||||
( echo "Error. Directory 'local_plugins/${plugin}' for local plugin " \
|
||||
"'${plugin}' missing" >&2 )
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo 'No local plugings to install.'
|
||||
fi
|
63
bin/make_docs.ts
Normal file
63
bin/make_docs.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import {exec} from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import pjson from '../src/package.json'
|
||||
|
||||
const VERSION=pjson.version
|
||||
console.log(`Building docs for version ${VERSION}`)
|
||||
|
||||
const createDirIfNotExists = (dir: fs.PathLike) => {
|
||||
if (!fs.existsSync(dir)){
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function copyFolderSync(from: fs.PathLike, to: fs.PathLike) {
|
||||
if(fs.existsSync(to)){
|
||||
const stat = fs.lstatSync(to)
|
||||
if (stat.isDirectory()){
|
||||
fs.rmSync(to, { recursive: true })
|
||||
}
|
||||
else{
|
||||
fs.rmSync(to)
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(to);
|
||||
fs.readdirSync(from).forEach(element => {
|
||||
if (fs.lstatSync(path.join(<string>from, element)).isFile()) {
|
||||
if (typeof from === "string") {
|
||||
if (typeof to === "string") {
|
||||
fs.copyFileSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (typeof from === "string") {
|
||||
if (typeof to === "string") {
|
||||
copyFolderSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exec('asciidoctor -v', (err,stdout)=>{
|
||||
if (err){
|
||||
console.log('Please install asciidoctor')
|
||||
console.log('https://asciidoctor.org/docs/install-toolchain/')
|
||||
process.exit(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
createDirIfNotExists('../out')
|
||||
createDirIfNotExists('../out/doc')
|
||||
createDirIfNotExists('../out/doc/api')
|
||||
|
||||
|
||||
|
||||
exec(`asciidoctor -D ../out/doc ../doc/index.adoc ../*/**.adoc -a VERSION=${VERSION}`)
|
||||
exec(`asciidoctor -D ../out/doc/api ../doc/api/*.adoc -a VERSION=${VERSION}`)
|
||||
|
||||
copyFolderSync('../doc/public/', '../out/doc/')
|
83
bin/migrateDB.ts
Normal file
83
bin/migrateDB.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
// DB migration
|
||||
import {readFileSync} from 'node:fs'
|
||||
import {Database, DatabaseType} from "ueberdb2";
|
||||
import path from "node:path";
|
||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||
|
||||
|
||||
// file1 = source, file2 = target
|
||||
// pnpm run migrateDB --file1 <db1.json> --file2 <db2.json>
|
||||
const arg = process.argv.slice(2);
|
||||
|
||||
if (arg.length != 4) {
|
||||
console.error('Wrong number of arguments!. Call with pnpm run migrateDB --file1 source.json target.json')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
type SettingsConfig = {
|
||||
dbType: string,
|
||||
dbSettings: any
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"dbType": "<your-db-type>",
|
||||
"dbSettings": {
|
||||
<your-db-settings>
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
let firstDBSettingsFile: string
|
||||
let secondDBSettingsFile: string
|
||||
|
||||
|
||||
if (arg[0] == "--file1") {
|
||||
firstDBSettingsFile = arg[1]
|
||||
} else if (arg[0] === "--file2") {
|
||||
secondDBSettingsFile = arg[1]
|
||||
}
|
||||
|
||||
if (arg[2] == "--file1") {
|
||||
firstDBSettingsFile = arg[3]
|
||||
} else if (arg[2] === "--file2") {
|
||||
secondDBSettingsFile = arg[3]
|
||||
}
|
||||
|
||||
|
||||
|
||||
const settingsfile = JSON.parse(readFileSync(path.join(settings.root,firstDBSettingsFile!)).toString()) as SettingsConfig
|
||||
const settingsfile2 = JSON.parse(readFileSync(path.join(settings.root,secondDBSettingsFile!)).toString()) as SettingsConfig
|
||||
|
||||
console.log(settingsfile2)
|
||||
if ("filename" in settingsfile.dbSettings) {
|
||||
settingsfile.dbSettings.filename = path.join(settings.root, settingsfile.dbSettings.filename)
|
||||
console.log(settingsfile.dbType + " location is "+ settingsfile.dbSettings.filename)
|
||||
}
|
||||
|
||||
if ("filename" in settingsfile2.dbSettings) {
|
||||
settingsfile2.dbSettings.filename = path.join(settings.root, settingsfile2.dbSettings.filename)
|
||||
console.log(settingsfile2.dbType + " location is "+ settingsfile2.dbSettings.filename)
|
||||
}
|
||||
|
||||
const ueberdb1 = new Database(settingsfile.dbType as DatabaseType, settingsfile.dbSettings)
|
||||
const ueberdb2 = new Database(settingsfile2.dbType as DatabaseType, settingsfile2.dbSettings)
|
||||
|
||||
const handleSync = async ()=>{
|
||||
await ueberdb1.init()
|
||||
await ueberdb2.init()
|
||||
|
||||
const allKeys = await ueberdb1.findKeys('*','')
|
||||
for (const key of allKeys) {
|
||||
const foundVal = await ueberdb1.get(key)!
|
||||
await ueberdb2.set(key, foundVal)
|
||||
}
|
||||
}
|
||||
|
||||
handleSync().then(()=>{
|
||||
console.log("Done syncing dbs")
|
||||
}).catch(e=>{
|
||||
console.log(`Error syncing db ${e}`)
|
||||
})
|
||||
|
||||
|
|
@ -1,25 +1,26 @@
|
|||
{
|
||||
"name": "bin",
|
||||
"version": "2.2.2",
|
||||
"version": "2.3.0",
|
||||
"description": "",
|
||||
"main": "checkAllPads.js",
|
||||
"directories": {
|
||||
"doc": "doc"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.3",
|
||||
"axios": "^1.8.4",
|
||||
"ep_etherpad-lite": "workspace:../src",
|
||||
"log4js": "^6.9.1",
|
||||
"semver": "^7.6.3",
|
||||
"tsx": "^4.17.0",
|
||||
"ueberdb2": "^4.2.92"
|
||||
"semver": "^7.7.1",
|
||||
"tsx": "^4.19.3",
|
||||
"ueberdb2": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"typescript": "^5.5.4"
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/semver": "^7.7.0",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"makeDocs": "node --import tsx make_docs.ts",
|
||||
"checkPad": "node --import tsx checkPad.ts",
|
||||
"checkAllPads": "node --import tsx checkAllPads.ts",
|
||||
"createUserSession": "node --import tsx createUserSession.ts",
|
||||
|
@ -33,7 +34,8 @@
|
|||
"stalePlugins": "node --import tsx ./plugins/stalePlugins.ts",
|
||||
"checkPlugin": "node --import tsx ./plugins/checkPlugin.ts",
|
||||
"plugins": "node --import tsx ./plugins.ts",
|
||||
"generateChangelog": "node --import tsx generateReleaseNotes.ts"
|
||||
"generateChangelog": "node --import tsx generateReleaseNotes.ts",
|
||||
"migrateDB": "node --import tsx migrateDB.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
|
|
|
@ -23,17 +23,13 @@ const possibleActions = [
|
|||
]
|
||||
|
||||
const install = ()=> {
|
||||
|
||||
let registryPlugins: string[] = [];
|
||||
let localPlugins: string[] = [];
|
||||
|
||||
if (args.indexOf('--path') !== -1) {
|
||||
const indexToSplit = args.indexOf('--path');
|
||||
registryPlugins = args.slice(1, indexToSplit);
|
||||
localPlugins = args.slice(indexToSplit + 1);
|
||||
} else {
|
||||
registryPlugins = args;
|
||||
}
|
||||
const argsAsString: string = args.join(" ");
|
||||
const regexRegistryPlugins = /(?<=i\s)(.*?)(?=--github|--path|$)/;
|
||||
const regexLocalPlugins = /(?<=--path\s)(.*?)(?=--github|$)/;
|
||||
const regexGithubPlugins = /(?<=--github\s)(.*?)(?=--path|$)/;
|
||||
const registryPlugins = argsAsString.match(regexRegistryPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||
const localPlugins = argsAsString.match(regexLocalPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||
const githubPlugins = argsAsString.match(regexGithubPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||
|
||||
async function run() {
|
||||
for (const plugin of registryPlugins) {
|
||||
|
@ -53,6 +49,11 @@ const install = ()=> {
|
|||
console.log(`Installing plugin from path: ${plugin}`);
|
||||
await linkInstaller.installFromPath(plugin);
|
||||
}
|
||||
|
||||
for (const plugin of githubPlugins) {
|
||||
console.log(`Installing plugin from github: ${plugin}`);
|
||||
await linkInstaller.installFromGitHub(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
|
|
|
@ -197,7 +197,7 @@ try {
|
|||
|
||||
try {
|
||||
console.log('Building documentation...');
|
||||
run('node ./make_docs.js');
|
||||
run('pnpm run makeDocs');
|
||||
console.log('Updating ether.github.com master branch...');
|
||||
run('git pull --ff-only', {cwd: '../ether.github.com/'});
|
||||
console.log('Committing documentation...');
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export default defineConfig({
|
|||
{ text: 'Stats', link: '/stats.md' },
|
||||
{text: 'Skins', link: '/skins.md' },
|
||||
{text: 'Demo', link: '/demo.md' },
|
||||
{text: 'CLI', link: '/cli.md'},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
29
doc/cli.md
Normal file
29
doc/cli.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# CLI
|
||||
|
||||
You can find different tools for migrating things, checking your Etherpad health in the bin directory.
|
||||
One of these is the migrateDB command. It takes two settings.json files and copies data from one source to another one.
|
||||
In this example we migrate from the old dirty db to the new rustydb engine. So we copy these files to the root of the etherpad-directory.
|
||||
|
||||
````json
|
||||
{
|
||||
"dbType": "dirty",
|
||||
"dbSettings": {
|
||||
"filename": "./var/rusty.db"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
|
||||
````json
|
||||
{
|
||||
"dbType": "rustydb",
|
||||
"dbSettings": {
|
||||
"filename": "./var/rusty2.db"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
After that we need to move the data from dirty to rustydb.
|
||||
Therefore, we call `pnpm run migrateDB --file1 test1.json --file2 test2.json` with these two files in our root directories. After some time the data should be copied over to the new database.
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.3.2"
|
||||
"vitepress": "^1.6.3"
|
||||
},
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
|
|
|
@ -9,6 +9,7 @@ services:
|
|||
build:
|
||||
context: .
|
||||
args:
|
||||
# Attention: installed plugins in the node_modules folder get overwritten during volume mount in dev
|
||||
ETHERPAD_PLUGINS:
|
||||
# change from development to production if needed
|
||||
target: development
|
||||
|
@ -57,7 +58,7 @@ services:
|
|||
# ports:
|
||||
# - "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data/pgdata
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
postgres_data:
|
||||
|
|
|
@ -42,7 +42,7 @@ services:
|
|||
# ports:
|
||||
# - "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data/pgdata
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
|
3
local_plugins/.gitignore
vendored
Normal file
3
local_plugins/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# ignore everything
|
||||
*
|
||||
!.gitignore
|
55
make_docs.js
55
make_docs.js
|
@ -1,55 +0,0 @@
|
|||
import {exec} from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import pjson from './src/package.json' assert {type: "json"}
|
||||
|
||||
const VERSION=pjson.version
|
||||
console.log(`Building docs for version ${VERSION}`)
|
||||
|
||||
const createDirIfNotExists = (dir) => {
|
||||
if (!fs.existsSync(dir)){
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function copyFolderSync(from, to) {
|
||||
if(fs.existsSync(to)){
|
||||
const stat = fs.lstatSync(to)
|
||||
if (stat.isDirectory()){
|
||||
fs.rmSync(to, { recursive: true })
|
||||
}
|
||||
else{
|
||||
fs.rmSync(to)
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(to);
|
||||
fs.readdirSync(from).forEach(element => {
|
||||
if (fs.lstatSync(path.join(from, element)).isFile()) {
|
||||
fs.copyFileSync(path.join(from, element), path.join(to, element))
|
||||
} else {
|
||||
copyFolderSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exec('asciidoctor -v', (err,stdout)=>{
|
||||
if (err){
|
||||
console.log('Please install asciidoctor')
|
||||
console.log('https://asciidoctor.org/docs/install-toolchain/')
|
||||
process.exit(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
createDirIfNotExists('./out')
|
||||
createDirIfNotExists('./out/doc')
|
||||
createDirIfNotExists('./out/doc/api')
|
||||
|
||||
|
||||
|
||||
exec(`asciidoctor -D out/doc doc/index.adoc */**.adoc -a VERSION=${VERSION}`)
|
||||
exec(`asciidoctor -D out/doc/api ./doc/api/*.adoc -a VERSION=${VERSION}`)
|
||||
|
||||
copyFolderSync('./doc/public/', './out/doc/')
|
|
@ -30,7 +30,8 @@
|
|||
"remove-plugins": "pnpm --filter bin run remove-plugins",
|
||||
"list-plugins": "pnpm --filter bin run list-plugins",
|
||||
"build:etherpad": "pnpm --filter admin run build-copy && pnpm --filter ui run build-copy",
|
||||
"build:ui": "pnpm --filter ui run build-copy && pnpm --filter admin run build-copy"
|
||||
"build:ui": "pnpm --filter ui run build-copy && pnpm --filter admin run build-copy",
|
||||
"makeDocs": "pnpm --filter bin run makeDocs"
|
||||
},
|
||||
"dependencies": {
|
||||
"ep_etherpad-lite": "workspace:./src"
|
||||
|
@ -49,6 +50,6 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/ether/etherpad-lite.git"
|
||||
},
|
||||
"version": "2.2.2",
|
||||
"version": "2.3.0",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
7204
pnpm-lock.yaml
generated
7204
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -4,3 +4,7 @@ packages:
|
|||
- bin
|
||||
- doc
|
||||
- ui
|
||||
onlyBuiltDependencies:
|
||||
- '@scarf/scarf'
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
* 3) if you want to use newlines in the default value of a string parameter,
|
||||
* use "\n" as usual.
|
||||
*
|
||||
* "defaultPadText" : "${DEFAULT_PAD_TEXT}Line 1\nLine 2"
|
||||
* "defaultPadText" : "${DEFAULT_PAD_TEXT:Line 1\nLine 2}"
|
||||
*/
|
||||
{
|
||||
/*
|
||||
|
@ -171,6 +171,14 @@
|
|||
*/
|
||||
"showSettingsInAdminPage": "${SHOW_SETTINGS_IN_ADMIN_PAGE:true}",
|
||||
|
||||
/*
|
||||
* Settings for cleanup of pads
|
||||
*/
|
||||
"cleanup": {
|
||||
"enabled": false,
|
||||
"keepRevisions": 5
|
||||
},
|
||||
|
||||
/*
|
||||
The authentication method used by the server.
|
||||
The default value is sso
|
||||
|
@ -194,6 +202,15 @@
|
|||
},
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* Enables the use of a different server. We have a different one that syncs changes from the original server.
|
||||
* It is hosted on GitHub and should not be blocked by many firewalls.
|
||||
* https://etherpad.org/ep_infos
|
||||
*/
|
||||
|
||||
"updateServer": "https://etherpad.org/ep_infos",
|
||||
|
||||
/*
|
||||
* The type of the database.
|
||||
*
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
* 3) if you want to use newlines in the default value of a string parameter,
|
||||
* use "\n" as usual.
|
||||
*
|
||||
* "defaultPadText" : "${DEFAULT_PAD_TEXT}Line 1\nLine 2"
|
||||
* "defaultPadText" : "${DEFAULT_PAD_TEXT:Line 1\nLine 2}"
|
||||
*/
|
||||
{
|
||||
/*
|
||||
|
@ -162,6 +162,14 @@
|
|||
*/
|
||||
"showSettingsInAdminPage": true,
|
||||
|
||||
/*
|
||||
* Settings for cleanup of pads
|
||||
*/
|
||||
"cleanup": {
|
||||
"enabled": false,
|
||||
"keepRevisions": 5
|
||||
},
|
||||
|
||||
/*
|
||||
* Node native SSL support
|
||||
*
|
||||
|
@ -271,6 +279,14 @@
|
|||
"pageDown": true
|
||||
},
|
||||
|
||||
/*
|
||||
* Enables the use of a different server. We have a different one that syncs changes from the original server.
|
||||
* It is hosted on GitHub and should not be blocked by many firewalls.
|
||||
* https://etherpad.org/ep_infos
|
||||
*/
|
||||
|
||||
"updateServer": "https://etherpad.org/ep_infos",
|
||||
|
||||
/*
|
||||
* Should we suppress errors from being visible in the default Pad Text?
|
||||
*/
|
||||
|
|
|
@ -82,6 +82,12 @@
|
|||
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/errorhandling"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "restApi",
|
||||
"hooks": {
|
||||
"expressCreateServer": "ep_etherpad-lite/node/handler/RestAPI"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "socketio",
|
||||
"hooks": {
|
||||
|
|
|
@ -16,10 +16,18 @@
|
|||
]
|
||||
},
|
||||
"admin.page-title": "لوحة تحكم المسؤول - Etherpad",
|
||||
"admin_plugins": "مدير المكونات الإضافية",
|
||||
"admin_plugins.available": "المكونات الإضافية المتاحة",
|
||||
"admin_plugins.available_not-found": "لم يتم العثور على أي مكونات إضافية.",
|
||||
"admin_plugins.available_fetching": "جاري الجلب…",
|
||||
"admin_plugins.available_install.value": "تنصيب",
|
||||
"admin_plugins.available_search.placeholder": "ابحث عن المكونات الإضافية للتثبيت",
|
||||
"admin_plugins.description": "الوصف",
|
||||
"admin_plugins.installed": "الإضافات المثبتة",
|
||||
"admin_plugins.installed_fetching": "جارٍ إحضار المكونات الإضافية المثبتة ...",
|
||||
"admin_plugins.installed_nothing": "لم تقم بتثبيت أي مكونات إضافية حتى الآن.",
|
||||
"admin_plugins.installed_uninstall.value": "إلغاء التثبيت",
|
||||
"admin_plugins.last-update": "آخر تحديث",
|
||||
"admin_plugins.name": "الاسم",
|
||||
"admin_plugins.page-title": "مدير البرنامج المساعد - Etherpad",
|
||||
"admin_plugins.version": "الإصدار",
|
||||
|
@ -31,8 +39,10 @@
|
|||
"admin_plugins_info.plugins": "الإضافات المثبتة",
|
||||
"admin_plugins_info.page-title": "معلومات البرنامج المساعد - Etherpad",
|
||||
"admin_plugins_info.version": "إصدار Etherpad",
|
||||
"admin_plugins_info.version_latest": "أحدث إصدار متاح",
|
||||
"admin_plugins_info.version_number": "رقم الإصدار",
|
||||
"admin_settings": "إعدادات",
|
||||
"admin_settings.current": "التكوين الحالي",
|
||||
"admin_settings.current_example-devel": "مثال على قالب إعدادات التطوير",
|
||||
"admin_settings.current_example-prod": "مثال على قالب إعدادات الإنتاج",
|
||||
"admin_settings.current_restart.value": "أعد تشغيل Etherpad",
|
||||
|
@ -73,6 +83,8 @@
|
|||
"pad.settings.fontType": "نوع الخط:",
|
||||
"pad.settings.fontType.normal": "عادي",
|
||||
"pad.settings.language": "اللغة:",
|
||||
"pad.settings.deletePad": "حذف الوسادة",
|
||||
"pad.delete.confirm": "هل تريد حقا حذف هذه الوسادة؟",
|
||||
"pad.settings.about": "حول",
|
||||
"pad.settings.poweredBy": "مدعوم من",
|
||||
"pad.importExport.import_export": "استيراد/تصدير",
|
||||
|
@ -110,6 +122,8 @@
|
|||
"pad.modals.deleted": "محذوف.",
|
||||
"pad.modals.deleted.explanation": "تمت إزالة هذا الباد.",
|
||||
"pad.modals.rateLimited": "معدل محدود.",
|
||||
"pad.modals.rateLimited.explanation": "لقد أرسلت الكثير من الرسائل إلى هذه اللوحة مما أدى إلى قطع الاتصال بك.",
|
||||
"pad.modals.rejected.explanation": "رفض الخادم الرسالة التي أرسلها متصفحك.",
|
||||
"pad.modals.rejected.cause": "ربما تم تحديث الخادم أثناء عرض اللوحة ، أو ربما كان هناك خطأ في Etherpad. حاول إعادة تحميل الصفحة.",
|
||||
"pad.modals.disconnected": "لم تعد متصلا.",
|
||||
"pad.modals.disconnected.explanation": "تم فقدان الاتصال بالخادم",
|
||||
|
|
|
@ -35,6 +35,10 @@
|
|||
"admin_plugins_info.version_number": "Нумар вэрсіі",
|
||||
"admin_settings": "Налады",
|
||||
"admin_settings.current": "Цяперашняя канфігурацыя",
|
||||
"admin_settings.current_example-devel": "Прыклад шаблёну наладаў распрацоўкі",
|
||||
"admin_settings.current_example-prod": "Прыклад шаблёну наладаў вытворчасьці",
|
||||
"admin_settings.current_restart.value": "Перазапуск Etherpad",
|
||||
"admin_settings.current_save.value": "Захаваць налады",
|
||||
"admin_settings.page-title": "Налады — Etherpad",
|
||||
"index.newPad": "Стварыць",
|
||||
"index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:",
|
||||
|
@ -72,6 +76,7 @@
|
|||
"pad.settings.fontType.normal": "Звычайны",
|
||||
"pad.settings.language": "Мова:",
|
||||
"pad.settings.about": "Пра",
|
||||
"pad.settings.poweredBy": "Працуе на",
|
||||
"pad.importExport.import_export": "Імпарт/Экспарт",
|
||||
"pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты",
|
||||
"pad.importExport.importSuccessful": "Пасьпяхова!",
|
||||
|
@ -106,6 +111,9 @@
|
|||
"pad.modals.corruptPad.cause": "Гэта можа быць выклікана няправільнай канфігурацыяй сэрвэру або іншымі нечаканымі дзеяньнямі. Калі ласка, скантактуйцеся з адміністратарам службы.",
|
||||
"pad.modals.deleted": "Выдалены.",
|
||||
"pad.modals.deleted.explanation": "Гэты дакумэнт быў выдалены.",
|
||||
"pad.modals.rateLimited": "Хуткасьць абмежаваная.",
|
||||
"pad.modals.rateLimited.explanation": "Вы адаслалі так шмат паведамленьняў, што гэты дакумэнт вас адключыў.",
|
||||
"pad.modals.rejected.explanation": "Сэрвэр адхіліў паведамленьне, адасланае вашым броўзэрам.",
|
||||
"pad.modals.disconnected": "Вы былі адключаныя.",
|
||||
"pad.modals.disconnected.explanation": "Злучэньне з сэрвэрам было страчанае",
|
||||
"pad.modals.disconnected.cause": "Магчыма, сэрвэр недаступны. Калі ласка, паведаміце адміністратару службы, калі праблема будзе паўтарацца.",
|
||||
|
|
49
src/locales/ce.json
Normal file
49
src/locales/ce.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Умар"
|
||||
]
|
||||
},
|
||||
"admin.page-title": "Администраторан панель — Etherpad",
|
||||
"admin_plugins": "Плагинийн менеджер",
|
||||
"admin_plugins.available": "ТӀекхочуш йолу плагинаш",
|
||||
"admin_plugins.available_not-found": "Плагинаш ца карийна.",
|
||||
"admin_plugins.available_fetching": "Схьаоьцуш...",
|
||||
"admin_plugins.available_install.value": "ДӀахӀоттайе",
|
||||
"admin_plugins.installed_uninstall.value": "ДӀайаккха",
|
||||
"admin_plugins.last-update": "ТӀаьххьара карлайаккхар",
|
||||
"admin_plugins.name": "ЦӀе",
|
||||
"admin_plugins.page-title": "Плагинийн менеджер — Etherpad",
|
||||
"admin_plugins.version": "Верси",
|
||||
"admin_plugins_info.version_number": "Версин лоьмар",
|
||||
"admin_settings": "Нисдаран гӀирс",
|
||||
"admin_settings.current": "Карара конфигураци",
|
||||
"pad.colorpicker.save": "Ӏалашйан",
|
||||
"pad.colorpicker.cancel": "Йухайаккхар",
|
||||
"pad.loading": "Чуйолуш…",
|
||||
"pad.permissionDenied": "Хьан бакъонаш йац тӀекхача",
|
||||
"pad.settings.padSettings": "Документан нисдаран гӀирс",
|
||||
"pad.settings.myView": "Сан васт",
|
||||
"pad.settings.stickychat": "Гуттара а гайта чат",
|
||||
"pad.settings.language": "Мотт:",
|
||||
"pad.settings.about": "Проектах лаьцна",
|
||||
"pad.importExport.importSuccessful": "Кхиамца!",
|
||||
"pad.modals.cancel": "Йухайаккхар",
|
||||
"pad.share.link": "Хьажорг",
|
||||
"pad.chat": "Чат",
|
||||
"timeslider.toolbar.authors": "Авторш:",
|
||||
"timeslider.toolbar.exportlink.title": "Экспорт",
|
||||
"timeslider.month.january": "январь",
|
||||
"timeslider.month.february": "февраль",
|
||||
"timeslider.month.march": "март",
|
||||
"timeslider.month.april": "апрель",
|
||||
"timeslider.month.may": "май",
|
||||
"timeslider.month.june": "июнь",
|
||||
"timeslider.month.july": "июль",
|
||||
"timeslider.month.august": "август",
|
||||
"timeslider.month.september": "сентябрь",
|
||||
"timeslider.month.october": "октябрь",
|
||||
"timeslider.month.november": "ноябрь",
|
||||
"timeslider.month.december": "декабрь",
|
||||
"pad.impexp.importing": "Импорт йар..."
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
"Predatorix",
|
||||
"SamTV",
|
||||
"Sebastian Wallroth",
|
||||
"Ssgl",
|
||||
"Thargon",
|
||||
"Tim.krieger",
|
||||
"Wikinaut",
|
||||
|
@ -85,6 +86,8 @@
|
|||
"pad.settings.fontType": "Schriftart:",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Sprache:",
|
||||
"pad.settings.deletePad": "Pad löschen",
|
||||
"pad.delete.confirm": "Möchtest du dieses Pad wirklich löschen?",
|
||||
"pad.settings.about": "Über",
|
||||
"pad.settings.poweredBy": "Betrieben von",
|
||||
"pad.importExport.import_export": "Import/Export",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"authors": [
|
||||
"1917 Ekim Devrimi",
|
||||
"Erdemaslancan",
|
||||
"GolyatGeri",
|
||||
"Gorizon",
|
||||
"Gırd",
|
||||
"Kumkumuk",
|
||||
|
@ -78,6 +79,8 @@
|
|||
"pad.settings.fontType": "Babeta nuşti:",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Zıwan:",
|
||||
"pad.settings.deletePad": "Defteri bıesterê",
|
||||
"pad.delete.confirm": "Şıma raşti wazenê ke nê defteri bıesterên?",
|
||||
"pad.settings.about": "Heqa",
|
||||
"pad.settings.poweredBy": "Pheştidayoğ",
|
||||
"pad.importExport.import_export": "Zerredayış/Teberdayış",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"pad.settings.fontType": "Pismowa družyna:",
|
||||
"pad.settings.fontType.normal": "Normalny",
|
||||
"pad.settings.language": "Rěc:",
|
||||
"pad.settings.deletePad": "Zapisnik lašowaś",
|
||||
"pad.delete.confirm": "Cośo napšawdu toś ten zapisnik lašowaś?",
|
||||
"pad.settings.about": "Wó",
|
||||
"pad.settings.poweredBy": "Pódpěrany wót",
|
||||
"pad.importExport.import_export": "Import/Eksport",
|
||||
|
|
|
@ -14,10 +14,12 @@
|
|||
"admin_plugins": "Διαχειριστής πρόσθετων",
|
||||
"admin_plugins.available": "Διαθέσιμα πρόσθετα",
|
||||
"admin_plugins.available_not-found": "Δεν βρέθηκαν πρόσθετα.",
|
||||
"admin_plugins.available_fetching": "Ανακτάται...",
|
||||
"admin_plugins.available_install.value": "Εγκατάσταση",
|
||||
"admin_plugins.available_search.placeholder": "Αναζητήστε πρόσθετα για εγκατάσταση",
|
||||
"admin_plugins.description": "Περιγραφή",
|
||||
"admin_plugins.installed": "Εγκατεστημένα πρόσθετα",
|
||||
"admin_plugins.installed_fetching": "Ανάκτηση εγκατεστημένων προσθηκών…",
|
||||
"admin_plugins.installed_nothing": "Δεν έχετε εγκαταστήσει πρόσθετα ακόμη.",
|
||||
"admin_plugins.installed_uninstall.value": "Απεγκατάσταση",
|
||||
"admin_plugins.last-update": "Τελευταία ενημέρωση",
|
||||
|
@ -75,6 +77,8 @@
|
|||
"pad.settings.fontType": "Τύπος γραμματοσειράς:",
|
||||
"pad.settings.fontType.normal": "Κανονική",
|
||||
"pad.settings.language": "Γλώσσα:",
|
||||
"pad.settings.deletePad": "Διαγραφή Pad",
|
||||
"pad.delete.confirm": "Θέλετε πραγματικά να διαγράψετε αυτό το pad;",
|
||||
"pad.settings.about": "Σχετικά",
|
||||
"pad.settings.poweredBy": "Υποστηρίζεται από",
|
||||
"pad.importExport.import_export": "Εισαγωγή/Εξαγωγή",
|
||||
|
@ -89,7 +93,7 @@
|
|||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Μπορείτε να εισάγετε απλό κείμενο ή HTML. Για προηγμένες δυνατότητες εισαγωγής παρακαλούμε <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">εγκαταστήστε το AbiWord ή το LibreOffice</a>.",
|
||||
"pad.modals.connected": "Συνδεμένοι.",
|
||||
"pad.modals.reconnecting": "Επανασύνδεση στο pad σας...",
|
||||
"pad.modals.reconnecting": "Επανασύνδεση στο pad σας…",
|
||||
"pad.modals.forcereconnect": "Επιβολή επανασύνδεσης",
|
||||
"pad.modals.reconnecttimer": "Προσπάθεια επανασύνδεσης σε",
|
||||
"pad.modals.cancel": "Ακύρωση",
|
||||
|
@ -111,7 +115,9 @@
|
|||
"pad.modals.corruptPad.cause": "Αυτό μπορεί να οφείλεται σε ένα λάθος στη ρύθμιση του διακομιστή ή κάποια άλλη απρόβλεπτη συμπεριφορά. Παρακαλώ επικοινωνήστε με τον διαχειριστή της υπηρεσίας.",
|
||||
"pad.modals.deleted": "Διεγράφη.",
|
||||
"pad.modals.deleted.explanation": "Αυτό το pad έχει καταργηθεί.",
|
||||
"pad.modals.rateLimited.explanation": "Στείλατε πάρα πολλά μηνύματα σε αυτό το pad, επομένως σας αποσύνδεσε.",
|
||||
"pad.modals.rejected.explanation": "Ο διακομιστής απέρριψε ένα μήνυμα που στάλθηκε από το πρόγραμμα περιήγησής σας.",
|
||||
"pad.modals.rejected.cause": "Ο διακομιστής μπορεί να έχει ενημερωθεί ενώ προβάλλατε το pad ή ίσως υπάρχει σφάλμα στο Etherpad. Δοκιμάστε να φορτώσετε ξανά τη σελίδα.",
|
||||
"pad.modals.disconnected": "Είστε αποσυνδεδεμένοι.",
|
||||
"pad.modals.disconnected.explanation": "Χάθηκε η σύνδεση με τον διακομιστή",
|
||||
"pad.modals.disconnected.cause": "Ο διακομιστής μπορεί να μην είναι διαθέσιμος. Παρακαλούμε ειδοποιήστε τον διαχειριστή της υπηρεσίας εάν εξακολουθεί να συμβαίνει αυτό.",
|
||||
|
@ -124,6 +130,7 @@
|
|||
"pad.chat.loadmessages": "Φόρτωση περισσότερων μηνυμάτων",
|
||||
"pad.chat.stick.title": "Κρατήστε τη συνομιλία στην οθόνη",
|
||||
"pad.chat.writeMessage.placeholder": "Γράψτε το μήνυμα σας εδώ",
|
||||
"timeslider.followContents": "Ακολουθήστε τις ενημερώσεις περιεχομένου του pad",
|
||||
"timeslider.pageTitle": "{{appTitle}} Χρονοδιάγραμμα",
|
||||
"timeslider.toolbar.returnbutton": "Επιστροφή στο pad",
|
||||
"timeslider.toolbar.authors": "Συντάκτες:",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"pad.settings.fontType": "Font type:",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Language:",
|
||||
"pad.settings.deletePad": "Delete Pad",
|
||||
"pad.delete.confirm": "Do you really want to delete this pad?",
|
||||
"pad.settings.about": "About",
|
||||
"pad.settings.poweredBy": "Powered by",
|
||||
|
||||
|
|
|
@ -87,6 +87,8 @@
|
|||
"pad.settings.fontType": "Fonttityyppi:",
|
||||
"pad.settings.fontType.normal": "normaali",
|
||||
"pad.settings.language": "Kieli:",
|
||||
"pad.settings.deletePad": "Poista muistio",
|
||||
"pad.delete.confirm": "Haluatko todella poistaa tämän muistion?",
|
||||
"pad.settings.about": "Tietoja",
|
||||
"pad.settings.poweredBy": "Palvelun mahdollistaa",
|
||||
"pad.importExport.import_export": "Tuonti/vienti",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"Jean-Frédéric",
|
||||
"Leviathan",
|
||||
"Macofe",
|
||||
"Mahabarata",
|
||||
"Maxim21",
|
||||
"McDutchie",
|
||||
"Metroitendo",
|
||||
|
@ -21,6 +22,7 @@
|
|||
"Peter17",
|
||||
"Quenenni",
|
||||
"Rastus Vernon",
|
||||
"Spf",
|
||||
"Stephane Cottin",
|
||||
"Thibaut120094",
|
||||
"Tux-tn",
|
||||
|
@ -97,6 +99,8 @@
|
|||
"pad.settings.fontType": "Type de police :",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Langue :",
|
||||
"pad.settings.deletePad": "Supprimer le bloc-notes",
|
||||
"pad.delete.confirm": "Voulez-vous vraiment supprimer ce bloc-notes ?",
|
||||
"pad.settings.about": "À propos",
|
||||
"pad.settings.poweredBy": "Propulsé par",
|
||||
"pad.importExport.import_export": "Importer/Exporter",
|
||||
|
@ -157,7 +161,7 @@
|
|||
"timeslider.toolbar.exportlink.title": "Exporter",
|
||||
"timeslider.exportCurrent": "Exporter la version actuelle sous :",
|
||||
"timeslider.version": "Version {{version}}",
|
||||
"timeslider.saved": "Enregistré le {{day}} {{month}} {{year}}",
|
||||
"timeslider.saved": "Enregistrée le {{day}} {{month}} {{year}}",
|
||||
"timeslider.playPause": "Lecture / Pause des contenus du bloc-notes",
|
||||
"timeslider.backRevision": "Reculer d’une révision dans ce bloc-notes",
|
||||
"timeslider.forwardRevision": "Avancer d’une révision dans ce bloc-notes",
|
||||
|
|
|
@ -42,9 +42,9 @@
|
|||
"index.newPad": "Novo documento",
|
||||
"index.createOpenPad": "ou crea/abre un documento co nome:",
|
||||
"index.openPad": "abrir un Pad existente co nome:",
|
||||
"pad.toolbar.bold.title": "Resaltado (Ctrl-B)",
|
||||
"pad.toolbar.italic.title": "Cursiva (Ctrl-I)",
|
||||
"pad.toolbar.underline.title": "Subliñar (Ctrl-U)",
|
||||
"pad.toolbar.bold.title": "Grosa (Ctrl+B)",
|
||||
"pad.toolbar.italic.title": "Cursiva (Ctrl+I)",
|
||||
"pad.toolbar.underline.title": "Subliñar (Ctrl+U)",
|
||||
"pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)",
|
||||
"pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)",
|
||||
"pad.toolbar.ul.title": "Lista sen ordenar (Ctrl+Shift+L)",
|
||||
|
@ -74,6 +74,8 @@
|
|||
"pad.settings.fontType": "Tipo de letra:",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Lingua:",
|
||||
"pad.settings.deletePad": "Borrar o documento",
|
||||
"pad.delete.confirm": "Queres borrar este documento?",
|
||||
"pad.settings.about": "Acerca de",
|
||||
"pad.settings.poweredBy": "Grazas a",
|
||||
"pad.importExport.import_export": "Importar/Exportar",
|
||||
|
@ -88,7 +90,7 @@
|
|||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Só podes importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instala AbiWord</a>.",
|
||||
"pad.modals.connected": "Conectado.",
|
||||
"pad.modals.reconnecting": "Reconectando co seu documento...",
|
||||
"pad.modals.reconnecting": "Reconectando co teu documento...",
|
||||
"pad.modals.forcereconnect": "Forzar a reconexión",
|
||||
"pad.modals.reconnecttimer": "Intentarase reconectar en",
|
||||
"pad.modals.cancel": "Cancelar",
|
||||
|
|
|
@ -76,6 +76,8 @@
|
|||
"pad.settings.fontType": "סוג גופן:",
|
||||
"pad.settings.fontType.normal": "רגיל",
|
||||
"pad.settings.language": "שפה:",
|
||||
"pad.settings.deletePad": "מחיקת פנקס",
|
||||
"pad.delete.confirm": "למחוק את הפנקס הזה?",
|
||||
"pad.settings.about": "על אודות",
|
||||
"pad.settings.poweredBy": "מופעל על גבי",
|
||||
"pad.importExport.import_export": "יבוא/יצוא",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"pad.settings.fontType": "Pismowa družina:",
|
||||
"pad.settings.fontType.normal": "Normalny",
|
||||
"pad.settings.language": "Rěč:",
|
||||
"pad.settings.deletePad": "Zapisnik zhašeć",
|
||||
"pad.delete.confirm": "Chceće woprawdźe tutón zapisnik zhašeć?",
|
||||
"pad.settings.about": "Wo",
|
||||
"pad.settings.poweredBy": "Spěchowany wot",
|
||||
"pad.importExport.import_export": "Import/Eksport",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"pad.settings.fontType": "Typo de litteras:",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Lingua:",
|
||||
"pad.settings.deletePad": "Deler pad",
|
||||
"pad.delete.confirm": "Es tu secur de voler deler iste pad?",
|
||||
"pad.settings.about": "A proposito",
|
||||
"pad.settings.poweredBy": "Actionate per",
|
||||
"pad.importExport.import_export": "Importar/Exportar",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Akmaie Ajam",
|
||||
"Atriwidada",
|
||||
"Bennylin",
|
||||
"IvanLanin",
|
||||
|
@ -19,6 +20,7 @@
|
|||
"admin_plugins.installed": "Plugin terpasang",
|
||||
"admin_plugins.installed_fetching": "Mengambil plugin yang terpasang…",
|
||||
"admin_plugins.installed_nothing": "Anda belum memasang plugin apa pun.",
|
||||
"admin_plugins.installed_uninstall.value": "Uninstal",
|
||||
"admin_plugins.last-update": "Pembaruan terakhir",
|
||||
"admin_plugins.name": "Nama",
|
||||
"admin_plugins.page-title": "Manajer plugin - Etherpad",
|
||||
|
@ -74,6 +76,8 @@
|
|||
"pad.settings.rtlcheck": "Membaca dari kanan ke kiri?",
|
||||
"pad.settings.fontType": "Jenis fonta:",
|
||||
"pad.settings.language": "Bahasa:",
|
||||
"pad.settings.deletePad": "Hapus Pad",
|
||||
"pad.delete.confirm": "Apakah Anda benar-benar ingin menghapus pad ini?",
|
||||
"pad.settings.about": "Tentang",
|
||||
"pad.settings.poweredBy": "Ditenagai oleh",
|
||||
"pad.importExport.import_export": "Impor/Ekspor",
|
||||
|
|
|
@ -82,6 +82,8 @@
|
|||
"pad.settings.fontType": "글꼴 종류:",
|
||||
"pad.settings.fontType.normal": "보통",
|
||||
"pad.settings.language": "언어:",
|
||||
"pad.settings.deletePad": "패드 삭제",
|
||||
"pad.delete.confirm": "이 패드를 삭제하시겠습니까?",
|
||||
"pad.settings.about": "소개",
|
||||
"pad.settings.poweredBy": "제공:",
|
||||
"pad.importExport.import_export": "가져오기/내보내기",
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
"timeslider.toolbar.exportlink.title": "Exportéieren",
|
||||
"timeslider.exportCurrent": "Exportéiert déi aktuell Versioun als:",
|
||||
"timeslider.version": "Versioun {{version}}",
|
||||
"timeslider.saved": "Gespäichert de(n) {{day}} {{month}} {{year}}",
|
||||
"timeslider.saved": "Gespäichert de(n) {{day}}. {{month}} {{year}}",
|
||||
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||
"timeslider.month.january": "Januar",
|
||||
"timeslider.month.february": "Februar",
|
||||
|
|
61
src/locales/ln.json
Normal file
61
src/locales/ln.json
Normal file
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"BOKOBA VEROLY"
|
||||
]
|
||||
},
|
||||
"admin.page-title": "Admin Dashboard - Etherpad",
|
||||
"admin_plugins": "Mokambi ya plug-in",
|
||||
"admin_plugins.available": "Ba plugins oyo ezali",
|
||||
"admin_plugins.available_not-found": "Ba plugins ezwamaki te.",
|
||||
"admin_plugins.available_fetching": "Kozwa...",
|
||||
"admin_plugins.available_install.value": "Kotya",
|
||||
"admin_plugins.available_search.placeholder": "Bolukiluki ya ba plugins mpo na kotya",
|
||||
"admin_plugins.description": "Ndimbola",
|
||||
"admin_plugins.installed": "Ba plugins oyo etyamaki",
|
||||
"admin_plugins.installed_fetching": "Kozwa ba plugins oyo etyamaki...",
|
||||
"admin_plugins.installed_nothing": "Otikaki naino ba plugins te.",
|
||||
"admin_plugins.installed_uninstall.value": "Kofungola esika",
|
||||
"admin_plugins.last-update": "Makambo ya sika ya suka",
|
||||
"admin_plugins.name": "Nkombo na yango",
|
||||
"admin_plugins.page-title": "Gestionnaire de greffons — Etherpad",
|
||||
"admin_plugins.version": "Libongoli",
|
||||
"admin_plugins_info": "Informations de résolution de problème",
|
||||
"admin_plugins_info.hooks": "Crochets installés",
|
||||
"admin_plugins_info.hooks_client": "Crochets côté client",
|
||||
"admin_plugins_info.hooks_server": "Crochets côté serveur",
|
||||
"admin_plugins_info.parts": "Biteni oyo batye",
|
||||
"admin_plugins_info.plugins": "Ba plugins oyo etyamaki",
|
||||
"admin_plugins_info.page-title": "Makambo etali ordinatɛrɛ - Etherpad",
|
||||
"admin_plugins_info.version": "Libongoli ya Etherpad",
|
||||
"admin_plugins_info.version_latest": "Libongoli ya sika",
|
||||
"admin_plugins_info.version_number": "Numero ya version",
|
||||
"admin_settings": "Ndenge ya kobongisa yango",
|
||||
"admin_settings.current": "Configuration ya lelo",
|
||||
"admin_settings.current_example-devel": "Ndakisa modèle ya paramètres ya développement",
|
||||
"admin_settings.current_example-prod": "Ndakisa modèle ya paramètres ya production",
|
||||
"admin_settings.current_restart.value": "Bobandi lisusu Etherpad",
|
||||
"admin_settings.current_save.value": "Bomba ba Paramètres",
|
||||
"admin_settings.page-title": "Paramètres - Etherpad ya kosala",
|
||||
"index.newPad": "Pad ya sika",
|
||||
"index.createOpenPad": "to kosala/kofungola Pad na nkombo:",
|
||||
"index.openPad": "kofungola Pad oyo ezali na nkombo:",
|
||||
"pad.toolbar.bold.title": "Makomi ya moindo makasi (Ctrl+B)",
|
||||
"pad.toolbar.underline.title": "Mokanda ya nse (Ctrl+U)",
|
||||
"pad.toolbar.strikethrough.title": "Strikethrough (Ctrl+5)",
|
||||
"pad.toolbar.ol.title": "Liste oyo esɛngami (Ctrl+Shift+N)",
|
||||
"pad.toolbar.ul.title": "Liste oyo etyami na molongo te (Ctrl+Shift+L)",
|
||||
"pad.toolbar.indent.title": "Indent (TAB)",
|
||||
"pad.toolbar.unindent.title": "Mikuwa ya libándá (Shift+TAB)",
|
||||
"pad.toolbar.undo.title": "Undo (Ctrl+Z)",
|
||||
"pad.toolbar.redo.title": "Redo (Ctrl+Y)",
|
||||
"pad.toolbar.clearAuthorship.title": "Langi ya polele ya mokomi (Ctrl+Shift+C)",
|
||||
"pad.toolbar.import_export.title": "Kokotisa/kobimisa na/na ba formats ya ba fichiers ndenge na ndenge",
|
||||
"pad.toolbar.timeslider.title": "Mokambi ya ntango",
|
||||
"pad.toolbar.savedRevision.title": "Kobomba lisusu",
|
||||
"pad.toolbar.settings.title": "Ndenge ya kobongisa yango",
|
||||
"pad.toolbar.embed.title": "Kopesa mpe kobakisa yango",
|
||||
"pad.toolbar.showusers.title": "Tyá bato oyo basalelaka yango",
|
||||
"pad.colorpicker.save": "Kobikisa",
|
||||
"pad.colorpicker.cancel": "Kolongola"
|
||||
}
|
|
@ -63,7 +63,7 @@
|
|||
"pad.colorpicker.save": "Išsaugoti",
|
||||
"pad.colorpicker.cancel": "Atšaukti",
|
||||
"pad.loading": "Įkraunama...",
|
||||
"pad.noCookie": "Slapuko nepavyko rasti. Prašome leisti slapukus interneto naršyklėje!",
|
||||
"pad.noCookie": "Slapuko rasti nepavyko. Prašome leisti slapukus savo naršyklėje! Jūsų sesija ir nustatymai nebus išsaugoti tarp apsilankymų. Taip gali būti dėl to, kad kai kuriose naršyklėse Etherpad yra įtrauktas į iFrame. Įsitikinkite, kad Etherpad yra tame pačiame padomenyje / domene kaip ir pirminis iFrame.",
|
||||
"pad.permissionDenied": "Jūs neturite leidimo patekti į šį bloknotą",
|
||||
"pad.settings.padSettings": "Bloknoto nustatymai",
|
||||
"pad.settings.myView": "Mano Vaizdas",
|
||||
|
@ -75,6 +75,8 @@
|
|||
"pad.settings.fontType": "Šrifto tipas:",
|
||||
"pad.settings.fontType.normal": "Normalus",
|
||||
"pad.settings.language": "Kalba:",
|
||||
"pad.settings.deletePad": "Ištrinti bloką",
|
||||
"pad.delete.confirm": "Ar tikrai norite ištrinti šį bloką?",
|
||||
"pad.settings.about": "Apie",
|
||||
"pad.settings.poweredBy": "Palaiko",
|
||||
"pad.importExport.import_export": "Importuoti/Eksportuoti",
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
"pad.settings.fontType": "Тип на фонт:",
|
||||
"pad.settings.fontType.normal": "Нормален",
|
||||
"pad.settings.language": "Јазик:",
|
||||
"pad.settings.deletePad": "Избриши тетратка",
|
||||
"pad.delete.confirm": "Дали навистина сакате да ја избришете тетраткава?",
|
||||
"pad.settings.about": "За додатоков",
|
||||
"pad.settings.poweredBy": "Овозможено од",
|
||||
"pad.importExport.import_export": "Увоз/Извоз",
|
||||
|
|
|
@ -85,6 +85,8 @@
|
|||
"pad.settings.fontType": "Lettertype:",
|
||||
"pad.settings.fontType.normal": "Normaal",
|
||||
"pad.settings.language": "Taal:",
|
||||
"pad.settings.deletePad": "Pad verwijderen",
|
||||
"pad.delete.confirm": "Wilt u dit pad echt verwijderen?",
|
||||
"pad.settings.about": "Over",
|
||||
"pad.settings.poweredBy": "Aangedreven door",
|
||||
"pad.importExport.import_export": "Importeren/exporteren",
|
||||
|
|
|
@ -9,6 +9,16 @@
|
|||
"ਪ੍ਰਚਾਰਕ"
|
||||
]
|
||||
},
|
||||
"admin_plugins.available_fetching": "ਲਿਆਉਣਾ ਪਿਆਂ...",
|
||||
"admin_plugins.available_install.value": "ਜੜੋ",
|
||||
"admin_plugins.description": "ਵੇਰਵਾ",
|
||||
"admin_plugins.last-update": "ਆਖਰੀ ਵਾਰ ਨਵਿਆਈਆ ਗਿਆ",
|
||||
"admin_plugins.name": "ਨਾਂ",
|
||||
"admin_plugins_info": "ਸਮੱਸਿਆ ਨਿਵਾਰਣ ਜਾਣਕਾਰੀ",
|
||||
"admin_settings": "ਤਰਜੀਹਾਂ",
|
||||
"admin_settings.current": "ਮੌਜੂਦਾ ਬਣਤਰ",
|
||||
"admin_settings.current_save.value": "ਤਰਜੀਹਾਂ ਸੰਭਾਲੋ",
|
||||
"admin_settings.page-title": "ਤਰਜੀਹਾਂ - ਈਥਰਪੈਡ",
|
||||
"index.newPad": "ਨਵਾਂ ਪੈਡ",
|
||||
"index.createOpenPad": "ਜਾਂ ਨਾਂ ਨਾਲ ਨਵਾਂ ਪੈਡ ਬਣਾਓ/ਖੋਲ੍ਹੋ:",
|
||||
"pad.toolbar.bold.title": "ਗੂੜ੍ਹਾ (Ctrl-B)",
|
||||
|
@ -25,7 +35,7 @@
|
|||
"pad.toolbar.import_export.title": "ਵੱਖ-ਵੱਖ ਫਾਇਲ ਫਾਰਮੈਟ ਤੋਂ/ਵਿੱਚ ਇੰਪੋਰਟ/ਐਕਸਪੋਰਟ ਕਰੋ",
|
||||
"pad.toolbar.timeslider.title": "ਸਮਾਂ-ਲਕੀਰ",
|
||||
"pad.toolbar.savedRevision.title": "ਦੁਹਰਾਅ ਸਾਂਭੋ",
|
||||
"pad.toolbar.settings.title": "ਸੈਟਿੰਗ",
|
||||
"pad.toolbar.settings.title": "ਪਸੰਦਾਂ",
|
||||
"pad.toolbar.embed.title": "ਇਹ ਪੈਡ ਸਾਂਝਾ ਤੇ ਇੰਬੈੱਡ ਕਰੋ",
|
||||
"pad.toolbar.showusers.title": "ਇਸ ਫੱਟੀ ਉੱਤੇ ਵਰਤੋਂਕਾਰ ਵਿਖਾਓ",
|
||||
"pad.colorpicker.save": "ਸੰਭਾਲੋ",
|
||||
|
@ -40,10 +50,11 @@
|
|||
"pad.settings.colorcheck": "ਲੇਖਕੀ ਰੰਗ",
|
||||
"pad.settings.linenocheck": "ਲਕੀਰ ਨੰਬਰ",
|
||||
"pad.settings.rtlcheck": "ਸਮੱਗਰੀ ਸੱਜੇ ਤੋਂ ਖੱਬੇ ਪੜ੍ਹਨੀ ਹੈ?",
|
||||
"pad.settings.fontType": "ਫੋਂਟ ਕਿਸਮ:",
|
||||
"pad.settings.fontType.normal": "ਸਧਾਰਨ",
|
||||
"pad.settings.fontType": "ਅੱਖਰ ਦੀ ਕਿਸਮ:",
|
||||
"pad.settings.fontType.normal": "ਆਮ",
|
||||
"pad.settings.language": "ਭਾਸ਼ਾ:",
|
||||
"pad.importExport.import_export": "ਇੰਪੋਰਟ/ਐਕਸਪੋਰਟ",
|
||||
"pad.settings.about": "ਬਾਬਤ",
|
||||
"pad.importExport.import_export": "ਦਰਾਮਦ/ਬਰਾਮਦ",
|
||||
"pad.importExport.import": "ਕੋਈ ਵੀ ਟੈਕਸਟ ਫਾਇਲ ਜਾਂ ਦਸਤਾਵੇਜ਼ ਅੱਪਲੋਡ ਕਰੋ",
|
||||
"pad.importExport.importSuccessful": "ਸਫ਼ਲ!",
|
||||
"pad.importExport.export": "ਮੌਜੂਦਾ ਪੈਡ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰੋ:",
|
||||
|
@ -57,6 +68,7 @@
|
|||
"pad.modals.connected": "ਕੁਨੈਕਟ ਹੈ।",
|
||||
"pad.modals.reconnecting": "..ਤੁਹਾਡੇ ਪੈਡ ਨਾਲ ਮੁੜ-ਕੁਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ",
|
||||
"pad.modals.forcereconnect": "ਧੱਕੇ ਨਾਲ ਮੁੜ-ਕੁਨੈਕਟ ਕਰੋ",
|
||||
"pad.modals.cancel": "ਰੱਦ ਕਰੋ",
|
||||
"pad.modals.userdup": "ਹੋਰ ਵਿੰਡੋ ਵਿੱਚ ਖੁੱਲ੍ਹਿਆ ਹੈ",
|
||||
"pad.modals.userdup.explanation": "ਇਹ ਪੈਡ ਇਸ ਕੰਪਿਊਟਰ 'ਤੇ ਇੱਕ ਤੋਂ ਵੱਧ ਫਰੋਲੂ ਬਾਰੀ ਵਿੱਚ ਖੁੱਲ੍ਹਿਆ ਜਾਪਦਾ ਹੈ।",
|
||||
"pad.modals.userdup.advice": "ਸਗੋਂ ਇਹ ਬਾਰੀ ਵਰਤਣ ਵਾਸਤੇ ਮੁੜ ਜੁੜੋ।",
|
||||
|
@ -80,11 +92,12 @@
|
|||
"pad.modals.disconnected.cause": "ਸਰਵਰ ਨਾਮੌਜੂਦ ਹੋ ਸਕਦਾ ਹੈ। ਜੇਕਰ ਇਹ ਹੁੰਦਾ ਰਹੇ ਤਾਂ ਮਿਹਰਬਾਨੀ ਕਰਕੇ ਸੇਵਾ ਪ੍ਰਬੰਧਕ ਨੂੰ ਖ਼ਬਰ ਕਰੋ।",
|
||||
"pad.share": "ਇਹ ਪੈਡ ਸਾਂਝਾ ਕਰੋ",
|
||||
"pad.share.readonly": "ਕੇਵਲ ਪੜ੍ਹਨ ਲਈ",
|
||||
"pad.share.link": "ਲਿੰਕ",
|
||||
"pad.share.link": "ਕੜੀ",
|
||||
"pad.share.emebdcode": "ਇੰਬੈੱਡ URL",
|
||||
"pad.chat": "ਗੱਲਬਾਤ",
|
||||
"pad.chat.title": "ਇਹ ਪੈਡ ਲਈ ਗੱਲਬਾਤ ਖੋਲ੍ਹੋ।",
|
||||
"pad.chat.loadmessages": "ਹੋਰ ਸੁਨੇਹੇ ਲੋਡ ਕਰੋ",
|
||||
"pad.chat.loadmessages": "ਹੋਰ ਸੁਨੇਹੇ ਲੱਦੋ",
|
||||
"pad.chat.writeMessage.placeholder": "ਆਪਣਾ ਸੁਨੇਹਾ ਇੱਥੇ ਲਿਖੋ",
|
||||
"timeslider.pageTitle": "{{appTitle}} ਸਮਾਂ-ਲਕੀਰ",
|
||||
"timeslider.toolbar.returnbutton": "ਪੈਡ ਉੱਤੇ ਵਾਪਸ",
|
||||
"timeslider.toolbar.authors": "ਲੇਖਕ:",
|
||||
|
@ -110,18 +123,18 @@
|
|||
"timeslider.month.november": "ਨਵੰਬਰ",
|
||||
"timeslider.month.december": "ਦਸੰਬਰ",
|
||||
"timeslider.unnamedauthors": "{{num}} ਬੇਨਾਮ {[plural(num) one: ਲੇਖਕ, other: ਲੇਖਕ ]}",
|
||||
"pad.savedrevs.marked": "ਇਹ ਰੀਵਿਜ਼ਨ ਨੂੰ ਹੁਣ ਸੰਭਾਲੇ ਹੋਏ ਰੀਵਿਜ਼ਨ ਵਜੋਂ ਮੰਨਿਆ ਗਿਆ ਹੈ",
|
||||
"pad.savedrevs.marked": "ਇਹ ਦੁਹਰਾਅ ਨੂੰ ਹੁਣ ਸੰਭਾਲੇ ਹੋਏ ਦੁਹਰਾਅ ਵਜੋਂ ਮੰਨਿਆ ਗਿਆ ਹੈ",
|
||||
"pad.savedrevs.timeslider": "ਤੁਸੀੰ ਸਾੰਭੀਆੰ ਹੋਈਆੰ ਵਰਜਨਾੰ ਸਮਾੰਸਲਾਈਡਰ ਤੇ ਜਾ ਕੇ ਵੇਖ ਸਕਦੇ ਹੋ",
|
||||
"pad.userlist.entername": "ਆਪਣਾ ਨਾਂ ਦਿਉ",
|
||||
"pad.userlist.unnamed": "ਬੇਨਾਮ",
|
||||
"pad.editbar.clearcolors": "ਪੂਰੇ ਦਸਾਤਵੇਜ਼ ਉੱਤੇ ਪਰਮਾਣਕਿਤਾ ਰੰਗ ਸਾਫ਼ ਕਰਨੇ ਹਨ?",
|
||||
"pad.impexp.importbutton": "ਹੁਣੇ ਇੰਪੋਰਟ ਕਰੋ",
|
||||
"pad.impexp.importbutton": "ਹੁਣੇ ਦਰਾਮਦ ਕਰੋ",
|
||||
"pad.impexp.importing": "...ਇੰਪੋਰਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ",
|
||||
"pad.impexp.confirmimport": "ਕੋਈ ਫ਼ਾਈਲ ਦਰਾਮਦ ਕਾਰਨ ਨਾਲ਼ ਪੈਡ ਦੀ ਮੌਜੂਦਾ ਲਿਖਤ ਉੱਤੇ ਲਿਖਿਆ ਜਾਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਸੱਚੀਂ ਇਹ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||
"pad.impexp.convertFailed": "ਅਸੀਂ ਇਸ ਫ਼ਾਈਲ ਦੀ ਦਰਾਮਦ ਨਹੀਂ ਕਰ ਸਕੇ। ਮਿਹਰਬਾਨੀ ਕਰਕੇ ਕੋਈ ਵੱਖਰੀ ਦਸਤਾਵੇਜ਼ੀ ਰੂਪ-ਰੇਖਾ ਵਰਤੋ ਜਾਂ ਹੱਥੀਂ ਨਕਲ-ਚੇਪੀ ਕਰੋ।",
|
||||
"pad.impexp.padHasData": "ਅਸੀ ਇਸ ਫਾਈਲ ਨੂੰ ਆਯਾਤ ਨਹੀੰ ਕਰ ਸਕੇ ਕਿਉੰਕਿ ਇਸ ਪੈਡ ਉੱਤੇ ਪਹਿਲਾੰ ਹੀ ਤਬਦੀਲੀਆੰ ਕੀਤੀਆੰ ਜਾ ਚੁਕੀਆੰ ਹਨ, ਕਿਰਪਾ ਕਰਕੇ ਨਵੇੰ ਪੈਡ ਵਿਚ ਆਯਾਤ ਕਰੋ",
|
||||
"pad.impexp.padHasData": "ਅਸੀ ਇਸ ਫਾਈਲ ਨੂੰ ਦਰਾਮਦ ਨਹੀੰ ਕਰ ਸਕੇ ਕਿਉੰਕਿ ਇਸ ਕਾਗਜ਼ ਉੱਤੇ ਪਹਿਲਾਂ ਹੀ ਤਬਦੀਲੀਆਂ ਕੀਤੀਆਂ ਜਾ ਚੁਕੀਆਂ ਹਨ, ਕਿਰਪਾ ਕਰਕੇ ਨਵੇਂ ਕਾਗਜ਼ ਵਿਚ ਦਰਾਮਦ ਕਰੋ",
|
||||
"pad.impexp.uploadFailed": "ਅੱਪਲੋਡ ਲਈ ਫੇਲ੍ਹ ਹੈ, ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।",
|
||||
"pad.impexp.importfailed": "ਇੰਪੋਰਟ ਫੇਲ੍ਹ ਹੈ",
|
||||
"pad.impexp.importfailed": "ਦਰਾਮਦ ਨਾਕਾਮ",
|
||||
"pad.impexp.copypaste": "ਕਾਪੀ ਕਰੋ ਚੇਪੋ ਜੀ",
|
||||
"pad.impexp.exportdisabled": "{{type}} ਫਾਰਮੈਟ ਵਜੋਂ ਬਰਾਮਦ ਕਰਨਾ ਬੰਦ ਹੈ। ਵੇਰਵੇ ਵਾਸਤੇ ਆਪਣੇ ਸਿਸਟਮ ਦੇ ਪਰਬੰਧਕ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।"
|
||||
}
|
||||
|
|
|
@ -71,6 +71,8 @@
|
|||
"pad.settings.rtlcheck": "Ël contnù, dev-lo esse lesù da drita a snistra?",
|
||||
"pad.settings.fontType": "Sòrt ëd caràter:",
|
||||
"pad.settings.language": "Lenga:",
|
||||
"pad.settings.deletePad": "Eliminé ël blochet",
|
||||
"pad.delete.confirm": "Veul-lo për da bon eliminé cost blochet?",
|
||||
"pad.settings.about": "A propòsit",
|
||||
"pad.settings.poweredBy": "Potensià da",
|
||||
"pad.importExport.import_export": "Amporté/Esporté",
|
||||
|
|
|
@ -23,22 +23,22 @@
|
|||
]
|
||||
},
|
||||
"admin.page-title": "Painel administrativo - Etherpad",
|
||||
"admin_plugins": "Gerente de complementos",
|
||||
"admin_plugins": "Gerenciador de complementos",
|
||||
"admin_plugins.available": "Plugins disponíveis",
|
||||
"admin_plugins.available_not-found": "Nenhuma complementos encontrado.",
|
||||
"admin_plugins.available_not-found": "Nenhum plugin encontrado.",
|
||||
"admin_plugins.available_fetching": "Buscando…",
|
||||
"admin_plugins.available_install.value": "Instalar",
|
||||
"admin_plugins.available_search.placeholder": "Procure por plug-ins para instalar",
|
||||
"admin_plugins.available_search.placeholder": "Procure plugins para instalar",
|
||||
"admin_plugins.description": "Descrição",
|
||||
"admin_plugins.installed": "Complementos instalados",
|
||||
"admin_plugins.installed_fetching": "Buscando plug-ins instalados…",
|
||||
"admin_plugins.installed_nothing": "Você ainda não instalou nenhum plug-in.",
|
||||
"admin_plugins.installed": "Plugins instalados",
|
||||
"admin_plugins.installed_fetching": "Buscando plugins instalados…",
|
||||
"admin_plugins.installed_nothing": "Você ainda não instalou nenhum plugin.",
|
||||
"admin_plugins.installed_uninstall.value": "Desinstalar",
|
||||
"admin_plugins.last-update": "Última atualização",
|
||||
"admin_plugins.name": "Nome",
|
||||
"admin_plugins.page-title": "Gerenciador de plug-ins - Etherpad",
|
||||
"admin_plugins.page-title": "Gerenciador de plugins - Etherpad",
|
||||
"admin_plugins.version": "Versão",
|
||||
"admin_plugins_info": "Informações sobre solução",
|
||||
"admin_plugins_info": "Resolução de problemas",
|
||||
"admin_plugins_info.hooks": "Ganchos instalados",
|
||||
"admin_plugins_info.hooks_client": "Ganchos do lado do cliente",
|
||||
"admin_plugins_info.hooks_server": "Ganchos do lado do servidor",
|
||||
|
@ -90,6 +90,8 @@
|
|||
"pad.settings.fontType": "Tipo de fonte:",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Idioma:",
|
||||
"pad.settings.deletePad": "Apagar Pad",
|
||||
"pad.delete.confirm": "Tem certeza de que quer deletar este pad?",
|
||||
"pad.settings.about": "Sobre",
|
||||
"pad.settings.poweredBy": "Fornecido por",
|
||||
"pad.importExport.import_export": "Importar/Exportar",
|
||||
|
@ -127,7 +129,7 @@
|
|||
"pad.modals.deleted": "Excluído.",
|
||||
"pad.modals.deleted.explanation": "Esta nota foi removida.",
|
||||
"pad.modals.rateLimited": "Limitado.",
|
||||
"pad.modals.rateLimited.explanation": "Você enviou muitas mensagens para este pad por isso será desconectado.",
|
||||
"pad.modals.rateLimited.explanation": "Você enviou muitas mensagens para esta nota por isso será desconectado.",
|
||||
"pad.modals.rejected.explanation": "O servidor rejeitou uma mensagem que foi enviada pelo seu navegador.",
|
||||
"pad.modals.rejected.cause": "O server pode ter sido atualizado enquanto visualizava esta nota, ou talvez seja apenas um bug do Etherpad. Tenta recarregar a página.",
|
||||
"pad.modals.disconnected": "Você foi desconectado.",
|
||||
|
@ -142,7 +144,7 @@
|
|||
"pad.chat.loadmessages": "Carregar mais mensagens",
|
||||
"pad.chat.stick.title": "Cole o bate-papo na tela",
|
||||
"pad.chat.writeMessage.placeholder": "Escreva sua mensagem aqui",
|
||||
"timeslider.followContents": "Siga as atualizações de conteúdo do pad",
|
||||
"timeslider.followContents": "Siga as atualizações de conteúdo da nota",
|
||||
"timeslider.pageTitle": "Linha do tempo de {{appTitle}}",
|
||||
"timeslider.toolbar.returnbutton": "Retornar para a nota",
|
||||
"timeslider.toolbar.authors": "Autores:",
|
||||
|
@ -151,9 +153,9 @@
|
|||
"timeslider.exportCurrent": "Exportar a versão atual em formato:",
|
||||
"timeslider.version": "Versão {{version}}",
|
||||
"timeslider.saved": "Salvo em {{day}} de {{month}} de {{year}}",
|
||||
"timeslider.playPause": "Reproduzir / Pausar conteúdo no Pad",
|
||||
"timeslider.backRevision": "Voltar a uma revisão anterior neste Pad",
|
||||
"timeslider.forwardRevision": "Ir a uma revisão posterior neste Pad",
|
||||
"timeslider.playPause": "Reproduzir / Pausar conteúdos da Nota",
|
||||
"timeslider.backRevision": "Voltar a uma revisão anterior nesta Nota",
|
||||
"timeslider.forwardRevision": "Ir a uma revisão posterior nesta Nota",
|
||||
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
|
||||
"timeslider.month.january": "Janeiro",
|
||||
"timeslider.month.february": "Fevereiro",
|
||||
|
|
|
@ -87,6 +87,8 @@
|
|||
"pad.settings.fontType": "Тип шрифта:",
|
||||
"pad.settings.fontType.normal": "Обычный",
|
||||
"pad.settings.language": "Язык:",
|
||||
"pad.settings.deletePad": "Удалить документ",
|
||||
"pad.delete.confirm": "Вы действительно хотите удалить этот документ?",
|
||||
"pad.settings.about": "О проекте",
|
||||
"pad.settings.poweredBy": "Проект основан на",
|
||||
"pad.importExport.import_export": "Импорт/экспорт",
|
||||
|
|
|
@ -77,6 +77,8 @@
|
|||
"pad.settings.fontType": "Vrsta pisave:",
|
||||
"pad.settings.fontType.normal": "Normalno",
|
||||
"pad.settings.language": "Jezik:",
|
||||
"pad.settings.deletePad": "Izbriši ploščico",
|
||||
"pad.delete.confirm": "Res želite izbrisati to ploščico?",
|
||||
"pad.settings.about": "Kolofon",
|
||||
"pad.settings.poweredBy": "Omogoča",
|
||||
"pad.importExport.import_export": "Uvoz/Izvoz",
|
||||
|
|
|
@ -76,6 +76,8 @@
|
|||
"pad.settings.fontType": "Typsnitt:",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Språk:",
|
||||
"pad.settings.deletePad": "Radera block",
|
||||
"pad.delete.confirm": "Vill du verkligen radera detta block?",
|
||||
"pad.settings.about": "Om",
|
||||
"pad.settings.poweredBy": "Drivs av",
|
||||
"pad.importExport.import_export": "Importera/Exportera",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"authors": [
|
||||
"Aefgh39622",
|
||||
"Andibecker",
|
||||
"Ekminarin",
|
||||
"Patsagorn Y.",
|
||||
"Trisorn Triboon"
|
||||
]
|
||||
|
@ -121,7 +122,7 @@
|
|||
"pad.share.readonly": "อ่านเท่านั้น",
|
||||
"pad.share.link": "ลิงก์",
|
||||
"pad.share.emebdcode": "URL แบบฝังตัว",
|
||||
"pad.chat": "แชท",
|
||||
"pad.chat": "แชต",
|
||||
"pad.chat.title": "เปิดการแชทสำหรับแผ่นจดบันทึกนี้",
|
||||
"pad.chat.loadmessages": "โหลดข้อความเพิ่มเติม",
|
||||
"pad.chat.stick.title": "ปักการสนทนาไว้บนหน้าจอ",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"Shangkuanlc",
|
||||
"Shizhao",
|
||||
"Stang",
|
||||
"TFX202X",
|
||||
"VulpesVulpes825",
|
||||
"Yfdyh000",
|
||||
"乌拉跨氪",
|
||||
|
@ -92,6 +93,8 @@
|
|||
"pad.settings.fontType": "字体类型:",
|
||||
"pad.settings.fontType.normal": "正常",
|
||||
"pad.settings.language": "语言:",
|
||||
"pad.settings.deletePad": "删除记事本",
|
||||
"pad.delete.confirm": "您确定要删除此记事本吗?",
|
||||
"pad.settings.about": "关于",
|
||||
"pad.settings.poweredBy": "技术支持来自",
|
||||
"pad.importExport.import_export": "导入/导出",
|
||||
|
|
|
@ -82,6 +82,8 @@
|
|||
"pad.settings.fontType": "字型類型:",
|
||||
"pad.settings.fontType.normal": "正常",
|
||||
"pad.settings.language": "語言:",
|
||||
"pad.settings.deletePad": "刪除記事本",
|
||||
"pad.delete.confirm": "您確定要刪除此記事本?",
|
||||
"pad.settings.about": "關於",
|
||||
"pad.settings.poweredBy": "技術支援來自",
|
||||
"pad.importExport.import_export": "導入/匯出",
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
import {deserializeOps} from '../../static/js/Changeset';
|
||||
import ChatMessage from '../../static/js/ChatMessage';
|
||||
import {Builder} from "../../static/js/Builder";
|
||||
import {Attribute} from "../../static/js/types/Attribute";
|
||||
const CustomError = require('../utils/customError');
|
||||
const padManager = require('./PadManager');
|
||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||
|
@ -563,11 +565,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
|||
const oldText = pad.text();
|
||||
atext.text += '\n';
|
||||
|
||||
const eachAttribRun = (attribs: string[], func:Function) => {
|
||||
const eachAttribRun = (attribs: string, func:Function) => {
|
||||
let textIndex = 0;
|
||||
const newTextStart = 0;
|
||||
const newTextEnd = atext.text.length;
|
||||
for (const op of Changeset.deserializeOps(attribs)) {
|
||||
for (const op of deserializeOps(attribs)) {
|
||||
const nextIndex = textIndex + op.chars;
|
||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
||||
|
@ -577,10 +579,10 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
|||
};
|
||||
|
||||
// create a new changeset with a helper builder object
|
||||
const builder = Changeset.builder(oldText.length);
|
||||
const builder = new Builder(oldText.length);
|
||||
|
||||
// assemble each line into the builder
|
||||
eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => {
|
||||
eachAttribRun(atext.attribs, (start: number, end: number, attribs:Attribute[]) => {
|
||||
builder.insert(atext.text.substring(start, end), attribs);
|
||||
});
|
||||
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
|
||||
const db = require('./DB');
|
||||
const CustomError = require('../utils/customError');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
import padutils, {randomString} from "../../static/js/pad_utils";
|
||||
|
||||
exports.getColorPalette = () => [
|
||||
'#ffc7c7',
|
||||
|
@ -169,7 +169,7 @@ exports.getAuthorId = async (token: string, user: object) => {
|
|||
* @param {String} token The token
|
||||
*/
|
||||
exports.getAuthor4Token = async (token: string) => {
|
||||
warnDeprecated(
|
||||
padutils.warnDeprecated(
|
||||
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
||||
return await getAuthor4Token(token);
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const randomString = require('../../static/js/pad_utils').randomString;
|
||||
import {randomString} from "../../static/js/pad_utils";
|
||||
const db = require('./DB');
|
||||
const padManager = require('./PadManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
|
|
|
@ -7,10 +7,10 @@ import {MapArrayType} from "../types/MapType";
|
|||
* The pad object, defined with joose
|
||||
*/
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
import {applyToAText, checkRep, copyAText, deserializeOps, makeAText, makeSplice, opsFromAText, pack, unpack} from '../../static/js/Changeset';
|
||||
import ChatMessage from '../../static/js/ChatMessage';
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
const Stream = require('../utils/Stream');
|
||||
const assert = require('assert').strict;
|
||||
const db = require('./DB');
|
||||
|
@ -23,8 +23,10 @@ const CustomError = require('../utils/customError');
|
|||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const randomString = require('../utils/randomstring');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
const promises = require('../utils/promises');
|
||||
import pad_utils from "../../static/js/pad_utils";
|
||||
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
|
||||
import {} from '../utils/promises';
|
||||
import {timesLimit} from "async";
|
||||
|
||||
/**
|
||||
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
||||
|
@ -40,7 +42,7 @@ exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
|
|||
class Pad {
|
||||
private db: Database;
|
||||
private atext: AText;
|
||||
private pool: APool;
|
||||
private pool: AttributePool;
|
||||
private head: number;
|
||||
private chatHead: number;
|
||||
private publicStatus: boolean;
|
||||
|
@ -56,7 +58,7 @@ class Pad {
|
|||
*/
|
||||
constructor(id:string, database = db) {
|
||||
this.db = database;
|
||||
this.atext = Changeset.makeAText('\n');
|
||||
this.atext = makeAText('\n');
|
||||
this.pool = new AttributePool();
|
||||
this.head = -1;
|
||||
this.chatHead = -1;
|
||||
|
@ -93,13 +95,13 @@ class Pad {
|
|||
* @param {String} authorId The id of the author
|
||||
* @return {Promise<number|string>}
|
||||
*/
|
||||
async appendRevision(aChangeset:AChangeSet, authorId = '') {
|
||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
||||
async appendRevision(aChangeset:string, authorId = '') {
|
||||
const newAText = applyToAText(aChangeset, this.atext, this.pool);
|
||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
||||
this.head !== -1) {
|
||||
return this.head;
|
||||
}
|
||||
Changeset.copyAText(newAText, this.atext);
|
||||
copyAText(newAText, this.atext);
|
||||
|
||||
const newRev = ++this.head;
|
||||
|
||||
|
@ -126,11 +128,11 @@ class Pad {
|
|||
pad: this,
|
||||
authorId,
|
||||
get author() {
|
||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
pad_utils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
return this.authorId;
|
||||
},
|
||||
set author(authorId) {
|
||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
pad_utils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
this.authorId = authorId;
|
||||
},
|
||||
...this.head === 0 ? {} : {
|
||||
|
@ -215,7 +217,7 @@ class Pad {
|
|||
]);
|
||||
const apool = this.apool();
|
||||
let atext = keyAText;
|
||||
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
|
||||
for (const cs of changesets) atext = applyToAText(cs, atext, apool);
|
||||
return atext;
|
||||
}
|
||||
|
||||
|
@ -293,7 +295,7 @@ class Pad {
|
|||
(!ins && start > 0 && orig[start - 1] === '\n');
|
||||
if (!willEndWithNewline) ins += '\n';
|
||||
if (ndel === 0 && ins.length === 0) return;
|
||||
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
|
||||
const changeset = makeSplice(orig, start, ndel, ins);
|
||||
await this.appendRevision(changeset, authorId);
|
||||
}
|
||||
|
||||
|
@ -330,7 +332,7 @@ class Pad {
|
|||
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
||||
* `msgOrText.time` instead.
|
||||
*/
|
||||
async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
|
||||
async appendChatMessage(msgOrText: string| ChatMessage, authorId = null, time = null) {
|
||||
const msg =
|
||||
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
|
||||
this.chatHead++;
|
||||
|
@ -393,7 +395,7 @@ class Pad {
|
|||
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
||||
text = exports.cleanText(context.content);
|
||||
}
|
||||
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
|
||||
const firstChangeset = makeSplice('\n', 0, 0, text);
|
||||
await this.appendRevision(firstChangeset, authorId);
|
||||
}
|
||||
await hooks.aCallAll('padLoad', {pad: this});
|
||||
|
@ -437,11 +439,11 @@ class Pad {
|
|||
// let the plugins know the pad was copied
|
||||
await hooks.aCallAll('padCopy', {
|
||||
get originalPad() {
|
||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
pad_utils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
return this.srcPad;
|
||||
},
|
||||
get destinationID() {
|
||||
warnDeprecated(
|
||||
pad_utils.warnDeprecated(
|
||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||
return this.dstPad.id;
|
||||
},
|
||||
|
@ -520,8 +522,8 @@ class Pad {
|
|||
const oldAText = this.atext;
|
||||
|
||||
// based on Changeset.makeSplice
|
||||
const assem = Changeset.smartOpAssembler();
|
||||
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
|
||||
const assem = new SmartOpAssembler();
|
||||
for (const op of opsFromAText(oldAText)) assem.append(op);
|
||||
assem.endDocument();
|
||||
|
||||
// although we have instantiated the dstPad with '\n', an additional '\n' is
|
||||
|
@ -533,16 +535,16 @@ class Pad {
|
|||
|
||||
// create a changeset that removes the previous text and add the newText with
|
||||
// all atributes present on the source pad
|
||||
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
|
||||
const changeset = pack(oldLength, newLength, assem.toString(), newText);
|
||||
dstPad.appendRevision(changeset, authorId);
|
||||
|
||||
await hooks.aCallAll('padCopy', {
|
||||
get originalPad() {
|
||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
pad_utils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
return this.srcPad;
|
||||
},
|
||||
get destinationID() {
|
||||
warnDeprecated(
|
||||
pad_utils.warnDeprecated(
|
||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||
return this.dstPad.id;
|
||||
},
|
||||
|
@ -585,12 +587,14 @@ class Pad {
|
|||
p.push(db.remove(`pad2readonly:${padID}`));
|
||||
|
||||
// delete all chat messages
|
||||
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
|
||||
// @ts-ignore
|
||||
p.push(timesLimit(this.chatHead + 1, 500, async (i: string) => {
|
||||
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
|
||||
}));
|
||||
|
||||
// delete all revisions
|
||||
p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => {
|
||||
// @ts-ignore
|
||||
p.push(timesLimit(this.head + 1, 500, async (i: string) => {
|
||||
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
|
||||
}));
|
||||
|
||||
|
@ -603,7 +607,7 @@ class Pad {
|
|||
p.push(padManager.removePad(padID));
|
||||
p.push(hooks.aCallAll('padRemove', {
|
||||
get padID() {
|
||||
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||
pad_utils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||
return this.pad.id;
|
||||
},
|
||||
pad: this,
|
||||
|
@ -706,7 +710,7 @@ class Pad {
|
|||
}
|
||||
})
|
||||
.batch(100).buffer(99);
|
||||
let atext = Changeset.makeAText('\n');
|
||||
let atext = makeAText('\n');
|
||||
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
||||
try {
|
||||
assert(authorId != null);
|
||||
|
@ -717,10 +721,10 @@ class Pad {
|
|||
assert(timestamp > 0);
|
||||
assert(changeset != null);
|
||||
assert.equal(typeof changeset, 'string');
|
||||
Changeset.checkRep(changeset);
|
||||
const unpacked = Changeset.unpack(changeset);
|
||||
checkRep(changeset);
|
||||
const unpacked = unpack(changeset);
|
||||
let text = atext.text;
|
||||
for (const op of Changeset.deserializeOps(unpacked.ops)) {
|
||||
for (const op of deserializeOps(unpacked.ops)) {
|
||||
if (['=', '-'].includes(op.opcode)) {
|
||||
assert(text.length >= op.chars);
|
||||
const consumed = text.slice(0, op.chars);
|
||||
|
@ -731,7 +735,7 @@ class Pad {
|
|||
}
|
||||
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
|
||||
}
|
||||
atext = Changeset.applyToAText(changeset, atext, pool);
|
||||
atext = applyToAText(changeset, atext, pool);
|
||||
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
||||
} catch (err:any) {
|
||||
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
import {UserSettingsObject} from "../types/UserSettingsObject";
|
||||
|
||||
const authorManager = require('./AuthorManager');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const padManager = require('./PadManager');
|
||||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
|
@ -30,7 +30,7 @@ const settings = require('../utils/Settings');
|
|||
const webaccess = require('../hooks/express/webaccess');
|
||||
const log4js = require('log4js');
|
||||
const authLogger = log4js.getLogger('auth');
|
||||
const {padutils} = require('../../static/js/pad_utils');
|
||||
import padutils from '../../static/js/pad_utils'
|
||||
|
||||
const DENY = Object.freeze({accessStatus: 'deny'});
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
*/
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const promises = require('../utils/promises');
|
||||
import {firstSatisfies} from '../utils/promises';
|
||||
const randomString = require('../utils/randomstring');
|
||||
const db = require('./DB');
|
||||
const groupManager = require('./GroupManager');
|
||||
|
@ -79,7 +79,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
|||
groupID: string;
|
||||
validUntil: number;
|
||||
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
|
||||
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
|
||||
const sessionInfo = await firstSatisfies(sessionInfoPromises, isMatch) as any;
|
||||
if (sessionInfo == null) return undefined;
|
||||
return sessionInfo.authorID;
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
const ejs = require('ejs');
|
||||
const fs = require('fs');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const path = require('path');
|
||||
const resolve = require('resolve');
|
||||
const settings = require('../utils/Settings');
|
||||
|
|
|
@ -20,14 +20,15 @@
|
|||
*/
|
||||
|
||||
import {MapArrayType} from "../types/MapType";
|
||||
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
const api = require('../db/API');
|
||||
const padManager = require('../db/PadManager');
|
||||
const settings = require('../utils/Settings');
|
||||
import createHTTPError from 'http-errors';
|
||||
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
|
||||
import {Http2ServerRequest} from "node:http2";
|
||||
import {publicKeyExported} from "../security/OAuth2Provider";
|
||||
import {jwtVerify} from "jose";
|
||||
import {apikey} from './APIKeyHandler'
|
||||
import {APIFields, apikey} from './APIKeyHandler'
|
||||
// a list of all functions
|
||||
const version:MapArrayType<any> = {};
|
||||
|
||||
|
@ -141,6 +142,7 @@ version['1.3.0'] = {
|
|||
setText: ['padID', 'text', 'authorId'],
|
||||
};
|
||||
|
||||
|
||||
// set the latest available API version here
|
||||
exports.latestApiVersion = '1.3.0';
|
||||
|
||||
|
@ -148,13 +150,6 @@ exports.latestApiVersion = '1.3.0';
|
|||
exports.version = version;
|
||||
|
||||
|
||||
type APIFields = {
|
||||
apikey: string;
|
||||
api_key: string;
|
||||
padID: string;
|
||||
padName: string;
|
||||
authorization: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an HTTP API call
|
||||
|
@ -188,8 +183,17 @@ exports.handle = async function (apiVersion: string, functionName: string, field
|
|||
throw new createHTTPError.Unauthorized('no or wrong API Key');
|
||||
}
|
||||
try {
|
||||
await jwtVerify(req.headers.authorization!.replace("Bearer ", ""), publicKeyExported!, {algorithms: ['RS256'],
|
||||
requiredClaims: ["admin"]})
|
||||
const clientIds: string[] = settings.sso.clients?.map((client: {client_id: string}) => client.client_id);
|
||||
const jwtToCheck = req.headers.authorization.replace("Bearer ", "")
|
||||
const payload = jwtDecode(jwtToCheck)
|
||||
// client_credentials
|
||||
if (clientIds.includes(<string>payload.sub)) {
|
||||
await jwtVerify(jwtToCheck, publicKeyExported!, {algorithms: ['RS256']})
|
||||
} else {
|
||||
// authorization_code
|
||||
await jwtVerify(jwtToCheck, publicKeyExported!, {algorithms: ['RS256'],
|
||||
requiredClaims: ["admin"]})
|
||||
}
|
||||
} catch (e) {
|
||||
throw new createHTTPError.Unauthorized('no or wrong OAuth token');
|
||||
}
|
||||
|
|
|
@ -7,6 +7,16 @@ const settings = require('../utils/Settings');
|
|||
|
||||
const apiHandlerLogger = log4js.getLogger('APIHandler');
|
||||
|
||||
|
||||
|
||||
export type APIFields = {
|
||||
apikey: string;
|
||||
api_key: string;
|
||||
padID: string;
|
||||
padName: string;
|
||||
authorization: string;
|
||||
}
|
||||
|
||||
// ensure we have an apikey
|
||||
export let apikey:string|null = null;
|
||||
const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt');
|
||||
|
|
|
@ -31,7 +31,7 @@ import os from 'os';
|
|||
const importHtml = require('../utils/ImportHtml');
|
||||
const importEtherpad = require('../utils/ImportEtherpad');
|
||||
import log4js from 'log4js';
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
|
||||
const logger = log4js.getLogger('ImportHandler');
|
||||
|
||||
|
|
|
@ -21,28 +21,30 @@
|
|||
|
||||
import {MapArrayType} from "../types/MapType";
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
const padManager = require('../db/PadManager');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import {checkRep, cloneAText, compose, deserializeOps, follow, identity, inverse, makeAText, makeSplice, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, oldLen, prepareForWire, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
|
||||
import ChatMessage from '../../static/js/ChatMessage';
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
const AttributeManager = require('../../static/js/AttributeManager');
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
const {padutils} = require('../../static/js/pad_utils');
|
||||
import padutils from '../../static/js/pad_utils';
|
||||
const readOnlyManager = require('../db/ReadOnlyManager');
|
||||
const settings = require('../utils/Settings');
|
||||
const securityManager = require('../db/SecurityManager');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs.js');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||
import log4js from 'log4js';
|
||||
const messageLogger = log4js.getLogger('message');
|
||||
const accessLogger = log4js.getLogger('access');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const stats = require('../stats')
|
||||
const assert = require('assert').strict;
|
||||
import {RateLimiterMemory} from 'rate-limiter-flexible';
|
||||
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
|
||||
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
|
||||
import {ChangeSet} from "../types/ChangeSet";
|
||||
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, PadDeleteMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
|
||||
import {Builder} from "../../static/js/Builder";
|
||||
const webaccess = require('../hooks/express/webaccess');
|
||||
const { checkValidRev } = require('../utils/checkValidRev');
|
||||
|
||||
|
@ -209,12 +211,51 @@ exports.handleDisconnect = async (socket:any) => {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
const handlePadDelete = async (socket: any, padDeleteMessage: PadDeleteMessage) => {
|
||||
const session = sessioninfos[socket.id];
|
||||
if (!session || !session.author || !session.padId) throw new Error('session not ready');
|
||||
if (await padManager.doesPadExist(padDeleteMessage.data.padId)) {
|
||||
const retrievedPad = await padManager.getPad(padDeleteMessage.data.padId)
|
||||
// Only the one doing the first revision can delete the pad, otherwise people could troll a lot
|
||||
const firstContributor = await retrievedPad.getRevisionAuthor(0)
|
||||
if (session.author === firstContributor) {
|
||||
retrievedPad.remove()
|
||||
} else {
|
||||
|
||||
type ShoutMessage = {
|
||||
message: string,
|
||||
sticky: boolean,
|
||||
}
|
||||
|
||||
const messageToShout: ShoutMessage = {
|
||||
message: 'You are not the creator of this pad, so you cannot delete it',
|
||||
sticky: false
|
||||
}
|
||||
const messageToSend = {
|
||||
type: "COLLABROOM",
|
||||
data: {
|
||||
type: "shoutMessage",
|
||||
payload: {
|
||||
message: messageToShout,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
socket.emit('shout',
|
||||
messageToSend
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles a message from a user
|
||||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||
exports.handleMessage = async (socket:any, message: ClientVarMessage) => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
|
||||
if (env === 'production') {
|
||||
|
@ -348,15 +389,16 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
|||
stats.counter('pendingEdits').inc();
|
||||
await padChannels.enqueue(thisSession.padId, {socket, message});
|
||||
break;
|
||||
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break;
|
||||
case 'CHAT_MESSAGE': await handleChatMessage(socket, message); break;
|
||||
case 'PAD_DELETE': await handlePadDelete(socket, message.data as unknown as PadDeleteMessage); break;
|
||||
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message as unknown as UserNewInfoMessage); break;
|
||||
case 'CHAT_MESSAGE': await handleChatMessage(socket, message as unknown as ChatMessageMessage); break;
|
||||
case 'GET_CHAT_MESSAGES': await handleGetChatMessages(socket, message); break;
|
||||
case 'SAVE_REVISION': await handleSaveRevisionMessage(socket, message); break;
|
||||
case 'SAVE_REVISION': await handleSaveRevisionMessage(socket, message as unknown as ClientSaveRevisionMessage); break;
|
||||
case 'CLIENT_MESSAGE': {
|
||||
const {type} = message.data.payload;
|
||||
try {
|
||||
switch (type) {
|
||||
case 'suggestUserName': handleSuggestUserName(socket, message); break;
|
||||
case 'suggestUserName': handleSuggestUserName(socket, message as unknown as ClientSuggestUserName); break;
|
||||
default: throw new Error('unknown message type');
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -384,7 +426,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleSaveRevisionMessage = async (socket:any, message: string) => {
|
||||
const handleSaveRevisionMessage = async (socket:any, message: ClientSaveRevisionMessage) => {
|
||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||
const pad = await padManager.getPad(padId, null, authorId);
|
||||
await pad.addSavedRevision(pad.head, authorId);
|
||||
|
@ -397,7 +439,7 @@ const handleSaveRevisionMessage = async (socket:any, message: string) => {
|
|||
* @param msg {Object} the message we're sending
|
||||
* @param sessionID {string} the socketIO session to which we're sending this message
|
||||
*/
|
||||
exports.handleCustomObjectMessage = (msg: typeof ChatMessage, sessionID: string) => {
|
||||
exports.handleCustomObjectMessage = (msg: CustomMessage, sessionID: string) => {
|
||||
if (msg.data.type === 'CUSTOM') {
|
||||
if (sessionID) {
|
||||
// a sessionID is targeted: directly to this sessionID
|
||||
|
@ -432,7 +474,7 @@ exports.handleCustomMessage = (padID: string, msgString:string) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleChatMessage = async (socket:any, message: typeof ChatMessage) => {
|
||||
const handleChatMessage = async (socket:any, message: ChatMessageMessage) => {
|
||||
const chatMessage = ChatMessage.fromObject(message.data.message);
|
||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||
// Don't trust the user-supplied values.
|
||||
|
@ -452,7 +494,7 @@ const handleChatMessage = async (socket:any, message: typeof ChatMessage) => {
|
|||
* @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message
|
||||
* object as the first argument and the destination pad ID as the second argument instead.
|
||||
*/
|
||||
exports.sendChatMessageToPadClients = async (mt: typeof ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => {
|
||||
exports.sendChatMessageToPadClients = async (mt: ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => {
|
||||
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
|
||||
padId = mt instanceof ChatMessage ? puId : padId;
|
||||
const pad = await padManager.getPad(padId, null, message.authorId);
|
||||
|
@ -499,7 +541,7 @@ const handleGetChatMessages = async (socket:any, {data: {start, end}}:any) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleSuggestUserName = (socket:any, message: typeof ChatMessage) => {
|
||||
const handleSuggestUserName = (socket:any, message: ClientSuggestUserName) => {
|
||||
const {newName, unnamedId} = message.data.payload;
|
||||
if (newName == null) throw new Error('missing newName');
|
||||
if (unnamedId == null) throw new Error('missing unnamedId');
|
||||
|
@ -519,7 +561,7 @@ const handleSuggestUserName = (socket:any, message: typeof ChatMessage) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId}}}: PadUserInfo) => {
|
||||
const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId}}}: UserNewInfoMessage) => {
|
||||
if (colorId == null) throw new Error('missing colorId');
|
||||
if (!name) name = null;
|
||||
const session = sessioninfos[socket.id];
|
||||
|
@ -567,7 +609,9 @@ const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
||||
const handleUserChanges = async (socket:any, message: {
|
||||
data: ClientUserChangesMessage
|
||||
}) => {
|
||||
// This one's no longer pending, as we're gonna process it now
|
||||
stats.counter('pendingEdits').dec();
|
||||
|
||||
|
@ -591,10 +635,10 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
|||
const pad = await padManager.getPad(thisSession.padId, null, thisSession.author);
|
||||
|
||||
// Verify that the changeset has valid syntax and is in canonical form
|
||||
Changeset.checkRep(changeset);
|
||||
checkRep(changeset);
|
||||
|
||||
// Validate all added 'author' attribs to be the same value as the current user
|
||||
for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) {
|
||||
for (const op of deserializeOps(unpack(changeset).ops)) {
|
||||
// + can add text with attribs
|
||||
// = can change or add attribs
|
||||
// - can have attribs, but they are discarded and don't show up in the attribs -
|
||||
|
@ -613,7 +657,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
|||
// ex. adoptChangesetAttribs
|
||||
|
||||
// Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
|
||||
let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
|
||||
let rebasedChangeset = moveOpsToNewPool(changeset, wireApool, pad.pool);
|
||||
|
||||
// ex. applyUserChanges
|
||||
let r = baseRev;
|
||||
|
@ -626,21 +670,21 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
|||
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
|
||||
if (changeset === c && thisSession.author === authorId) {
|
||||
// Assume this is a retransmission of an already applied changeset.
|
||||
rebasedChangeset = Changeset.identity(Changeset.unpack(changeset).oldLen);
|
||||
rebasedChangeset = identity(unpack(changeset).oldLen);
|
||||
}
|
||||
// At this point, both "c" (from the pad) and "changeset" (from the
|
||||
// client) are relative to revision r - 1. The follow function
|
||||
// rebases "changeset" so that it is relative to revision r
|
||||
// and can be applied after "c".
|
||||
rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool);
|
||||
rebasedChangeset = follow(c, rebasedChangeset, false, pad.pool);
|
||||
}
|
||||
|
||||
const prevText = pad.text();
|
||||
|
||||
if (Changeset.oldLen(rebasedChangeset) !== prevText.length) {
|
||||
if (oldLen(rebasedChangeset) !== prevText.length) {
|
||||
throw new Error(
|
||||
`Can't apply changeset ${rebasedChangeset} with oldLen ` +
|
||||
`${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
|
||||
`${oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
|
||||
}
|
||||
|
||||
const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author);
|
||||
|
@ -655,7 +699,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
|||
|
||||
// Make sure the pad always ends with an empty line.
|
||||
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
|
||||
const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
|
||||
const nlChangeset = makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
|
||||
await pad.appendRevision(nlChangeset, thisSession.author);
|
||||
}
|
||||
|
||||
|
@ -710,7 +754,7 @@ exports.updatePadClients = async (pad: PadType) => {
|
|||
const revChangeset = revision.changeset;
|
||||
const currentTime = revision.meta.timestamp;
|
||||
|
||||
const forWire = Changeset.prepareForWire(revChangeset, pad.pool);
|
||||
const forWire = prepareForWire(revChangeset, pad.pool);
|
||||
const msg = {
|
||||
type: 'COLLABROOM',
|
||||
data: {
|
||||
|
@ -738,14 +782,14 @@ exports.updatePadClients = async (pad: PadType) => {
|
|||
/**
|
||||
* Copied from the Etherpad Source Code. Don't know what this method does excatly...
|
||||
*/
|
||||
const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
||||
const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||
const text = atext.text;
|
||||
|
||||
// collect char positions of line markers (e.g. bullets) in new atext
|
||||
// that aren't at the start of a line
|
||||
const badMarkers = [];
|
||||
let offset = 0;
|
||||
for (const op of Changeset.deserializeOps(atext.attribs)) {
|
||||
for (const op of deserializeOps(atext.attribs)) {
|
||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
|
||||
if (hasMarker) {
|
||||
|
@ -767,7 +811,7 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
|||
// create changeset that removes these bad markers
|
||||
offset = 0;
|
||||
|
||||
const builder = Changeset.builder(text.length);
|
||||
const builder = new Builder(text.length);
|
||||
|
||||
badMarkers.forEach((pos) => {
|
||||
builder.keepText(text.substring(offset, pos));
|
||||
|
@ -785,7 +829,7 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||
const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
|
||||
const sessionInfo = sessioninfos[socket.id];
|
||||
if (sessionInfo == null) throw new Error('client disconnected');
|
||||
assert(sessionInfo.author);
|
||||
|
@ -793,8 +837,9 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context.
|
||||
|
||||
let {colorId: authorColorId, name: authorName} = message.userInfo || {};
|
||||
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) {
|
||||
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId as string)) {
|
||||
messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`);
|
||||
// @ts-ignore
|
||||
authorColorId = null;
|
||||
}
|
||||
await Promise.all([
|
||||
|
@ -872,7 +917,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
const revisionsNeeded = [];
|
||||
const changesets:MapArrayType<any> = {};
|
||||
|
||||
let startNum = message.client_rev + 1;
|
||||
let startNum = message.client_rev! + 1;
|
||||
let endNum = pad.getHeadRevisionNumber() + 1;
|
||||
|
||||
const headNum = pad.getHeadRevisionNumber();
|
||||
|
@ -901,7 +946,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
|
||||
// return pending changesets
|
||||
for (const r of revisionsNeeded) {
|
||||
const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool);
|
||||
const forWire = prepareForWire(changesets[r].changeset, pad.pool);
|
||||
const wireMsg = {type: 'COLLABROOM',
|
||||
data: {type: 'CLIENT_RECONNECT',
|
||||
headRev: pad.getHeadRevisionNumber(),
|
||||
|
@ -926,8 +971,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
let apool;
|
||||
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
|
||||
try {
|
||||
atext = Changeset.cloneAText(pad.atext);
|
||||
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
|
||||
atext = cloneAText(pad.atext);
|
||||
const attribsForWire = prepareForWire(atext.attribs, pad.pool);
|
||||
apool = attribsForWire.pool.toJsonable();
|
||||
atext.attribs = attribsForWire.translated;
|
||||
} catch (e:any) {
|
||||
|
@ -1142,13 +1187,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
|
|||
getPadLines(pad, startNum - 1),
|
||||
// Get all needed composite Changesets.
|
||||
...compositesChangesetNeeded.map(async (item) => {
|
||||
const changeset = await composePadChangesets(pad, item.start, item.end);
|
||||
const changeset = await exports.composePadChangesets(pad, item.start, item.end);
|
||||
composedChangesets[`${item.start}/${item.end}`] = changeset;
|
||||
}),
|
||||
// Get all needed revision Dates.
|
||||
...revTimesNeeded.map(async (revNum) => {
|
||||
const revDate = await pad.getRevisionDate(revNum);
|
||||
revisionDate[revNum] = Math.floor(revDate / 1000);
|
||||
revisionDate[revNum] = revDate;
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -1163,13 +1208,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
|
|||
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
|
||||
|
||||
const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`];
|
||||
const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
|
||||
const backwards = inverse(forwards, lines.textlines, lines.alines, pad.apool());
|
||||
|
||||
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
|
||||
Changeset.mutateTextLines(forwards, lines.textlines);
|
||||
mutateAttributionLines(forwards, lines.alines, pad.apool());
|
||||
mutateTextLines(forwards, lines.textlines);
|
||||
|
||||
const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
|
||||
const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
|
||||
const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool);
|
||||
const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool);
|
||||
|
||||
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
|
||||
const t2 = revisionDate[compositeEnd - 1];
|
||||
|
@ -1195,12 +1240,12 @@ const getPadLines = async (pad: PadType, revNum: number) => {
|
|||
if (revNum >= 0) {
|
||||
atext = await pad.getInternalRevisionAText(revNum);
|
||||
} else {
|
||||
atext = Changeset.makeAText('\n');
|
||||
atext = makeAText('\n');
|
||||
}
|
||||
|
||||
return {
|
||||
textlines: Changeset.splitTextLines(atext.text),
|
||||
alines: Changeset.splitAttributionLines(atext.attribs, atext.text),
|
||||
textlines: splitTextLines(atext.text),
|
||||
alines: splitAttributionLines(atext.attribs, atext.text),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1208,7 +1253,7 @@ const getPadLines = async (pad: PadType, revNum: number) => {
|
|||
* Tries to rebuild the composePadChangeset function of the original Etherpad
|
||||
* https://github.com/ether/pad/blob/master/etherpad/src/etherpad/control/pad/pad_changeset_control.js#L241
|
||||
*/
|
||||
const composePadChangesets = async (pad: PadType, startNum: number, endNum: number) => {
|
||||
exports.composePadChangesets = async (pad: PadType, startNum: number, endNum: number) => {
|
||||
// fetch all changesets we need
|
||||
const headNum = pad.getHeadRevisionNumber();
|
||||
endNum = Math.min(endNum, headNum + 1);
|
||||
|
@ -1235,7 +1280,7 @@ const composePadChangesets = async (pad: PadType, startNum: number, endNum: numb
|
|||
|
||||
for (r = startNum + 1; r < endNum; r++) {
|
||||
const cs = changesets[r];
|
||||
changeset = Changeset.compose(changeset, cs, pool);
|
||||
changeset = compose(changeset as string, cs as string, pool);
|
||||
}
|
||||
return changeset;
|
||||
} catch (e) {
|
||||
|
|
1527
src/node/handler/RestAPI.ts
Normal file
1527
src/node/handler/RestAPI.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,11 +5,12 @@ import {ErrorCaused} from "../../types/ErrorCaused";
|
|||
import {QueryType} from "../../types/QueryType";
|
||||
|
||||
import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer";
|
||||
import {PackageData} from "../../types/PackageInfo";
|
||||
|
||||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
||||
import {PackageData, PackageInfo} from "../../types/PackageInfo";
|
||||
import semver from 'semver';
|
||||
import log4js from 'log4js';
|
||||
import {MapArrayType} from "../../types/MapType";
|
||||
|
||||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const logger = log4js.getLogger('adminPlugins');
|
||||
|
||||
|
||||
|
@ -20,10 +21,34 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
|||
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
|
||||
if (!isAdmin) return;
|
||||
|
||||
socket.on('getInstalled', (query:string) => {
|
||||
const checkPluginForUpdates = async () => {
|
||||
let results: MapArrayType<PackageInfo>
|
||||
try {
|
||||
results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
|
||||
} catch (error) {
|
||||
console.error('Error checking for plugin updates:', error);
|
||||
return [];
|
||||
}
|
||||
return Object.keys(pluginDefs.plugins).filter((plugin) => {
|
||||
if (!results[plugin]) return false;
|
||||
|
||||
const latestVersion = results[plugin].version;
|
||||
const currentVersion = pluginDefs.plugins[plugin].package.version;
|
||||
|
||||
return semver.gt(latestVersion, currentVersion);
|
||||
})
|
||||
}
|
||||
|
||||
socket.on('getInstalled', async (query: string) => {
|
||||
// send currently installed plugins
|
||||
const installed =
|
||||
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
||||
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
||||
|
||||
const updatable = await checkPluginForUpdates();
|
||||
|
||||
installed.forEach((plugin) => {
|
||||
plugin.updatable = updatable.includes(plugin.name);
|
||||
})
|
||||
|
||||
socket.emit('results:installed', {installed});
|
||||
});
|
||||
|
@ -31,16 +56,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
|||
socket.on('checkUpdates', async () => {
|
||||
// Check plugins for updates
|
||||
try {
|
||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ 60 * 10);
|
||||
|
||||
const updatable = Object.keys(pluginDefs.plugins).filter((plugin) => {
|
||||
if (!results[plugin]) return false;
|
||||
|
||||
const latestVersion = results[plugin].version;
|
||||
const currentVersion = pluginDefs.plugins[plugin].package.version;
|
||||
|
||||
return semver.gt(latestVersion, currentVersion);
|
||||
});
|
||||
const updatable = checkPluginForUpdates();
|
||||
|
||||
socket.emit('results:updatable', {updatable});
|
||||
} catch (err) {
|
||||
|
|
|
@ -13,6 +13,7 @@ const settings = require('../../utils/Settings');
|
|||
const UpdateCheck = require('../../utils/UpdateCheck');
|
||||
const padManager = require('../../db/PadManager');
|
||||
const api = require('../../db/API');
|
||||
const cleanup = require('../../utils/Cleanup');
|
||||
|
||||
|
||||
const queryPadLimit = 12;
|
||||
|
@ -252,6 +253,40 @@ exports.socketio = (hookName: string, {io}: any) => {
|
|||
}
|
||||
})
|
||||
|
||||
socket.on('cleanupPadRevisions', async (padId: string) => {
|
||||
if (!settings.cleanup.enabled) {
|
||||
socket.emit('results:cleanupPadRevisions', {
|
||||
error: 'Cleanup disabled. Enable cleanup in settings.json: cleanup.enabled => true',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const padExists = await padManager.doesPadExists(padId);
|
||||
if (padExists) {
|
||||
logger.info(`Cleanup pad revisions: ${padId}`);
|
||||
try {
|
||||
const result = await cleanup.deleteRevisions(padId, settings.cleanup.keepRevisions)
|
||||
if (result) {
|
||||
socket.emit('results:cleanupPadRevisions', {
|
||||
padId: padId,
|
||||
keepRevisions: settings.cleanup.keepRevisions,
|
||||
});
|
||||
logger.info('successful cleaned up pad: ', padId)
|
||||
} else {
|
||||
socket.emit('results:cleanupPadRevisions', {
|
||||
error: 'Error cleaning up pad',
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(`Error in pad ${padId}: ${err.stack || err}`);
|
||||
socket.emit('results:cleanupPadRevisions', {
|
||||
error: err.toString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('restartServer', async () => {
|
||||
logger.info('Admin request to restart server through a socket on /admin/settings');
|
||||
settings.reloadSettings();
|
||||
|
|
|
@ -12,6 +12,7 @@ const webaccess = require('./webaccess');
|
|||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
|
||||
import {build, buildSync} from 'esbuild'
|
||||
import {ArgsExpressType} from "../../types/ArgsExpressType";
|
||||
let ioI: { sockets: { sockets: any[]; }; } | null = null
|
||||
|
||||
exports.socketio = (hookName: string, {io}: any) => {
|
||||
|
@ -19,7 +20,7 @@ exports.socketio = (hookName: string, {io}: any) => {
|
|||
}
|
||||
|
||||
|
||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||
exports.expressPreSession = async (hookName:string, {app}:ArgsExpressType) => {
|
||||
// This endpoint is intended to conform to:
|
||||
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
|
||||
app.get('/health', (req:any, res:any) => {
|
||||
|
@ -40,7 +41,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
|
|||
|
||||
app.get('/robots.txt', (req:any, res:any) => {
|
||||
let filePath =
|
||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt');
|
||||
path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt');
|
||||
res.sendFile(filePath, (err:any) => {
|
||||
// there is no custom robots.txt, send the default robots.txt which dissallows all
|
||||
if (err) {
|
||||
|
@ -111,9 +112,9 @@ const convertTypescript = (content: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleLiveReload = async (args: any, padString: string, timeSliderString: string, indexString: any) => {
|
||||
const handleLiveReload = async (args: ArgsExpressType, padString: string, timeSliderString: string, indexString: any) => {
|
||||
const chokidar = await import('chokidar')
|
||||
const watcher = chokidar.watch(path.join(settings.root, 'src', 'static', 'js'));
|
||||
const watcher = chokidar.watch(path.join(settings.root, 'src', 'static', 'js'), {});
|
||||
let routeHandlers: { [key: string]: Function } = {};
|
||||
|
||||
const setRouteHandler = (path: string, newHandler: Function) => {
|
||||
|
@ -243,37 +244,37 @@ const convertTypescriptWatched = (content: string, cb: (output:string, hash: str
|
|||
})
|
||||
}
|
||||
|
||||
exports.expressCreateServer = async (hookName: string, args: any, cb: Function) => {
|
||||
exports.expressCreateServer = async (_hookName: string, args: ArgsExpressType, cb: Function) => {
|
||||
const padString = eejs.require('ep_etherpad-lite/templates/padBootstrap.js', {
|
||||
pluginModules: (() => {
|
||||
const pluginModules = new Set();
|
||||
for (const part of plugins.parts) {
|
||||
for (const [, hookFnName] of Object.entries(part.client_hooks || {})) {
|
||||
// @ts-ignore
|
||||
pluginModules.add(hookFnName.split(':')[0]);
|
||||
}
|
||||
pluginModules: (() => {
|
||||
const pluginModules = new Set();
|
||||
for (const part of plugins.parts) {
|
||||
for (const [, hookFnName] of Object.entries(part.client_hooks || {})) {
|
||||
// @ts-ignore
|
||||
pluginModules.add(hookFnName.split(':')[0]);
|
||||
}
|
||||
return [...pluginModules];
|
||||
})(),
|
||||
settings,
|
||||
})
|
||||
}
|
||||
return [...pluginModules];
|
||||
})(),
|
||||
settings,
|
||||
})
|
||||
|
||||
const indexString = eejs.require('ep_etherpad-lite/templates/indexBootstrap.js', {
|
||||
})
|
||||
|
||||
const timeSliderString = eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', {
|
||||
pluginModules: (() => {
|
||||
const pluginModules = new Set();
|
||||
for (const part of plugins.parts) {
|
||||
for (const [, hookFnName] of Object.entries(part.client_hooks || {})) {
|
||||
// @ts-ignore
|
||||
pluginModules.add(hookFnName.split(':')[0]);
|
||||
}
|
||||
const timeSliderString = eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', {
|
||||
pluginModules: (() => {
|
||||
const pluginModules = new Set();
|
||||
for (const part of plugins.parts) {
|
||||
for (const [, hookFnName] of Object.entries(part.client_hooks || {})) {
|
||||
// @ts-ignore
|
||||
pluginModules.add(hookFnName.split(':')[0]);
|
||||
}
|
||||
return [...pluginModules];
|
||||
})(),
|
||||
settings,
|
||||
})
|
||||
}
|
||||
return [...pluginModules];
|
||||
})(),
|
||||
settings,
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
@ -294,37 +295,25 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
|
|||
fileNamePad = `padbootstrap-${padSliderWrite.hash}.min.js`
|
||||
fileNameTimeSlider = `timeSliderBootstrap-${timeSliderWrite.hash}.min.js`
|
||||
fileNameIndex = `indexBootstrap-${indexWrite.hash}.min.js`
|
||||
const pathNamePad = path.join(outdir, fileNamePad)
|
||||
const pathNameTimeSlider = path.join(outdir, fileNameTimeSlider)
|
||||
const pathNameIndex = path.join(outdir, 'index.js')
|
||||
|
||||
if (!fs.existsSync(pathNamePad)) {
|
||||
fs.writeFileSync(pathNamePad, padSliderWrite.output);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pathNameIndex)) {
|
||||
fs.writeFileSync(pathNameIndex, indexWrite.output);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(pathNameTimeSlider)) {
|
||||
fs.writeFileSync(pathNameTimeSlider,timeSliderWrite.output)
|
||||
}
|
||||
|
||||
args.app.get("/"+fileNamePad, (req: any, res: any) => {
|
||||
res.sendFile(pathNamePad)
|
||||
args.app.get("/"+fileNamePad, (_req, res) => {
|
||||
res.header('Content-Type', 'application/javascript');
|
||||
res.send(padSliderWrite.output)
|
||||
})
|
||||
|
||||
args.app.get("/"+fileNameIndex, (req: any, res: any) => {
|
||||
res.sendFile(pathNameIndex)
|
||||
args.app.get("/"+fileNameIndex, (_req, res) => {
|
||||
res.header('Content-Type', 'application/javascript');
|
||||
res.send(indexWrite.output)
|
||||
})
|
||||
|
||||
args.app.get("/"+fileNameTimeSlider, (req: any, res: any) => {
|
||||
res.sendFile(pathNameTimeSlider)
|
||||
args.app.get("/"+fileNameTimeSlider, (_req, res) => {
|
||||
res.header('Content-Type', 'application/javascript');
|
||||
res.send(timeSliderWrite.output)
|
||||
})
|
||||
|
||||
// serve index.html under /
|
||||
args.app.get('/', (req: any, res: any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req, settings, entrypoint: "/"+fileNameIndex}));
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req, settings, entrypoint: "./"+fileNameIndex}));
|
||||
});
|
||||
|
||||
|
||||
|
@ -342,7 +331,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
|
|||
req,
|
||||
toolbar,
|
||||
isReadOnly,
|
||||
entrypoint: "/"+fileNamePad
|
||||
entrypoint: "../"+fileNamePad
|
||||
})
|
||||
res.send(content);
|
||||
});
|
||||
|
@ -356,7 +345,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
|
|||
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
||||
req,
|
||||
toolbar,
|
||||
entrypoint: "/"+fileNameTimeSlider
|
||||
entrypoint: "../../"+fileNameTimeSlider
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -4,11 +4,10 @@ import {MapArrayType} from "../../types/MapType";
|
|||
import {PartType} from "../../types/PartType";
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const minify = require('../../utils/Minify');
|
||||
const path = require('path');
|
||||
import {minify} from '../../utils/Minify';
|
||||
import path from 'node:path';
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const settings = require('../../utils/Settings');
|
||||
import CachingMiddleware from '../../utils/caching_middleware';
|
||||
|
||||
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
||||
const getTar = async () => {
|
||||
|
@ -32,15 +31,10 @@ const getTar = async () => {
|
|||
};
|
||||
|
||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||
// Cache both minified and static.
|
||||
const assetCache = new CachingMiddleware();
|
||||
// Cache static assets
|
||||
app.all(/\/js\/(.*)/, assetCache.handle.bind(assetCache));
|
||||
app.all(/\/css\/(.*)/, assetCache.handle.bind(assetCache));
|
||||
|
||||
// Minify will serve static files compressed (minify enabled). It also has
|
||||
// file-specific hacks for ace/require-kernel/etc.
|
||||
app.all('/static/:filename(*)', minify.minify);
|
||||
app.all('/static/:filename(*)', minify);
|
||||
|
||||
// serve plugin definitions
|
||||
// not very static, but served here so that client can do
|
||||
|
|
|
@ -49,8 +49,21 @@ exports.userCanModify = (padId: string, req: SocketClientRequest) => {
|
|||
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
|
||||
exports.authnFailureDelayMs = 1000;
|
||||
|
||||
const staticResources = [
|
||||
/^\/padbootstrap-[a-zA-Z0-9]+\.min\.js$/,
|
||||
/^\/timeSliderBootstrap-[a-zA-Z0-9]+\.min\.js$/,
|
||||
/^\/manifest.json$/
|
||||
]
|
||||
|
||||
const checkAccess = async (req:any, res:any, next: Function) => {
|
||||
const requireAdmin = req.path.toLowerCase().startsWith('/admin-auth');
|
||||
for (const staticResource of staticResources) {
|
||||
if (req.path.match(staticResource)) {
|
||||
console.log(`Loading [${staticResource}] ${req.path}`);
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin
|
||||
|
@ -177,6 +190,10 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
res.status(401).send('Authentication Required');
|
||||
return;
|
||||
}
|
||||
if (ctx.username === '__proto__' || ctx.username === 'constructor' || ctx.username === 'prototype') {
|
||||
res.end(403);
|
||||
return;
|
||||
}
|
||||
settings.users[ctx.username].username = ctx.username;
|
||||
// Make a shallow copy so that the password property can be deleted (to prevent it from
|
||||
// appearing in logs or in the database) without breaking future authentication attempts.
|
||||
|
|
|
@ -7,8 +7,8 @@ const languages = require('languages4translatewiki');
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const _ = require('underscore');
|
||||
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
|
||||
const existsSync = require('../utils/path_exists');
|
||||
const pluginDefs = require('../../static/js/pluginfw/plugin_defs');
|
||||
import existsSync from '../utils/path_exists';
|
||||
const settings = require('../utils/Settings');
|
||||
|
||||
// returns all existing messages merged together and grouped by langcode
|
||||
|
|
|
@ -30,7 +30,7 @@ const configuration: Configuration = {
|
|||
if(account === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (account.is_admin) {
|
||||
if (account.is_admin ) {
|
||||
return {
|
||||
accountId: id,
|
||||
claims: () => ({
|
||||
|
@ -153,7 +153,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
|
|||
});
|
||||
|
||||
|
||||
args.app.post('/interaction/:uid', async (req: Http2ServerRequest, res: Http2ServerResponse, next:Function) => {
|
||||
args.app.post('/interaction/:uid', async (req, res, next) => {
|
||||
const formid = new IncomingForm();
|
||||
try {
|
||||
// @ts-ignore
|
||||
|
@ -226,7 +226,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
|
|||
})
|
||||
|
||||
|
||||
args.app.get('/interaction/:uid', async (req: Request, res: Response, next: Function) => {
|
||||
args.app.get('/interaction/:uid', async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
uid, prompt, params, session,
|
||||
|
|
|
@ -42,6 +42,10 @@ if (settings.dumpOnUncleanExit) {
|
|||
const addProxyToAxios = (url: URL) => {
|
||||
axios.defaults.proxy = {
|
||||
host: url.hostname,
|
||||
auth: {
|
||||
username: url.username,
|
||||
password: url.password,
|
||||
},
|
||||
port: Number(url.port),
|
||||
protocol: url.protocol,
|
||||
}
|
||||
|
@ -74,7 +78,7 @@ const express = require('./hooks/express');
|
|||
const hooks = require('../static/js/pluginfw/hooks');
|
||||
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
|
||||
const plugins = require('../static/js/pluginfw/plugins');
|
||||
const {Gate} = require('./utils/promises');
|
||||
import {Gate} from './utils/promises';
|
||||
const stats = require('./stats')
|
||||
|
||||
const logger = log4js.getLogger('server');
|
||||
|
@ -100,7 +104,7 @@ const removeSignalListener = (signal: NodeJS.Signals, listener: NodeJS.SignalsLi
|
|||
};
|
||||
|
||||
|
||||
let startDoneGate: { resolve: () => void; }
|
||||
let startDoneGate: Gate<unknown>
|
||||
exports.start = async () => {
|
||||
switch (state) {
|
||||
case State.INITIAL:
|
||||
|
@ -181,12 +185,14 @@ exports.start = async () => {
|
|||
} catch (err) {
|
||||
logger.error('Error occurred while starting Etherpad');
|
||||
state = State.STATE_TRANSITION_FAILED;
|
||||
// @ts-ignore
|
||||
startDoneGate.resolve();
|
||||
return await exports.exit(err);
|
||||
}
|
||||
|
||||
logger.info('Etherpad is running');
|
||||
state = State.RUNNING;
|
||||
// @ts-ignore
|
||||
startDoneGate.resolve();
|
||||
|
||||
// Return the HTTP server to make it easier to write tests.
|
||||
|
@ -228,11 +234,13 @@ exports.stop = async () => {
|
|||
} catch (err) {
|
||||
logger.error('Error occurred while stopping Etherpad');
|
||||
state = State.STATE_TRANSITION_FAILED;
|
||||
// @ts-ignore
|
||||
stopDoneGate.resolve();
|
||||
return await exports.exit(err);
|
||||
}
|
||||
logger.info('Etherpad stopped');
|
||||
state = State.STOPPED;
|
||||
// @ts-ignore
|
||||
stopDoneGate.resolve();
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import {Express} from "express";
|
||||
|
||||
export type ArgsExpressType = {
|
||||
app:any,
|
||||
app:Express,
|
||||
io: any,
|
||||
server:any
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {MapArrayType} from "./MapType";
|
||||
import AttributePool from "../../static/js/AttributePool";
|
||||
|
||||
export type PadType = {
|
||||
id: string,
|
||||
apool: ()=>APool,
|
||||
apool: ()=>AttributePool,
|
||||
atext: AText,
|
||||
pool: APool,
|
||||
pool: AttributePool,
|
||||
getInternalRevisionAText: (text:number|string)=>Promise<AText>,
|
||||
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
|
||||
getRevisionAuthor: (rev: number)=>Promise<string>,
|
||||
|
@ -35,6 +36,7 @@ export type APool = {
|
|||
clone: ()=>APool,
|
||||
check: ()=>Promise<void>,
|
||||
eachAttrib: (callback: (key: string, value: any)=>void)=>void,
|
||||
getAttrib: (key: number)=>any,
|
||||
}
|
||||
|
||||
|
||||
|
|
9
src/node/types/Revision.ts
Normal file
9
src/node/types/Revision.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import {AChangeSet} from "./PadType";
|
||||
|
||||
export type Revision = {
|
||||
changeset: AChangeSet,
|
||||
meta: {
|
||||
author: string,
|
||||
timestamp: number,
|
||||
}
|
||||
}
|
168
src/node/utils/Cleanup.ts
Normal file
168
src/node/utils/Cleanup.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
'use strict'
|
||||
|
||||
import {AChangeSet} from "../types/PadType";
|
||||
import {Revision} from "../types/Revision";
|
||||
|
||||
const promises = require('./promises');
|
||||
const padManager = require('ep_etherpad-lite/node/db/PadManager');
|
||||
const db = require('ep_etherpad-lite/node/db/DB');
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const padMessageHandler = require('ep_etherpad-lite/node/handler/PadMessageHandler');
|
||||
const log4js = require('log4js');
|
||||
const logger = log4js.getLogger('cleanup');
|
||||
|
||||
exports.deleteAllRevisions = async (padID: string): Promise<void> => {
|
||||
|
||||
const randomPadId = padID + 'aertdfdf' + Math.random().toString(10)
|
||||
|
||||
let pad = await padManager.getPad(padID);
|
||||
await pad.copyPadWithoutHistory(randomPadId, false);
|
||||
pad = await padManager.getPad(randomPadId);
|
||||
await pad.copyPadWithoutHistory(padID, true);
|
||||
await pad.remove();
|
||||
}
|
||||
|
||||
const createRevision = async (aChangeset: AChangeSet, timestamp: number, isKeyRev: boolean, authorId: string, atext: any, pool: any) => {
|
||||
|
||||
if (authorId !== '') pool.putAttrib(['author', authorId]);
|
||||
|
||||
return {
|
||||
changeset: aChangeset,
|
||||
meta: {
|
||||
author: authorId,
|
||||
timestamp: timestamp,
|
||||
...isKeyRev ? {
|
||||
pool: pool,
|
||||
atext: atext,
|
||||
} : {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
exports.deleteRevisions = async (padId: string, keepRevisions: number): Promise<boolean> => {
|
||||
|
||||
logger.debug('Start cleanup revisions', padId)
|
||||
|
||||
let pad = await padManager.getPad(padId);
|
||||
await pad.check()
|
||||
|
||||
logger.debug('Initial pad is valid')
|
||||
|
||||
if (pad.head <= keepRevisions) {
|
||||
logger.debug('Pad has not enough revisions')
|
||||
return false
|
||||
}
|
||||
|
||||
padMessageHandler.kickSessionsFromPad(padId)
|
||||
|
||||
const cleanupUntilRevision = pad.head - keepRevisions
|
||||
logger.debug('Composing changesets: ', cleanupUntilRevision)
|
||||
const changeset = await padMessageHandler.composePadChangesets(pad, 0, cleanupUntilRevision + 1)
|
||||
|
||||
const revisions: Revision[] = [];
|
||||
|
||||
await promises.timesLimit(keepRevisions + 1, 500, async (i: number) => {
|
||||
const rev = i + cleanupUntilRevision
|
||||
revisions[rev] = await pad.getRevision(rev)
|
||||
});
|
||||
|
||||
logger.debug('Loaded revisions: ', revisions.length)
|
||||
|
||||
await promises.timesLimit(pad.head + 1, 500, async (i: string) => {
|
||||
await db.remove(`pad:${padId}:revs:${i}`, null);
|
||||
});
|
||||
|
||||
let padContent = await db.get(`pad:${padId}`)
|
||||
padContent.head = keepRevisions
|
||||
if (padContent.savedRevisions) {
|
||||
let newSavedRevisions = []
|
||||
|
||||
for (let i = 0; i < padContent.savedRevisions.length; i++) {
|
||||
if (padContent.savedRevisions[i].revNum > cleanupUntilRevision) {
|
||||
padContent.savedRevisions[i].revNum = padContent.savedRevisions[i].revNum - cleanupUntilRevision
|
||||
newSavedRevisions.push(padContent.savedRevisions[i])
|
||||
}
|
||||
}
|
||||
padContent.savedRevisions = newSavedRevisions
|
||||
}
|
||||
await db.set(`pad:${padId}`, padContent);
|
||||
|
||||
let newAText = Changeset.makeAText('\n');
|
||||
let pool = pad.apool()
|
||||
|
||||
newAText = Changeset.applyToAText(changeset, newAText, pool);
|
||||
|
||||
const revision = await createRevision(
|
||||
changeset,
|
||||
revisions[cleanupUntilRevision].meta.timestamp,
|
||||
0 === pad.getKeyRevisionNumber(0),
|
||||
'',
|
||||
newAText,
|
||||
pool
|
||||
);
|
||||
|
||||
const p: Promise<void>[] = [];
|
||||
|
||||
p.push(db.set(`pad:${padId}:revs:0`, revision))
|
||||
|
||||
p.push(promises.timesLimit(keepRevisions, 500, async (i: number) => {
|
||||
const rev = i + cleanupUntilRevision + 1
|
||||
const newRev = rev - cleanupUntilRevision;
|
||||
|
||||
newAText = Changeset.applyToAText(revisions[rev].changeset, newAText, pool);
|
||||
|
||||
const revision = await createRevision(
|
||||
revisions[rev].changeset,
|
||||
revisions[rev].meta.timestamp,
|
||||
newRev === pad.getKeyRevisionNumber(newRev),
|
||||
revisions[rev].meta.author,
|
||||
newAText,
|
||||
pool
|
||||
);
|
||||
|
||||
await db.set(`pad:${padId}:revs:${newRev}`, revision);
|
||||
}));
|
||||
|
||||
await Promise.all(p)
|
||||
|
||||
logger.debug('Finished migration. Checking pad now')
|
||||
|
||||
padManager.unloadPad(padId);
|
||||
|
||||
let newPad = await padManager.getPad(padId);
|
||||
await newPad.check();
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
exports.checkTodos = async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// TODO: Move to settings
|
||||
const settings = {
|
||||
minHead: 100,
|
||||
keepRevisions: 100,
|
||||
minAge: 1,//1000 * 60 * 60 * 24,
|
||||
}
|
||||
|
||||
await Promise.all((await padManager.listAllPads()).padIDs.map(async (padId: string) => {
|
||||
// TODO: Handle concurrency
|
||||
const pad = await padManager.getPad(padId);
|
||||
|
||||
const revisionDate = await pad.getRevisionDate(pad.getHeadRevisionNumber())
|
||||
|
||||
if (pad.head < settings.minHead || padMessageHandler.padUsersCount(padId) > 0 || Date.now() < revisionDate + settings.minAge) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await exports.deleteRevisions(padId, settings.keepRevisions)
|
||||
if (result) {
|
||||
logger.info('successful cleaned up pad: ', padId)
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error(`Error in pad ${padId}: ${err.stack || err}`);
|
||||
return;
|
||||
}
|
||||
}));
|
||||
}
|
|
@ -19,8 +19,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
import AttributePool from "../../static/js/AttributePool";
|
||||
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||
const { checkValidRev } = require('./checkValidRev');
|
||||
|
||||
/*
|
||||
|
@ -30,7 +31,7 @@ exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any;
|
|||
const _analyzeLine = exports._analyzeLine;
|
||||
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
const attribLines = splitAttributionLines(atext.attribs, atext.text);
|
||||
const apool = pad.pool;
|
||||
|
||||
const pieces = [];
|
||||
|
@ -51,14 +52,14 @@ type LineModel = {
|
|||
[id:string]:string|number|LineModel
|
||||
}
|
||||
|
||||
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
|
||||
exports._analyzeLine = (text:string, aline: string, apool: AttributePool) => {
|
||||
const line: LineModel = {};
|
||||
|
||||
// identify list
|
||||
let lineMarker = 0;
|
||||
line.listLevel = 0;
|
||||
if (aline) {
|
||||
const [op] = Changeset.deserializeOps(aline);
|
||||
const [op] = deserializeOps(aline);
|
||||
if (op != null) {
|
||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||
let listType = attribs.get('list');
|
||||
|
@ -78,7 +79,7 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
|
|||
}
|
||||
if (lineMarker) {
|
||||
line.text = text.substring(1);
|
||||
line.aline = Changeset.subattribution(aline, 1);
|
||||
line.aline = subattribution(aline, 1);
|
||||
} else {
|
||||
line.text = text;
|
||||
line.aline = aline;
|
||||
|
|
|
@ -18,7 +18,7 @@ import {MapArrayType} from "../types/MapType";
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const padManager = require('../db/PadManager');
|
||||
const _ = require('underscore');
|
||||
|
@ -27,7 +27,9 @@ const hooks = require('../../static/js/pluginfw/hooks');
|
|||
const eejs = require('../eejs');
|
||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
|
||||
const padutils = require('../../static/js/pad_utils').padutils;
|
||||
import padutils from "../../static/js/pad_utils";
|
||||
import {StringIterator} from "../../static/js/StringIterator";
|
||||
import {StringAssembler} from "../../static/js/StringAssembler";
|
||||
|
||||
const getPadHTML = async (pad: PadType, revNum: string) => {
|
||||
let atext = pad.atext;
|
||||
|
@ -44,7 +46,7 @@ const getPadHTML = async (pad: PadType, revNum: string) => {
|
|||
const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string[]) => {
|
||||
const apool = pad.apool();
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
const attribLines = splitAttributionLines(atext.attribs, atext.text);
|
||||
|
||||
const tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
|
||||
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||
|
@ -80,6 +82,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
|||
css += '<style>\n';
|
||||
|
||||
for (const a of Object.keys(apool.numToAttrib)) {
|
||||
// @ts-ignore
|
||||
const attr = apool.numToAttrib[a];
|
||||
|
||||
// skip non author attributes
|
||||
|
@ -115,6 +118,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
|||
// see hook exportHtmlAdditionalTagsWithData
|
||||
attrib = propName;
|
||||
}
|
||||
// @ts-ignore
|
||||
const propTrueNum = apool.putAttrib(attrib, true);
|
||||
if (propTrueNum >= 0) {
|
||||
anumMap[propTrueNum] = i;
|
||||
|
@ -127,8 +131,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
|||
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||
// becomes
|
||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||
const taker = Changeset.stringIterator(text);
|
||||
const assem = Changeset.stringAssembler();
|
||||
const taker = new StringIterator(text);
|
||||
const assem = new StringAssembler();
|
||||
const openTags:string[] = [];
|
||||
|
||||
const getSpanClassFor = (i: string) => {
|
||||
|
@ -204,7 +208,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
|||
return;
|
||||
}
|
||||
|
||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
||||
// @ts-ignore
|
||||
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||
idx += numChars;
|
||||
|
||||
// this iterates over every op string and decides which tags to open or to close
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
import {AText, PadType} from "../types/PadType";
|
||||
import {MapType} from "../types/MapType";
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||
import {StringIterator} from "../../static/js/StringIterator";
|
||||
import {StringAssembler} from "../../static/js/StringAssembler";
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const padManager = require('../db/PadManager');
|
||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
|
@ -45,13 +47,14 @@ const getPadTXT = async (pad: PadType, revNum: string) => {
|
|||
const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||
const apool = pad.apool();
|
||||
const textLines = atext.text.slice(0, -1).split('\n');
|
||||
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
|
||||
const attribLines = splitAttributionLines(atext.attribs, atext.text);
|
||||
|
||||
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||
const anumMap: MapType = {};
|
||||
const css = '';
|
||||
|
||||
props.forEach((propName, i) => {
|
||||
// @ts-ignore
|
||||
const propTrueNum = apool.putAttrib([propName, true], true);
|
||||
if (propTrueNum >= 0) {
|
||||
anumMap[propTrueNum] = i;
|
||||
|
@ -69,8 +72,8 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
|||
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||
// becomes
|
||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||
const taker = Changeset.stringIterator(text);
|
||||
const assem = Changeset.stringAssembler();
|
||||
const taker = new StringIterator(text);
|
||||
const assem = new StringAssembler();
|
||||
|
||||
let idx = 0;
|
||||
|
||||
|
@ -79,7 +82,7 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
||||
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||
idx += numChars;
|
||||
|
||||
for (const o of ops) {
|
||||
|
|
|
@ -18,7 +18,7 @@ import {APool} from "../types/PadType";
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
const {Pad} = require('../db/Pad');
|
||||
const Stream = require('./Stream');
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
|
@ -61,7 +61,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
|||
try {
|
||||
const processRecord = async (key:string, value: null|{
|
||||
padIDs: string|Record<string, unknown>,
|
||||
pool: APool
|
||||
pool: AttributePool
|
||||
}) => {
|
||||
if (!value) return;
|
||||
const keyParts = key.split(':');
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
*/
|
||||
|
||||
import log4js from 'log4js';
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import {deserializeOps} from '../../static/js/Changeset';
|
||||
const contentcollector = require('../../static/js/contentcollector');
|
||||
import jsdom from 'jsdom';
|
||||
import {PadType} from "../types/PadType";
|
||||
import {Builder} from "../../static/js/Builder";
|
||||
|
||||
const apiLogger = log4js.getLogger('ImportHtml');
|
||||
let processor:any;
|
||||
|
@ -69,13 +70,13 @@ exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => {
|
|||
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
|
||||
|
||||
// create a new changeset with a helper builder object
|
||||
const builder = Changeset.builder(1);
|
||||
const builder = new Builder(1);
|
||||
|
||||
// assemble each line into the builder
|
||||
let textIndex = 0;
|
||||
const newTextStart = 0;
|
||||
const newTextEnd = newText.length;
|
||||
for (const op of Changeset.deserializeOps(newAttribs)) {
|
||||
for (const op of deserializeOps(newAttribs)) {
|
||||
const nextIndex = textIndex + op.chars;
|
||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||
const start = Math.max(newTextStart, textIndex);
|
||||
|
|
|
@ -21,20 +21,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const settings = require('./Settings');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||
const mime = require('mime-types');
|
||||
const Threads = require('threads');
|
||||
const log4js = require('log4js');
|
||||
const sanitizePathname = require('./sanitizePathname');
|
||||
import {TransformResult} from "esbuild";
|
||||
import mime from 'mime-types';
|
||||
import log4js from 'log4js';
|
||||
import {compressCSS, compressJS} from './MinifyWorker'
|
||||
|
||||
const settings = require('./Settings');
|
||||
import {promises as fs} from 'fs';
|
||||
import path from 'node:path';
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||
import sanitizePathname from './sanitizePathname';
|
||||
const logger = log4js.getLogger('Minify');
|
||||
|
||||
const ROOT_DIR = path.join(settings.root, 'src/static/');
|
||||
|
||||
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
|
||||
|
||||
const LIBRARY_WHITELIST = [
|
||||
'async',
|
||||
|
@ -48,10 +48,10 @@ const LIBRARY_WHITELIST = [
|
|||
|
||||
// What follows is a terrible hack to avoid loop-back within the server.
|
||||
// TODO: Serve files from another service, or directly from the file system.
|
||||
const requestURI = async (url, method, headers) => {
|
||||
const requestURI = async (url: string | URL, method: any, headers: { [x: string]: any; }) => {
|
||||
const parsedUrl = new URL(url);
|
||||
let status = 500;
|
||||
const content = [];
|
||||
const content: any[] = [];
|
||||
const mockRequest = {
|
||||
url,
|
||||
method,
|
||||
|
@ -61,7 +61,7 @@ const requestURI = async (url, method, headers) => {
|
|||
let mockResponse;
|
||||
const p = new Promise((resolve) => {
|
||||
mockResponse = {
|
||||
writeHead: (_status, _headers) => {
|
||||
writeHead: (_status: number, _headers: { [x: string]: any; }) => {
|
||||
status = _status;
|
||||
for (const header in _headers) {
|
||||
if (Object.prototype.hasOwnProperty.call(_headers, header)) {
|
||||
|
@ -69,37 +69,63 @@ const requestURI = async (url, method, headers) => {
|
|||
}
|
||||
}
|
||||
},
|
||||
setHeader: (header, value) => {
|
||||
setHeader: (header: string, value: { toString: () => any; }) => {
|
||||
headers[header.toLowerCase()] = value.toString();
|
||||
},
|
||||
header: (header, value) => {
|
||||
header: (header: string, value: { toString: () => any; }) => {
|
||||
headers[header.toLowerCase()] = value.toString();
|
||||
},
|
||||
write: (_content) => {
|
||||
write: (_content: any) => {
|
||||
_content && content.push(_content);
|
||||
},
|
||||
end: (_content) => {
|
||||
end: (_content: any) => {
|
||||
_content && content.push(_content);
|
||||
resolve([status, headers, content.join('')]);
|
||||
},
|
||||
};
|
||||
});
|
||||
await minify(mockRequest, mockResponse);
|
||||
await _minify(mockRequest, mockResponse);
|
||||
return await p;
|
||||
};
|
||||
|
||||
const requestURIs = (locations, method, headers, callback) => {
|
||||
const _requestURIs = (locations: any[], method: any, headers: {
|
||||
[x: string]:
|
||||
/**
|
||||
* This Module manages all /minified/* requests. It controls the
|
||||
* minification && compression of Javascript and CSS.
|
||||
*/
|
||||
/*
|
||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
any;
|
||||
}, callback: (arg0: any[], arg1: any[], arg2: any[]) => void) => {
|
||||
Promise.all(locations.map(async (loc) => {
|
||||
try {
|
||||
return await requestURI(loc, method, headers);
|
||||
} catch (err) {
|
||||
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
|
||||
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
|
||||
// @ts-ignore
|
||||
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
|
||||
return [500, headers, ''];
|
||||
}
|
||||
})).then((responses) => {
|
||||
// @ts-ignore
|
||||
const statuss = responses.map((x) => x[0]);
|
||||
// @ts-ignore
|
||||
const headerss = responses.map((x) => x[1]);
|
||||
// @ts-ignore
|
||||
const contentss = responses.map((x) => x[2]);
|
||||
callback(statuss, headerss, contentss);
|
||||
});
|
||||
|
@ -119,11 +145,12 @@ const compatPaths = {
|
|||
* @param req the Express request
|
||||
* @param res the Express response
|
||||
*/
|
||||
const minify = async (req, res) => {
|
||||
const _minify = async (req:any, res:any) => {
|
||||
let filename = req.params.filename;
|
||||
try {
|
||||
filename = sanitizePathname(filename);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
|
||||
res.writeHead(404, {});
|
||||
res.end();
|
||||
|
@ -131,6 +158,7 @@ const minify = async (req, res) => {
|
|||
}
|
||||
|
||||
// Backward compatibility for plugins that require() files from old paths.
|
||||
// @ts-ignore
|
||||
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
|
||||
if (newLocation != null) {
|
||||
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
|
||||
|
@ -193,7 +221,7 @@ const minify = async (req, res) => {
|
|||
res.writeHead(200, {});
|
||||
res.end();
|
||||
} else if (req.method === 'GET') {
|
||||
const content = await getFileCompressed(filename, contentType);
|
||||
const content = await getFileCompressed(filename, contentType as string);
|
||||
res.header('Content-Type', contentType);
|
||||
res.writeHead(200, {});
|
||||
res.write(content);
|
||||
|
@ -205,7 +233,7 @@ const minify = async (req, res) => {
|
|||
};
|
||||
|
||||
// Check for the existance of the file and get the last modification date.
|
||||
const statFile = async (filename, dirStatLimit) => {
|
||||
const statFile = async (filename: string, dirStatLimit: number):Promise<(any | boolean)[]> => {
|
||||
/*
|
||||
* The only external call to this function provides an explicit value for
|
||||
* dirStatLimit: this check could be removed.
|
||||
|
@ -221,6 +249,7 @@ const statFile = async (filename, dirStatLimit) => {
|
|||
try {
|
||||
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
|
||||
// Stat the directory instead.
|
||||
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
|
||||
|
@ -234,69 +263,64 @@ const statFile = async (filename, dirStatLimit) => {
|
|||
|
||||
let contentCache = new Map();
|
||||
|
||||
const getFileCompressed = async (filename, contentType) => {
|
||||
const getFileCompressed = async (filename: any, contentType: string) => {
|
||||
if (contentCache.has(filename)) {
|
||||
return contentCache.get(filename);
|
||||
}
|
||||
let content = await getFile(filename);
|
||||
let content: Buffer|string = await getFile(filename);
|
||||
if (!content || !settings.minify) {
|
||||
return content;
|
||||
} else if (contentType === 'application/javascript') {
|
||||
return await new Promise((resolve) => {
|
||||
threadsPool.queue(async ({compressJS}) => {
|
||||
return await new Promise(async (resolve) => {
|
||||
try {
|
||||
logger.info('Compress JS file %s.', filename);
|
||||
|
||||
content = content.toString();
|
||||
try {
|
||||
logger.info('Compress JS file %s.', filename);
|
||||
|
||||
content = content.toString();
|
||||
const compressResult = await compressJS(content);
|
||||
|
||||
if (compressResult.error) {
|
||||
console.error(`Error compressing JS (${filename}) using terser`, compressResult.error);
|
||||
} else {
|
||||
content = compressResult.code.toString(); // Convert content obj code to string
|
||||
}
|
||||
let compressResult: TransformResult<{ minify: boolean }>
|
||||
compressResult = await compressJS(content);
|
||||
content = compressResult.code.toString(); // Convert content obj code to string
|
||||
} catch (error) {
|
||||
console.error('getFile() returned an error in ' +
|
||||
`getFileCompressed(${filename}, ${contentType}): ${error}`);
|
||||
console.error(`Error compressing JS (${filename}) using esbuild`, error);
|
||||
}
|
||||
contentCache.set(filename, content);
|
||||
resolve(content);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getFile() returned an error in ' +
|
||||
`getFileCompressed(${filename}, ${contentType}): ${error}`);
|
||||
}
|
||||
contentCache.set(filename, content);
|
||||
resolve(content);
|
||||
});
|
||||
} else if (contentType === 'text/css') {
|
||||
return await new Promise((resolve) => {
|
||||
threadsPool.queue(async ({compressCSS}) => {
|
||||
return await new Promise(async (resolve) => {
|
||||
try {
|
||||
logger.info('Compress CSS file %s.', filename);
|
||||
|
||||
try {
|
||||
logger.info('Compress CSS file %s.', filename);
|
||||
|
||||
const compressResult = await compressCSS(path.resolve(ROOT_DIR,filename));
|
||||
|
||||
if (compressResult.error) {
|
||||
console.error(`Error compressing CSS (${filename}) using terser`, compressResult.error);
|
||||
} else {
|
||||
content = compressResult
|
||||
}
|
||||
content = await compressCSS(path.resolve(ROOT_DIR, filename));
|
||||
} catch (error) {
|
||||
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
||||
}
|
||||
contentCache.set(filename, content);
|
||||
resolve(content);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('getFile() returned an error in ' +
|
||||
`getFileCompressed(${filename}, ${contentType}): ${e}`);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
contentCache.set(filename, content);
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
const getFile = async (filename) => {
|
||||
const getFile = async (filename: any) => {
|
||||
return await fs.readFile(path.resolve(ROOT_DIR, filename));
|
||||
};
|
||||
|
||||
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
|
||||
export const minify = (req:any, res:any, next:Function) => _minify(req, res).catch((err) => next(err || new Error(err)));
|
||||
|
||||
exports.requestURIs = requestURIs;
|
||||
export const requestURIs = _requestURIs;
|
||||
|
||||
exports.shutdown = async (hookName, context) => {
|
||||
await threadsPool.terminate();
|
||||
export const shutdown = async (hookName: string, context:any) => {
|
||||
contentCache = new Map();
|
||||
};
|
|
@ -3,14 +3,13 @@
|
|||
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
||||
*/
|
||||
|
||||
import {expose} from 'threads'
|
||||
import {build, transform} from 'esbuild';
|
||||
|
||||
/*
|
||||
* Minify JS content
|
||||
* @param {string} content - JS content to minify
|
||||
*/
|
||||
const compressJS = async (content) => {
|
||||
export const compressJS = async (content: string) => {
|
||||
return await transform(content, {minify: true});
|
||||
}
|
||||
|
||||
|
@ -19,7 +18,7 @@ const compressJS = async (content) => {
|
|||
* @param {string} filename - name of the file
|
||||
* @param {string} ROOT_DIR - the root dir of Etherpad
|
||||
*/
|
||||
const compressCSS = async (content) => {
|
||||
export const compressCSS = async (content: string) => {
|
||||
const transformedCSS = await build(
|
||||
{
|
||||
entryPoints: [content],
|
||||
|
@ -41,8 +40,3 @@ const compressCSS = async (content) => {
|
|||
)
|
||||
return transformedCSS.outputFiles[0].text
|
||||
};
|
||||
|
||||
expose({
|
||||
compressJS: compressJS,
|
||||
compressCSS,
|
||||
});
|
|
@ -107,6 +107,7 @@ exports.ttl = {
|
|||
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
|
||||
}
|
||||
|
||||
exports.updateServer = "https://static.etherpad.org"
|
||||
|
||||
|
||||
/*
|
||||
|
@ -169,11 +170,11 @@ exports.authenticationMethod = 'sso'
|
|||
/*
|
||||
* The Type of the database
|
||||
*/
|
||||
exports.dbType = 'dirty';
|
||||
exports.dbType = 'rustydb';
|
||||
/**
|
||||
* This setting is passed with dbType to ueberDB to set up the database
|
||||
*/
|
||||
exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
|
||||
exports.dbSettings = {filename: path.join(exports.root, 'var/rusty.db')};
|
||||
|
||||
/**
|
||||
* The default Text of a new pad
|
||||
|
@ -379,6 +380,14 @@ exports.sso = {
|
|||
*/
|
||||
exports.showSettingsInAdminPage = true;
|
||||
|
||||
/*
|
||||
* Settings for cleanup of pads
|
||||
*/
|
||||
exports.cleanup = {
|
||||
enabled: false,
|
||||
keepRevisions: 100,
|
||||
}
|
||||
|
||||
/*
|
||||
* By default, when caret is moved out of viewport, it scrolls the minimum
|
||||
* height needed to make this line visible.
|
||||
|
@ -837,7 +846,7 @@ exports.reloadSettings = () => {
|
|||
exports.skinName = 'colibris';
|
||||
}
|
||||
|
||||
if (!exports.socketTransportProtocols.includes("websocket") || exports.socketTransportProtocols.includes("polling")) {
|
||||
if (!exports.socketTransportProtocols.includes("websocket") || !exports.socketTransportProtocols.includes("polling")) {
|
||||
logger.warn("Invalid socketTransportProtocols setting. Please check out settings.json.template and update your settings.json. Falling back to the default ['websocket', 'polling'].");
|
||||
exports.socketTransportProtocols = ['websocket', 'polling'];
|
||||
}
|
||||
|
@ -941,6 +950,12 @@ exports.reloadSettings = () => {
|
|||
logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`);
|
||||
}
|
||||
|
||||
if (exports.dbType === 'rustydb' || exports.dbType === "sqlite") {
|
||||
exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename);
|
||||
logger.warn(`File location: ${exports.dbSettings.filename}`);
|
||||
}
|
||||
|
||||
|
||||
if (exports.ip === '') {
|
||||
// using Unix socket for connectivity
|
||||
logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue