Merge branch 'develop'

This commit is contained in:
Richard Hansen 2021-02-15 12:47:33 -05:00
commit 113df1f186
310 changed files with 11226 additions and 21462 deletions

View file

@ -17,6 +17,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Install libreoffice - name: Install libreoffice
run: | run: |
sudo add-apt-repository -y ppa:libreoffice/ppa sudo add-apt-repository -y ppa:libreoffice/ppa
@ -24,11 +28,11 @@ jobs:
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
- name: Install all dependencies and symlink for ep_etherpad-lite - name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh run: src/bin/installDeps.sh
# configures some settings and runs npm run test # configures some settings and runs npm run test
- name: Run the backend tests - name: Run the backend tests
run: tests/frontend/travis/runnerBackend.sh run: src/tests/frontend/travis/runnerBackend.sh
withplugins: withplugins:
# run on pushes to any branch # run on pushes to any branch
@ -43,18 +47,43 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Install libreoffice - name: Install libreoffice
run: | run: |
sudo add-apt-repository -y ppa:libreoffice/ppa sudo add-apt-repository -y ppa:libreoffice/ppa
sudo apt update sudo apt update
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
- name: Install all dependencies and symlink for ep_etherpad-lite - name: Install Etherpad plugins
run: bin/installDeps.sh run: >
npm install
ep_align
ep_author_hover
ep_cursortrace
ep_font_size
ep_hash_auth
ep_headings2
ep_image_upload
ep_markdown
ep_readonly_guest
ep_set_title_on_pad
ep_spellcheck
ep_subscript_and_superscript
ep_table_of_contents
- name: Install etherpad plugins # This must be run after installing the plugins, otherwise npm will try to
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents # hoist common dependencies by removing them from src/node_modules and
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
# logic appears to be buggy, because it sometimes removes dependencies from
# src/node_modules but fails to add them to the top-level node_modules. Even
# if npm correctly hoists the dependencies, the hoisting seems to confuse
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
- name: Install all dependencies and symlink for ep_etherpad-lite
run: src/bin/installDeps.sh
# configures some settings and runs npm run test # configures some settings and runs npm run test
- name: Run the backend tests - name: Run the backend tests
run: tests/frontend/travis/runnerBackend.sh run: src/tests/frontend/travis/runnerBackend.sh

View file

@ -21,6 +21,6 @@ jobs:
run: | run: |
docker build -t etherpad:test . docker build -t etherpad:test .
docker run -d -p 9001:9001 etherpad:test docker run -d -p 9001:9001 etherpad:test
./bin/installDeps.sh ./src/bin/installDeps.sh
sleep 3 # delay for startup? sleep 3 # delay for startup?
cd src && npm run test-container cd src && npm run test-container

View file

@ -0,0 +1,58 @@
name: "Frontend admin tests"
on: [push]
jobs:
withplugins:
name: with plugins
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Run sauce-connect-action
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: src/tests/frontend/travis/sauce_tunnel.sh
- name: Install all dependencies and symlink for ep_etherpad-lite
run: src/bin/installDeps.sh
# We intentionally install a much old ep_align version to test update minor versions
- name: Install etherpad plugins
run: npm install ep_align@0.2.27
# Nuke plugin tests
- name: Install etherpad plugins
run: rm -Rf node_modules/ep_align/static/tests/*
- name: export GIT_HASH to env
id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json"
- name: Write custom settings.json that enables the Admin UI tests
run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json"
- name: Remove standard frontend test files, so only admin tests are run
run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs
- name: Run the frontend admin tests
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
src/tests/frontend/travis/adminrunner.sh

View file

@ -11,16 +11,20 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Run sauce-connect-action - name: Run sauce-connect-action
shell: bash shell: bash
env: env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: tests/frontend/travis/sauce_tunnel.sh run: src/tests/frontend/travis/sauce_tunnel.sh
- name: Install all dependencies and symlink for ep_etherpad-lite - name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh run: src/bin/installDeps.sh
- name: export GIT_HASH to env - name: export GIT_HASH to env
id: environment id: environment
@ -37,7 +41,7 @@ jobs:
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }} GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: | run: |
tests/frontend/travis/runner.sh src/tests/frontend/travis/runner.sh
withplugins: withplugins:
name: with plugins name: with plugins
@ -47,19 +51,44 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Run sauce-connect-action - name: Run sauce-connect-action
shell: bash shell: bash
env: env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: tests/frontend/travis/sauce_tunnel.sh run: src/tests/frontend/travis/sauce_tunnel.sh
- name: Install Etherpad plugins
run: >
npm install
ep_align
ep_author_hover
ep_cursortrace
ep_font_size
ep_hash_auth
ep_headings2
ep_image_upload
ep_markdown
ep_readonly_guest
ep_set_title_on_pad
ep_spellcheck
ep_subscript_and_superscript
ep_table_of_contents
# This must be run after installing the plugins, otherwise npm will try to
# hoist common dependencies by removing them from src/node_modules and
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
# logic appears to be buggy, because it sometimes removes dependencies from
# src/node_modules but fails to add them to the top-level node_modules. Even
# if npm correctly hoists the dependencies, the hoisting seems to confuse
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
- name: Install all dependencies and symlink for ep_etherpad-lite - name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh run: src/bin/installDeps.sh
- name: Install etherpad plugins
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents ep_set_title_on_pad
- name: export GIT_HASH to env - name: export GIT_HASH to env
id: environment id: environment
@ -68,9 +97,12 @@ jobs:
- name: Write custom settings.json with loglevel WARN - name: Write custom settings.json with loglevel WARN
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json" run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json"
- name: Write custom settings.json that enables the Admin UI tests
run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json"
# XXX we should probably run all tests, because plugins could effect their results # XXX we should probably run all tests, because plugins could effect their results
- name: Remove standard frontend test files, so only plugin tests are run - name: Remove standard frontend test files, so only plugin tests are run
run: rm tests/frontend/specs/* run: rm src/tests/frontend/specs/*
- name: Run the frontend tests - name: Run the frontend tests
shell: bash shell: bash
@ -80,4 +112,4 @@ jobs:
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }} TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }} GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: | run: |
tests/frontend/travis/runner.sh src/tests/frontend/travis/runner.sh

View file

@ -17,6 +17,10 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Install lockfile-lint - name: Install lockfile-lint
run: npm install lockfile-lint run: npm install lockfile-lint

View file

@ -17,14 +17,18 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Install all dependencies and symlink for ep_etherpad-lite - name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh run: src/bin/installDeps.sh
- name: Install etherpad-load-test - name: Install etherpad-load-test
run: sudo npm install -g etherpad-load-test run: sudo npm install -g etherpad-load-test
- name: Run load test - name: Run load test
run: tests/frontend/travis/runnerLoadTest.sh run: src/tests/frontend/travis/runnerLoadTest.sh
withplugins: withplugins:
# run on pushes to any branch # run on pushes to any branch
@ -39,15 +43,39 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install all dependencies and symlink for ep_etherpad-lite - uses: actions/setup-node@v2
run: bin/installDeps.sh with:
node-version: 12
- name: Install etherpad-load-test - name: Install etherpad-load-test
run: sudo npm install -g etherpad-load-test run: sudo npm install -g etherpad-load-test
- name: Install etherpad plugins - name: Install etherpad plugins
run: npm install ep_align ep_author_hover ep_cursortrace ep_font_size ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_spellcheck ep_subscript_and_superscript ep_table_of_contents run: >
npm install
ep_align
ep_author_hover
ep_cursortrace
ep_font_size
ep_hash_auth
ep_headings2
ep_markdown
ep_readonly_guest
ep_set_title_on_pad
ep_spellcheck
ep_subscript_and_superscript
ep_table_of_contents
# This must be run after installing the plugins, otherwise npm will try to
# hoist common dependencies by removing them from src/node_modules and
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
# logic appears to be buggy, because it sometimes removes dependencies from
# src/node_modules but fails to add them to the top-level node_modules. Even
# if npm correctly hoists the dependencies, the hoisting seems to confuse
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
- name: Install all dependencies and symlink for ep_etherpad-lite
run: src/bin/installDeps.sh
# configures some settings and runs npm run test # configures some settings and runs npm run test
- name: Run load test - name: Run load test
run: tests/frontend/travis/runnerLoadTest.sh run: src/tests/frontend/travis/runnerLoadTest.sh

View file

@ -16,14 +16,18 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: docker network - name: docker network
run: docker network create --subnet=172.23.42.0/16 ep_net run: docker network create --subnet=172.23.42.0/16 ep_net
- name: build docker image - name: build docker image
run: | run: |
docker build -f Dockerfile -t epl-debian-slim . docker build -f Dockerfile -t epl-debian-slim .
docker build -f tests/ratelimit/Dockerfile.nginx -t nginx-latest . docker build -f src/tests/ratelimit/Dockerfile.nginx -t nginx-latest .
docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip . docker build -f src/tests/ratelimit/Dockerfile.anotherip -t anotherip .
- name: run docker images - name: run docker images
run: | run: |
docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim & docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &
@ -31,9 +35,9 @@ jobs:
docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip
- name: install dependencies and create symlink for ep_etherpad-lite - name: install dependencies and create symlink for ep_etherpad-lite
run: bin/installDeps.sh run: src/bin/installDeps.sh
- name: run rate limit test - name: run rate limit test
run: | run: |
cd tests/ratelimit cd src/tests/ratelimit
./testlimits.sh ./testlimits.sh

10
.gitignore vendored
View file

@ -3,11 +3,8 @@ node_modules
!settings.json.template !settings.json.template
APIKEY.txt APIKEY.txt
SESSIONKEY.txt SESSIONKEY.txt
bin/abiword.exe
bin/node.exe
etherpad-lite-win.zip etherpad-lite-win.zip
var/dirty.db var/dirty.db
bin/convertSettings.json
*~ *~
*.patch *.patch
npm-debug.log npm-debug.log
@ -15,9 +12,12 @@ npm-debug.log
.ep_initialized .ep_initialized
*.crt *.crt
*.key *.key
bin/etherpad-1.deb
credentials.json credentials.json
out/ out/
.nyc_output .nyc_output
./package-lock.json
.idea .idea
/package-lock.json
/src/bin/abiword.exe
/src/bin/convertSettings.json
/src/bin/etherpad-1.deb
/src/bin/node.exe

View file

@ -18,6 +18,11 @@ _set_loglevel_warn: &set_loglevel_warn |
settings.json.template >settings.json.template.new && settings.json.template >settings.json.template.new &&
mv settings.json.template.new settings.json.template mv settings.json.template.new settings.json.template
_enable_admin_tests: &enable_admin_tests |
sed -e 's/"enableAdminUITests": false/"enableAdminUITests": true,\n"users":{"admin":{"password":"changeme","is_admin":true}}/' \
settings.json.template >settings.json.template.new &&
mv settings.json.template.new settings.json.template
_install_libreoffice: &install_libreoffice >- _install_libreoffice: &install_libreoffice >-
sudo add-apt-repository -y ppa:libreoffice/ppa && sudo add-apt-repository -y ppa:libreoffice/ppa &&
sudo apt-get update && sudo apt-get update &&
@ -46,19 +51,20 @@ jobs:
name: "Test the Frontend without Plugins" name: "Test the Frontend without Plugins"
install: install:
- *set_loglevel_warn - *set_loglevel_warn
- "tests/frontend/travis/sauce_tunnel.sh" - *enable_admin_tests
- "bin/installDeps.sh" - "src/tests/frontend/travis/sauce_tunnel.sh"
- "src/bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
script: script:
- "./tests/frontend/travis/runner.sh" - "./src/tests/frontend/travis/runner.sh"
- name: "Run the Backend tests without Plugins" - name: "Run the Backend tests without Plugins"
install: install:
- *install_libreoffice - *install_libreoffice
- *set_loglevel_warn - *set_loglevel_warn
- "bin/installDeps.sh" - "src/bin/installDeps.sh"
- "cd src && npm install && cd -" - "cd src && npm install && cd -"
script: script:
- "tests/frontend/travis/runnerBackend.sh" - "src/tests/frontend/travis/runnerBackend.sh"
- name: "Test the Dockerfile" - name: "Test the Dockerfile"
install: install:
- "cd src && npm install && cd -" - "cd src && npm install && cd -"
@ -69,24 +75,25 @@ jobs:
- name: "Load test Etherpad without Plugins" - name: "Load test Etherpad without Plugins"
install: install:
- *set_loglevel_warn - *set_loglevel_warn
- "bin/installDeps.sh" - "src/bin/installDeps.sh"
- "cd src && npm install && cd -" - "cd src && npm install && cd -"
- "npm install -g etherpad-load-test" - "npm install -g etherpad-load-test"
script: script:
- "tests/frontend/travis/runnerLoadTest.sh" - "src/tests/frontend/travis/runnerLoadTest.sh"
# we can only frontend tests from the ether/ organization and not from forks. # we can only frontend tests from the ether/ organization and not from forks.
# To request tests to be run ask a maintainer to fork your repo to ether/ # To request tests to be run ask a maintainer to fork your repo to ether/
- if: fork = false - if: fork = false
name: "Test the Frontend Plugins only" name: "Test the Frontend Plugins only"
install: install:
- *set_loglevel_warn - *set_loglevel_warn
- "tests/frontend/travis/sauce_tunnel.sh" - *enable_admin_tests
- "bin/installDeps.sh" - "src/tests/frontend/travis/sauce_tunnel.sh"
- "rm tests/frontend/specs/*" - "src/bin/installDeps.sh"
- "rm src/tests/frontend/specs/*"
- *install_plugins - *install_plugins
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)" - "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
script: script:
- "./tests/frontend/travis/runner.sh" - "./src/tests/frontend/travis/runner.sh"
- name: "Lint test package-lock.json" - name: "Lint test package-lock.json"
install: install:
- "npm install lockfile-lint" - "npm install lockfile-lint"
@ -96,11 +103,11 @@ jobs:
install: install:
- *install_libreoffice - *install_libreoffice
- *set_loglevel_warn - *set_loglevel_warn
- "bin/installDeps.sh" - "src/bin/installDeps.sh"
- *install_plugins - *install_plugins
- "cd src && npm install && cd -" - "cd src && npm install && cd -"
script: script:
- "tests/frontend/travis/runnerBackend.sh" - "src/tests/frontend/travis/runnerBackend.sh"
- name: "Test the Dockerfile" - name: "Test the Dockerfile"
install: install:
- "cd src && npm install && cd -" - "cd src && npm install && cd -"
@ -111,24 +118,24 @@ jobs:
- name: "Load test Etherpad with Plugins" - name: "Load test Etherpad with Plugins"
install: install:
- *set_loglevel_warn - *set_loglevel_warn
- "bin/installDeps.sh" - "src/bin/installDeps.sh"
- *install_plugins - *install_plugins
- "cd src && npm install && cd -" - "cd src && npm install && cd -"
- "npm install -g etherpad-load-test" - "npm install -g etherpad-load-test"
script: script:
- "tests/frontend/travis/runnerLoadTest.sh" - "src/tests/frontend/travis/runnerLoadTest.sh"
- name: "Test rate limit" - name: "Test rate limit"
install: install:
- "docker network create --subnet=172.23.42.0/16 ep_net" - "docker network create --subnet=172.23.42.0/16 ep_net"
- "docker build -f Dockerfile -t epl-debian-slim ." - "docker build -f Dockerfile -t epl-debian-slim ."
- "docker build -f tests/ratelimit/Dockerfile.nginx -t nginx-latest ." - "docker build -f src/tests/ratelimit/Dockerfile.nginx -t nginx-latest ."
- "docker build -f tests/ratelimit/Dockerfile.anotherip -t anotherip ." - "docker build -f src/tests/ratelimit/Dockerfile.anotherip -t anotherip ."
- "docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest" - "docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest"
- "docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &" - "docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &"
- "docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip" - "docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip"
- "./bin/installDeps.sh" - "./src/bin/installDeps.sh"
script: script:
- "cd tests/ratelimit && bash testlimits.sh" - "cd src/tests/ratelimit && bash testlimits.sh"
notifications: notifications:
irc: irc:

View file

@ -1,3 +1,79 @@
# 1.8.8
### Security patches
* EJS has been updated to 3.1.6 to mitigate an Arbitrary Code Injection
### Compatibility changes
* Node.js 10.17.0 or newer is now required.
* The `bin/` and `tests/` directories were moved under `src/`. Symlinks were
added at the old locations to hopefully avoid breaking user scripts and other
tools.
* Dependencies are now installed with the `--no-optional` flag to speed
installation. Optional dependencies such as `sqlite3` must now be manually
installed (e.g., `(cd src && npm i sqlite3)`).
* Socket.IO messages are now limited to 10K bytes to make denial of service
attacks more difficult. This may cause issues when pasting large amounts of
text or with plugins that send large messages (e.g., `ep_image_upload`). You
can change the limit via `settings.json`; see `socketIo.maxHttpBufferSize`.
* The top-level `package.json` file, added in v1.8.7, has been removed due to
problematic npm behavior. Whenever you install a plugin you will see the
following benign warnings that can be safely ignored:
```
npm WARN saveError ENOENT: no such file or directory, open '.../package.json'
npm WARN enoent ENOENT: no such file or directory, open '.../package.json'
npm WARN develop No description
npm WARN develop No repository field.
npm WARN develop No README data
npm WARN develop No license field.
```
### Notable enhancements
* You can now generate a link to a specific line number in a pad. Appending
`#L10` to a pad URL will cause your browser to scroll down to line 10.
* Database performance is significantly improved.
* Admin UI now has test coverage in CI. (The tests are not enabled by default;
see `settings.json`.)
* New stats/metrics: `activePads`, `httpStartTime`, `lastDisconnected`,
`memoryUsageHeap`.
* Improved import UX.
* Browser caching improvements.
* Users can now pick absolute white (`#fff`) as their color.
* The `settings.json` template used for Docker images has new variables for
controlling rate limiting.
* Admin UI now has test coverage in CI. (The tests are not enabled by default
because the admin password is required; see `settings.json`.)
* For plugin authors:
* New `callAllSerial()` function that invokes hook functions like `callAll()`
except it supports asynchronous hook functions.
* `callFirst()` and `aCallFirst()` now support the same wide range of hook
function behaviors that `callAll()`, `aCallAll()`, and `callAllSerial()`
support. Also, they now warn when a hook function misbehaves.
* The following server-side hooks now support asynchronous hook functions:
`expressConfigure`, `expressCreateServer`, `padCopy`, `padRemove`
* Backend tests for plugins can now use the
[`ep_etherpad-lite/tests/backend/common`](src/tests/backend/common.js)
module to start the server and simplify API access.
* The `checkPlugins.js` script now automatically adds GitHub CI test coverage
badges for backend tests and npm publish.
### Notable fixes
* Enter key now stays in focus when inserted at bottom of viewport.
* Numbering for ordered list items now properly increments when exported to
text.
* Suppressed benign socket.io connection errors
* Interface no longer loses color variants on disconnect/reconnect event.
* General code quality is further significantly improved.
* Restarting Etherpad via `/admin` actions is more robust.
* Improved reliability of server shutdown and restart.
* No longer error if no buttons are visible.
* For plugin authors:
* Fixed `collectContentLineText` return value handling.
# 1.8.7 # 1.8.7
### Compatibility-breaking changes ### Compatibility-breaking changes
* **IMPORTANT:** It is no longer possible to protect a group pad with a * **IMPORTANT:** It is no longer possible to protect a group pad with a
@ -40,7 +116,7 @@
content in `.etherpad` exports content in `.etherpad` exports
* New `expressCloseServer` hook to close Express when required * New `expressCloseServer` hook to close Express when required
* The `padUpdate` hook context now includes `revs` and `changeset` * The `padUpdate` hook context now includes `revs` and `changeset`
* `checkPlugins.js` has various improvements to help plugin developers * `checkPlugin.js` has various improvements to help plugin developers
* The HTTP request object (and therefore the express-session state) is now * The HTTP request object (and therefore the express-session state) is now
accessible from within most `eejsBlock_*` hooks accessible from within most `eejsBlock_*` hooks
* Users without a `password` or `hash` property in `settings.json` are no longer * Users without a `password` or `hash` property in `settings.json` are no longer
@ -114,7 +190,7 @@
* MINOR: Fix ?showChat URL param issue * MINOR: Fix ?showChat URL param issue
* MINOR: Issue where timeslider URI fails to be correct if padID is numeric * MINOR: Issue where timeslider URI fails to be correct if padID is numeric
* MINOR: Include prompt for clear authorship when entire document is selected * MINOR: Include prompt for clear authorship when entire document is selected
* MINOR: Include full document aText every 100 revisions to make pad restoration on database curruption achievable * MINOR: Include full document aText every 100 revisions to make pad restoration on database corruption achievable
* MINOR: Several Colibris CSS fixes * MINOR: Several Colibris CSS fixes
* MINOR: Use mime library for mime types instead of hard-coded. * MINOR: Use mime library for mime types instead of hard-coded.
* MINOR: Don't show "new pad button" if instance is read only * MINOR: Don't show "new pad button" if instance is read only
@ -364,7 +440,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`.
# 1.5.3 # 1.5.3
* NEW: Accessibility support for Screen readers, includes new fonts and keyboard shortcuts * NEW: Accessibility support for Screen readers, includes new fonts and keyboard shortcuts
* NEW: API endpoint for Append Chat Message and Chat Backend Tests * NEW: API endpoint for Append Chat Message and Chat Backend Tests
* NEW: Error messages displayed on load are included in Default Pad Text (can be supressed) * NEW: Error messages displayed on load are included in Default Pad Text (can be suppressed)
* NEW: Content Collector can handle key values * NEW: Content Collector can handle key values
* NEW: getAttributesOnPosition Method * NEW: getAttributesOnPosition Method
* FIX: Firefox keeps attributes (bold etc) on cut/copy -> paste * FIX: Firefox keeps attributes (bold etc) on cut/copy -> paste
@ -433,7 +509,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`.
* Fix: Timeslider UI Fix * Fix: Timeslider UI Fix
* Fix: Remove Dokuwiki * Fix: Remove Dokuwiki
* Fix: Remove long paths from windows build (stops error during extract) * Fix: Remove long paths from windows build (stops error during extract)
* Fix: Various globals remvoed * Fix: Various globals removed
* Fix: Move all scripts into bin/ * Fix: Move all scripts into bin/
* Fix: Various CSS bugfixes for Mobile devices * Fix: Various CSS bugfixes for Mobile devices
* Fix: Overflow Toolbar * Fix: Overflow Toolbar
@ -509,7 +585,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`.
* FIX: HTML import (don't crash on malformed or blank HTML input; strip title out of html during import) * FIX: HTML import (don't crash on malformed or blank HTML input; strip title out of html during import)
* FIX: check if uploaded file only contains ascii chars when abiword disabled * FIX: check if uploaded file only contains ascii chars when abiword disabled
* FIX: Plugin search in /admin/plugins * FIX: Plugin search in /admin/plugins
* FIX: Don't create new pad if a non-existant read-only pad is accessed * FIX: Don't create new pad if a non-existent read-only pad is accessed
* FIX: Drop messages from unknown connections (would lead to a crash after a restart) * FIX: Drop messages from unknown connections (would lead to a crash after a restart)
* FIX: API: fix createGroupFor endpoint, if mapped group is deleted * FIX: API: fix createGroupFor endpoint, if mapped group is deleted
* FIX: Import form for other locales * FIX: Import form for other locales
@ -526,7 +602,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`.
* NEW: Bump log4js for improved logging * NEW: Bump log4js for improved logging
* Fix: Remove URL schemes which don't have RFC standard * Fix: Remove URL schemes which don't have RFC standard
* Fix: Fix safeRun subsequent restarts issue * Fix: Fix safeRun subsequent restarts issue
* Fix: Allow safeRun to pass arguements to run.sh * Fix: Allow safeRun to pass arguments to run.sh
* Fix: Include script for more efficient import * Fix: Include script for more efficient import
* Fix: Fix sysv comptibile script * Fix: Fix sysv comptibile script
* Fix: Fix client side changeset spamming * Fix: Fix client side changeset spamming
@ -565,7 +641,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`.
* Fix: Support Node 0.10 * Fix: Support Node 0.10
* Fix: Log HTTP on DEBUG log level * Fix: Log HTTP on DEBUG log level
* Fix: Server wont crash on import fails on 0 file import. * Fix: Server wont crash on import fails on 0 file import.
* Fix: Import no longer fails consistantly * Fix: Import no longer fails consistently
* Fix: Language support for non existing languages * Fix: Language support for non existing languages
* Fix: Mobile support for chat notifications are now usable * Fix: Mobile support for chat notifications are now usable
* Fix: Re-Enable Editbar buttons on reconnect * Fix: Re-Enable Editbar buttons on reconnect
@ -597,7 +673,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`.
* NEW: Admin dashboard mobile device support and new hooks for Admin dashboard * NEW: Admin dashboard mobile device support and new hooks for Admin dashboard
* NEW: Get current API version from API * NEW: Get current API version from API
* NEW: CLI script to delete pads * NEW: CLI script to delete pads
* Fix: Automatic client reconnection on disonnect * Fix: Automatic client reconnection on disconnect
* Fix: Text Export indentation now supports multiple indentations * Fix: Text Export indentation now supports multiple indentations
* Fix: Bugfix getChatHistory API method * Fix: Bugfix getChatHistory API method
* Fix: Stop Chrome losing caret after paste is texted * Fix: Stop Chrome losing caret after paste is texted
@ -617,7 +693,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`.
* Fix: Stop Opera browser inserting two new lines on enter keypress * Fix: Stop Opera browser inserting two new lines on enter keypress
* Fix: Stop timeslider from showing NaN on pads with only one revision * Fix: Stop timeslider from showing NaN on pads with only one revision
* Other: Allow timeslider tests to run and provide & fix various other frontend-tests * Other: Allow timeslider tests to run and provide & fix various other frontend-tests
* Other: Begin dropping referene to Lite. Etherpad Lite is now named "Etherpad" * Other: Begin dropping reference to Lite. Etherpad Lite is now named "Etherpad"
* Other: Update to latest jQuery * Other: Update to latest jQuery
* Other: Change loading message asking user to please wait on first build * Other: Change loading message asking user to please wait on first build
* Other: Allow etherpad to use global npm installation (Safe since node 6.3) * Other: Allow etherpad to use global npm installation (Safe since node 6.3)

View file

@ -35,7 +35,7 @@ WORKDIR /opt/etherpad-lite
COPY --chown=etherpad:0 ./ ./ COPY --chown=etherpad:0 ./ ./
# install node dependencies for Etherpad # install node dependencies for Etherpad
RUN bin/installDeps.sh && \ RUN src/bin/installDeps.sh && \
rm -rf ~/.npm/_cacache rm -rf ~/.npm/_cacache
# Install the plugins, if ETHERPAD_PLUGINS is not empty. # Install the plugins, if ETHERPAD_PLUGINS is not empty.

View file

@ -9,8 +9,8 @@ UNAME := $(shell uname -s)
ensure_marked_is_installed: ensure_marked_is_installed:
set -eu; \ set -eu; \
hash npm; \ hash npm; \
if [ $(shell npm list --prefix bin/doc >/dev/null 2>/dev/null; echo $$?) -ne "0" ]; then \ if [ $(shell npm list --prefix src/bin/doc >/dev/null 2>/dev/null; echo $$?) -ne "0" ]; then \
npm ci --prefix=bin/doc; \ npm ci --prefix=src/bin/doc; \
fi fi
docs: ensure_marked_is_installed $(outdoc_files) $(docassets) docs: ensure_marked_is_installed $(outdoc_files) $(docassets)
@ -21,7 +21,7 @@ out/doc/assets/%: doc/assets/%
out/doc/%.html: doc/%.md out/doc/%.html: doc/%.md
mkdir -p $(@D) mkdir -p $(@D)
node bin/doc/generate.js --format=html --template=doc/template.html $< > $@ node src/bin/doc/generate.js --format=html --template=doc/template.html $< > $@
ifeq ($(UNAME),Darwin) ifeq ($(UNAME),Darwin)
sed -i '' 's/__VERSION__/${VERSION}/' $@ sed -i '' 's/__VERSION__/${VERSION}/' $@
else else

View file

@ -13,7 +13,7 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane
# Installation # Installation
## Requirements ## Requirements
- `nodejs` >= **10.13.0**. - `nodejs` >= **10.17.0**.
## GNU/Linux and other UNIX-like systems ## GNU/Linux and other UNIX-like systems
@ -21,19 +21,22 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane
``` ```
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt install -y nodejs sudo apt install -y nodejs
git clone --branch master https://github.com/ether/etherpad-lite.git && cd etherpad-lite && bin/run.sh git clone --branch master https://github.com/ether/etherpad-lite.git &&
cd etherpad-lite &&
src/bin/run.sh
``` ```
### Manual install ### Manual install
You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.13.0**). You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.17.0**).
**As any user (we recommend creating a separate user called etherpad):** **As any user (we recommend creating a separate user called etherpad):**
1. Move to a folder where you want to install Etherpad. Clone the git repository: `git clone --branch master git://github.com/ether/etherpad-lite.git` 1. Move to a folder where you want to install Etherpad. Clone the git repository: `git clone --branch master git://github.com/ether/etherpad-lite.git`
2. Change into the new directory containing the cloned source code: `cd etherpad-lite` 2. Change into the new directory containing the cloned source code: `cd etherpad-lite`
3. run `bin/run.sh` and open <http://127.0.0.1:9001> in your browser. 3. run `src/bin/run.sh` and open <http://127.0.0.1:9001> in your browser.
To update to the latest released version, execute `git pull origin`. The next start with `bin/run.sh` will update the dependencies. To update to the latest released version, execute `git pull origin`. The next
start with `src/bin/run.sh` will update the dependencies.
[Next steps](#next-steps). [Next steps](#next-steps).
@ -53,11 +56,13 @@ You'll need [node.js](https://nodejs.org) and (optionally, though recommended) g
1. Grab the source, either 1. Grab the source, either
- download <https://github.com/ether/etherpad-lite/zipball/master> - download <https://github.com/ether/etherpad-lite/zipball/master>
- or `git clone --branch master https://github.com/ether/etherpad-lite.git` - or `git clone --branch master https://github.com/ether/etherpad-lite.git`
2. With a "Run as administrator" command prompt execute `bin\installOnWindows.bat` 2. With a "Run as administrator" command prompt execute
`src\bin\installOnWindows.bat`
Now, run `start.bat` and open <http://localhost:9001> in your browser. Now, run `start.bat` and open <http://localhost:9001> in your browser.
Update to the latest version with `git pull origin`, then run `bin\installOnWindows.bat`, again. Update to the latest version with `git pull origin`, then run
`src\bin\installOnWindows.bat`, again.
If cloning to a subdirectory within another project, you may need to do the following: If cloning to a subdirectory within another project, you may need to do the following:
@ -73,7 +78,9 @@ Find [here](doc/docker.md) information on running Etherpad in a container.
## Tweak the settings ## Tweak the settings
You can modify the settings in `settings.json`. You can modify the settings in `settings.json`.
If you need to handle multiple settings files, you can pass the path to a settings file to `bin/run.sh` using the `-s|--settings` option: this allows you to run multiple Etherpad instances from the same installation. If you need to handle multiple settings files, you can pass the path to a
settings file to `src/bin/run.sh` using the `-s|--settings` option: this allows
you to run multiple Etherpad instances from the same installation.
Similarly, `--credentials` can be used to give a settings override file, `--apikey` to give a different APIKEY.txt file and `--sessionkey` to give a non-default SESSIONKEY.txt. Similarly, `--credentials` can be used to give a settings override file, `--apikey` to give a different APIKEY.txt file and `--sessionkey` to give a non-default SESSIONKEY.txt.
**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`. **Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. For details, refer to `settings.json.template`.
Once you have access to your `/admin` section settings can be modified through the web browser. Once you have access to your `/admin` section settings can be modified through the web browser.
@ -98,7 +105,7 @@ Etherpad is very customizable through plugins. Instructions for installing theme
Run the following command in your Etherpad folder to get all of the features visible in the demo gif: Run the following command in your Etherpad folder to get all of the features visible in the demo gif:
``` ```
npm install ep_headings2 ep_markdown ep_comments_page ep_align ep_page_view ep_font_color ep_webrtc ep_embedded_hyperlinks2 npm install ep_headings2 ep_markdown ep_comments_page ep_align ep_font_color ep_webrtc ep_embedded_hyperlinks2
``` ```
## Customize the style with skin variants ## Customize the style with skin variants
@ -115,9 +122,13 @@ Documentation can be found in `doc/`.
# Development # Development
## Things you should know ## Things you should know
You can debug Etherpad using `bin/debugRun.sh`.
You can run Etherpad quickly launching `bin/fastRun.sh`. It's convenient for developers and advanced users. Be aware that it will skip the dependencies update, so remember to run `bin/installDeps.sh` after installing a new dependency or upgrading version. You can debug Etherpad using `src/bin/debugRun.sh`.
You can run Etherpad quickly launching `src/bin/fastRun.sh`. It's convenient for
developers and advanced users. Be aware that it will skip the dependencies
update, so remember to run `src/bin/installDeps.sh` after installing a new
dependency or upgrading version.
If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading). If you want to find out how Etherpad's `Easysync` works (the library that makes it really realtime), start with this [PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf) (complex, but worth reading).

1
bin Symbolic link
View file

@ -0,0 +1 @@
src/bin

View file

@ -1,91 +0,0 @@
/*
* This is a debug tool. It checks all revisions for data corruption
*/
if (process.argv.length != 2) {
console.error('Use: node bin/checkAllPads.js');
process.exit(1);
}
// load and initialize NPM
const npm = require('../src/node_modules/npm');
npm.load({}, async () => {
try {
// initialize the database
const settings = require('../src/node/utils/Settings');
const db = require('../src/node/db/DB');
await db.init();
// load modules
const Changeset = require('../src/static/js/Changeset');
const padManager = require('../src/node/db/PadManager');
// get all pads
const res = await padManager.listAllPads();
for (const padId of res.padIDs) {
const pad = await padManager.getPad(padId);
// check if the pad has a pool
if (pad.pool === undefined) {
console.error(`[${pad.id}] Missing attribute pool`);
continue;
}
// create an array with key kevisions
// key revisions always save the full pad atext
const head = pad.getHeadRevisionNumber();
const keyRevisions = [];
for (let rev = 0; rev < head; rev += 100) {
keyRevisions.push(rev);
}
// run through all key revisions
for (const keyRev of keyRevisions) {
// create an array of revisions we need till the next keyRevision or the End
const revisionsNeeded = [];
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
revisionsNeeded.push(rev);
}
// this array will hold all revision changesets
const revisions = [];
// run through all needed revisions and get them from the database
for (const revNum of revisionsNeeded) {
const revision = await db.get(`pad:${pad.id}:revs:${revNum}`);
revisions[revNum] = revision;
}
// check if the revision exists
if (revisions[keyRev] == null) {
console.error(`[${pad.id}] Missing revision ${keyRev}`);
continue;
}
// check if there is a atext in the keyRevisions
if (revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) {
console.error(`[${pad.id}] Missing atext in revision ${keyRev}`);
continue;
}
const apool = pad.pool;
let atext = revisions[keyRev].meta.atext;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
try {
const cs = revisions[rev].changeset;
atext = Changeset.applyToAText(cs, atext, apool);
} catch (e) {
console.error(`[${pad.id}] Bad changeset at revision ${i} - ${e.message}`);
}
}
}
console.log('finished');
process.exit(0);
}
} catch (err) {
console.trace(err);
process.exit(1);
}
});

View file

@ -1,92 +0,0 @@
/*
* This is a debug tool. It checks all revisions for data corruption
*/
if (process.argv.length != 3) {
console.error('Use: node bin/checkPad.js $PADID');
process.exit(1);
}
// get the padID
const padId = process.argv[2];
// load and initialize NPM;
const npm = require('../src/node_modules/npm');
npm.load({}, async () => {
try {
// initialize database
const settings = require('../src/node/utils/Settings');
const db = require('../src/node/db/DB');
await db.init();
// load modules
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const padManager = require('../src/node/db/PadManager');
const exists = await padManager.doesPadExists(padId);
if (!exists) {
console.error('Pad does not exist');
process.exit(1);
}
// get the pad
const pad = await padManager.getPad(padId);
// create an array with key revisions
// key revisions always save the full pad atext
const head = pad.getHeadRevisionNumber();
const keyRevisions = [];
for (let rev = 0; rev < head; rev += 100) {
keyRevisions.push(rev);
}
// run through all key revisions
for (const keyRev of keyRevisions) {
// create an array of revisions we need till the next keyRevision or the End
const revisionsNeeded = [];
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
revisionsNeeded.push(rev);
}
// this array will hold all revision changesets
const revisions = [];
// run through all needed revisions and get them from the database
for (const revNum of revisionsNeeded) {
const revision = await db.get(`pad:${padId}:revs:${revNum}`);
revisions[revNum] = revision;
}
// check if the pad has a pool
if (pad.pool === undefined) {
console.error('Attribute pool is missing');
process.exit(1);
}
// check if there is an atext in the keyRevisions
if (revisions[keyRev] === undefined || revisions[keyRev].meta === undefined || revisions[keyRev].meta.atext === undefined) {
console.error(`No atext in key revision ${keyRev}`);
continue;
}
const apool = pad.pool;
let atext = revisions[keyRev].meta.atext;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
try {
// console.log("check revision " + rev);
const cs = revisions[rev].changeset;
atext = Changeset.applyToAText(cs, atext, apool);
} catch (e) {
console.error(`Bad changeset at revision ${rev} - ${e.message}`);
continue;
}
}
console.log('finished');
process.exit(0);
}
} catch (e) {
console.trace(e);
process.exit(1);
}
});

View file

@ -1,111 +0,0 @@
/*
* This is a debug tool. It checks all revisions for data corruption
*/
if (process.argv.length != 3) {
console.error('Use: node bin/checkPadDeltas.js $PADID');
process.exit(1);
}
// get the padID
const padId = process.argv[2];
// load and initialize NPM;
const expect = require('expect.js');
const diff = require('diff');
var async = require('async');
const npm = require('../src/node_modules/npm');
var async = require('ep_etherpad-lite/node_modules/async');
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
npm.load({}, async () => {
try {
// initialize database
const settings = require('../src/node/utils/Settings');
const db = require('../src/node/db/DB');
await db.init();
// load modules
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const padManager = require('../src/node/db/PadManager');
const exists = await padManager.doesPadExists(padId);
if (!exists) {
console.error('Pad does not exist');
process.exit(1);
}
// get the pad
const pad = await padManager.getPad(padId);
// create an array with key revisions
// key revisions always save the full pad atext
const head = pad.getHeadRevisionNumber();
const keyRevisions = [];
for (var i = 0; i < head; i += 100) {
keyRevisions.push(i);
}
// create an array with all revisions
const revisions = [];
for (var i = 0; i <= head; i++) {
revisions.push(i);
}
let atext = Changeset.makeAText('\n');
// run trough all revisions
async.forEachSeries(revisions, (revNum, callback) => {
// console.log('Fetching', revNum)
db.db.get(`pad:${padId}:revs:${revNum}`, (err, revision) => {
if (err) return callback(err);
// check if there is a atext in the keyRevisions
if (~keyRevisions.indexOf(revNum) && (revision === undefined || revision.meta === undefined || revision.meta.atext === undefined)) {
console.error(`No atext in key revision ${revNum}`);
callback();
return;
}
try {
// console.log("check revision ", revNum);
const cs = revision.changeset;
atext = Changeset.applyToAText(cs, atext, pad.pool);
} catch (e) {
console.error(`Bad changeset at revision ${revNum} - ${e.message}`);
callback();
return;
}
if (~keyRevisions.indexOf(revNum)) {
try {
expect(revision.meta.atext.text).to.eql(atext.text);
expect(revision.meta.atext.attribs).to.eql(atext.attribs);
} catch (e) {
console.error(`Atext in key revision ${revNum} doesn't match computed one.`);
console.log(diff.diffChars(atext.text, revision.meta.atext.text).map((op) => { if (!op.added && !op.removed) op.value = op.value.length; return op; }));
// console.error(e)
// console.log('KeyRev. :', revision.meta.atext)
// console.log('Computed:', atext)
callback();
return;
}
}
setImmediate(callback);
});
}, (er) => {
if (pad.atext.text == atext.text) { console.log('ok'); } else {
console.error('Pad AText doesn\'t match computed one! (Computed ', atext.text.length, ', db', pad.atext.text.length, ')');
console.log(diff.diffChars(atext.text, pad.atext.text).map((op) => { if (!op.added && !op.removed) op.value = op.value.length; return op; }));
}
callback(er);
});
process.exit(0);
} catch (e) {
console.trace(e);
process.exit(1);
}
});

View file

@ -1,391 +0,0 @@
const startTime = Date.now();
const fs = require('fs');
const ueberDB = require('../src/node_modules/ueberdb2');
const mysql = require('../src/node_modules/ueberdb2/node_modules/mysql');
const async = require('../src/node_modules/async');
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
const settingsFile = process.argv[2];
const sqlOutputFile = process.argv[3];
// stop if the settings file is not set
if (!settingsFile || !sqlOutputFile) {
console.error('Use: node convert.js $SETTINGSFILE $SQLOUTPUT');
process.exit(1);
}
log('read settings file...');
// read the settings file and parse the json
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
log('done');
log('open output file...');
const sqlOutput = fs.openSync(sqlOutputFile, 'w');
const sql = 'SET CHARACTER SET UTF8;\n' +
'CREATE TABLE IF NOT EXISTS `store` ( \n' +
'`key` VARCHAR( 100 ) NOT NULL , \n' +
'`value` LONGTEXT NOT NULL , \n' +
'PRIMARY KEY ( `key` ) \n' +
') ENGINE = INNODB;\n' +
'START TRANSACTION;\n\n';
fs.writeSync(sqlOutput, sql);
log('done');
const etherpadDB = mysql.createConnection({
host: settings.etherpadDB.host,
user: settings.etherpadDB.user,
password: settings.etherpadDB.password,
database: settings.etherpadDB.database,
port: settings.etherpadDB.port,
});
// get the timestamp once
const timestamp = Date.now();
let padIDs;
async.series([
// get all padids out of the database...
function (callback) {
log('get all padIds out of the database...');
etherpadDB.query('SELECT ID FROM PAD_META', [], (err, _padIDs) => {
padIDs = _padIDs;
callback(err);
});
},
function (callback) {
log('done');
// create a queue with a concurrency 100
const queue = async.queue((padId, callback) => {
convertPad(padId, (err) => {
incrementPadStats();
callback(err);
});
}, 100);
// set the step callback as the queue callback
queue.drain = callback;
// add the padids to the worker queue
for (let i = 0, length = padIDs.length; i < length; i++) {
queue.push(padIDs[i].ID);
}
},
], (err) => {
if (err) throw err;
// write the groups
let sql = '';
for (const proID in proID2groupID) {
const groupID = proID2groupID[proID];
const subdomain = proID2subdomain[proID];
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`group:${groupID}`)}, ${etherpadDB.escape(JSON.stringify(groups[groupID]))});\n`;
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`mapper2group:subdomain:${subdomain}`)}, ${etherpadDB.escape(groupID)});\n`;
}
// close transaction
sql += 'COMMIT;';
// end the sql file
fs.writeSync(sqlOutput, sql, undefined, 'utf-8');
fs.closeSync(sqlOutput);
log('finished.');
process.exit(0);
});
function log(str) {
console.log(`${(Date.now() - startTime) / 1000}\t${str}`);
}
let padsDone = 0;
function incrementPadStats() {
padsDone++;
if (padsDone % 100 == 0) {
const averageTime = Math.round(padsDone / ((Date.now() - startTime) / 1000));
log(`${padsDone}/${padIDs.length}\t${averageTime} pad/s`);
}
}
var proID2groupID = {};
var proID2subdomain = {};
var groups = {};
function convertPad(padId, callback) {
const changesets = [];
const changesetsMeta = [];
const chatMessages = [];
const authors = [];
let apool;
let subdomain;
let padmeta;
async.series([
// get all needed db values
function (callback) {
async.parallel([
// get the pad revisions
function (callback) {
const sql = 'SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
} catch (e) { err = e; }
}
callback(err);
});
},
// get the chat entries
function (callback) {
const sql = 'SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false);
}
} catch (e) { err = e; }
}
callback(err);
});
},
// get the pad revisions meta data
function (callback) {
const sql = 'SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
} catch (e) { err = e; }
}
callback(err);
});
},
// get the attribute pool of this pad
function (callback) {
const sql = 'SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
apool = JSON.parse(results[0].JSON).x;
} catch (e) { err = e; }
}
callback(err);
});
},
// get the authors informations
function (callback) {
const sql = 'SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
// parse the pages
for (let i = 0, length = results.length; i < length; i++) {
parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true);
}
} catch (e) { err = e; }
}
callback(err);
});
},
// get the pad information
function (callback) {
const sql = 'SELECT JSON FROM `PAD_META` WHERE ID=?';
etherpadDB.query(sql, [padId], (err, results) => {
if (!err) {
try {
padmeta = JSON.parse(results[0].JSON).x;
} catch (e) { err = e; }
}
callback(err);
});
},
// get the subdomain
function (callback) {
// skip if this is no proPad
if (padId.indexOf('$') == -1) {
callback();
return;
}
// get the proID out of this padID
const proID = padId.split('$')[0];
const sql = 'SELECT subDomain FROM pro_domains WHERE ID = ?';
etherpadDB.query(sql, [proID], (err, results) => {
if (!err) {
subdomain = results[0].subDomain;
}
callback(err);
});
},
], callback);
},
function (callback) {
// saves all values that should be written to the database
const values = {};
// this is a pro pad, let's convert it to a group pad
if (padId.indexOf('$') != -1) {
const padIdParts = padId.split('$');
const proID = padIdParts[0];
const padName = padIdParts[1];
let groupID;
// this proID is not converted so far, do it
if (proID2groupID[proID] == null) {
groupID = `g.${randomString(16)}`;
// create the mappers for this new group
proID2groupID[proID] = groupID;
proID2subdomain[proID] = subdomain;
groups[groupID] = {pads: {}};
}
// use the generated groupID;
groupID = proID2groupID[proID];
// rename the pad
padId = `${groupID}$${padName}`;
// set the value for this pad in the group
groups[groupID].pads[padId] = 1;
}
try {
const newAuthorIDs = {};
const oldName2newName = {};
// replace the authors with generated authors
// we need to do that cause where the original etherpad saves pad local authors, the new (lite) etherpad uses them global
for (var i in apool.numToAttrib) {
var key = apool.numToAttrib[i][0];
const value = apool.numToAttrib[i][1];
// skip non authors and anonymous authors
if (key != 'author' || value == '') continue;
// generate new author values
const authorID = `a.${randomString(16)}`;
const authorColorID = authors[i].colorId || Math.floor(Math.random() * (exports.getColorPalette().length));
const authorName = authors[i].name || null;
// overwrite the authorID of the attribute pool
apool.numToAttrib[i][1] = authorID;
// write the author to the database
values[`globalAuthor:${authorID}`] = {colorId: authorColorID, name: authorName, timestamp};
// save in mappers
newAuthorIDs[i] = authorID;
oldName2newName[value] = authorID;
}
// save all revisions
for (var i = 0; i < changesets.length; i++) {
values[`pad:${padId}:revs:${i}`] = {changeset: changesets[i],
meta: {
author: newAuthorIDs[changesetsMeta[i].a],
timestamp: changesetsMeta[i].t,
atext: changesetsMeta[i].atext || undefined,
}};
}
// save all chat messages
for (var i = 0; i < chatMessages.length; i++) {
values[`pad:${padId}:chat:${i}`] = {text: chatMessages[i].lineText,
userId: oldName2newName[chatMessages[i].userId],
time: chatMessages[i].time};
}
// generate the latest atext
const fullAPool = (new AttributePool()).fromJsonable(apool);
const keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval;
let atext = changesetsMeta[keyRev].atext;
let curRev = keyRev;
while (curRev < padmeta.head) {
curRev++;
const changeset = changesets[curRev];
atext = Changeset.applyToAText(changeset, atext, fullAPool);
}
values[`pad:${padId}`] = {atext,
pool: apool,
head: padmeta.head,
chatHead: padmeta.numChatMessages};
} catch (e) {
console.error(`Error while converting pad ${padId}, pad skipped`);
console.error(e.stack ? e.stack : JSON.stringify(e));
callback();
return;
}
let sql = '';
for (var key in values) {
sql += `REPLACE INTO store VALUES (${etherpadDB.escape(key)}, ${etherpadDB.escape(JSON.stringify(values[key]))});\n`;
}
fs.writeSync(sqlOutput, sql, undefined, 'utf-8');
callback();
},
], callback);
}
/**
* This parses a Page like Etherpad uses them in the databases
* The offsets describes the length of a unit in the page, the data are
* all values behind each other
*/
function parsePage(array, pageStart, offsets, data, json) {
let start = 0;
const lengths = offsets.split(',');
for (let i = 0; i < lengths.length; i++) {
let unitLength = lengths[i];
// skip empty units
if (unitLength == '') continue;
// parse the number
unitLength = Number(unitLength);
// cut the unit out of data
const unit = data.substr(start, unitLength);
// put it into the array
array[pageStart + i] = json ? JSON.parse(unit) : unit;
// update start
start += unitLength;
}
}

View file

@ -1,51 +0,0 @@
/*
* A tool for deleting ALL GROUP sessions Etherpad user sessions from the CLI,
* because sometimes a brick is required to fix a face.
*/
const request = require('../src/node_modules/request');
const settings = require(`${__dirname}/../tests/container/loadSettings`).loadSettings();
const supertest = require(`${__dirname}/../src/node_modules/supertest`);
const api = supertest(`http://${settings.ip}:${settings.port}`);
const path = require('path');
const fs = require('fs');
// get the API Key
const filePath = path.join(__dirname, '../APIKEY.txt');
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
// Set apiVersion to base value, we change this later.
let apiVersion = 1;
let guids;
// Update the apiVersion
api.get('/api/')
.expect((res) => {
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error('No version set in API');
return;
})
.then(() => {
const guri = `/api/${apiVersion}/listAllGroups?apikey=${apikey}`;
api.get(guri)
.then((res) => {
guids = res.body.data.groupIDs;
guids.forEach((groupID) => {
const luri = `/api/${apiVersion}/listSessionsOfGroup?apikey=${apikey}&groupID=${groupID}`;
api.get(luri)
.then((res) => {
if (res.body.data) {
Object.keys(res.body.data).forEach((sessionID) => {
if (sessionID) {
console.log('Deleting', sessionID);
const duri = `/api/${apiVersion}/deleteSession?apikey=${apikey}&sessionID=${sessionID}`;
api.post(duri); // deletes
}
});
} else {
// no session in this group.
}
});
});
});
});

View file

@ -1,48 +0,0 @@
/*
* A tool for deleting pads from the CLI, because sometimes a brick is required
* to fix a window.
*/
const request = require('../src/node_modules/request');
const settings = require(`${__dirname}/../tests/container/loadSettings`).loadSettings();
const supertest = require(`${__dirname}/../src/node_modules/supertest`);
const api = supertest(`http://${settings.ip}:${settings.port}`);
const path = require('path');
const fs = require('fs');
if (process.argv.length != 3) {
console.error('Use: node deletePad.js $PADID');
process.exit(1);
}
// get the padID
const padId = process.argv[2];
// get the API Key
const filePath = path.join(__dirname, '../APIKEY.txt');
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
// Set apiVersion to base value, we change this later.
let apiVersion = 1;
// Update the apiVersion
api.get('/api/')
.expect((res) => {
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error('No version set in API');
return;
})
.end((err, res) => {
// Now we know the latest API version, let's delete pad
const uri = `/api/${apiVersion}/deletePad?apikey=${apikey}&padID=${padId}`;
api.post(uri)
.expect((res) => {
if (res.body.code === 1) {
console.error('Error deleting pad', res.body);
} else {
console.log('Deleted pad', res.body);
}
return;
})
.end(() => {});
});
// end

View file

@ -1,75 +0,0 @@
/*
* This is a debug tool. It helps to extract all datas of a pad and move it from
* a productive environment and to a develop environment to reproduce bugs
* there. It outputs a dirtydb file
*/
if (process.argv.length != 3) {
console.error('Use: node extractPadData.js $PADID');
process.exit(1);
}
// get the padID
const padId = process.argv[2];
const npm = require('../src/node_modules/npm');
npm.load({}, async (er) => {
if (er) {
console.error(`Could not load NPM: ${er}`);
process.exit(1);
}
try {
// initialize database
const settings = require('../src/node/utils/Settings');
const db = require('../src/node/db/DB');
await db.init();
// load extra modules
const dirtyDB = require('../src/node_modules/dirty');
const padManager = require('../src/node/db/PadManager');
const util = require('util');
// initialize output database
const dirty = dirtyDB(`${padId}.db`);
// Promise wrapped get and set function
const wrapped = db.db.db.wrappedDB;
const get = util.promisify(wrapped.get.bind(wrapped));
const set = util.promisify(dirty.set.bind(dirty));
// array in which required key values will be accumulated
const neededDBValues = [`pad:${padId}`];
// get the actual pad object
const pad = await padManager.getPad(padId);
// add all authors
neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`));
// add all revisions
for (let rev = 0; rev <= pad.head; ++rev) {
neededDBValues.push(`pad:${padId}:revs:${rev}`);
}
// add all chat values
for (let chat = 0; chat <= pad.chatHead; ++chat) {
neededDBValues.push(`pad:${padId}:chat:${chat}`);
}
for (const dbkey of neededDBValues) {
let dbvalue = await get(dbkey);
if (dbvalue && typeof dbvalue !== 'object') {
dbvalue = JSON.parse(dbvalue);
}
await set(dbkey, dbvalue);
}
console.log('finished');
process.exit(0);
} catch (er) {
console.error(er);
process.exit(1);
}
});

View file

@ -1,25 +0,0 @@
#!/bin/bash
#
# Run Etherpad directly, assuming all the dependencies are already installed.
#
# Useful for developers, or users that know what they are doing. If you just
# upgraded Etherpad version, installed a new dependency, or are simply unsure
# of what to do, please execute bin/installDeps.sh once before running this
# script.
set -eu
# source: https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source constants and usefull functions
. ${DIR}/../bin/functions.sh
echo "Running directly, without checking/installing dependencies"
# move to the base Etherpad directory. This will be necessary until Etherpad
# learns to run from arbitrary CWDs.
cd "${DIR}/.."
# run Etherpad main class
node $(compute_node_args) "${DIR}/../node_modules/ep_etherpad-lite/node/server.js" "$@"

View file

@ -1,103 +0,0 @@
const startTime = Date.now();
require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
const fs = require('fs');
const ueberDB = require('ep_etherpad-lite/node_modules/ueberdb2');
const settings = require('ep_etherpad-lite/node/utils/Settings');
const log4js = require('ep_etherpad-lite/node_modules/log4js');
const dbWrapperSettings = {
cache: 0,
writeInterval: 100,
json: false, // data is already json encoded
};
const db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB'));
const sqlFile = process.argv[2];
// stop if the settings file is not set
if (!sqlFile) {
console.error('Use: node importSqlFile.js $SQLFILE');
process.exit(1);
}
log('initializing db');
db.init((err) => {
// there was an error while initializing the database, output it and stop
if (err) {
console.error('ERROR: Problem while initializing the database');
console.error(err.stack ? err.stack : err);
process.exit(1);
} else {
log('done');
log('open output file...');
const lines = fs.readFileSync(sqlFile, 'utf8').split('\n');
const count = lines.length;
let keyNo = 0;
process.stdout.write(`Start importing ${count} keys...\n`);
lines.forEach((l) => {
if (l.substr(0, 27) == 'REPLACE INTO store VALUES (') {
const pos = l.indexOf("', '");
const key = l.substr(28, pos - 28);
let value = l.substr(pos + 3);
value = value.substr(0, value.length - 2);
console.log(`key: ${key} val: ${value}`);
console.log(`unval: ${unescape(value)}`);
db.set(key, unescape(value), null);
keyNo++;
if (keyNo % 1000 == 0) {
process.stdout.write(` ${keyNo}/${count}\n`);
}
}
});
process.stdout.write('\n');
process.stdout.write('done. waiting for db to finish transaction. depended on dbms this may take some time...\n');
db.doShutdown(() => {
log(`finished, imported ${keyNo} keys.`);
process.exit(0);
});
}
});
});
function log(str) {
console.log(`${(Date.now() - startTime) / 1000}\t${str}`);
}
unescape = function (val) {
// value is a string
if (val.substr(0, 1) == "'") {
val = val.substr(0, val.length - 1).substr(1);
return val.replace(/\\[0nrbtZ\\'"]/g, (s) => {
switch (s) {
case '\\0': return '\0';
case '\\n': return '\n';
case '\\r': return '\r';
case '\\b': return '\b';
case '\\t': return '\t';
case '\\Z': return '\x1a';
default: return s.substr(1);
}
});
}
// value is a boolean or NULL
if (val == 'NULL') {
return null;
}
if (val == 'true') {
return true;
}
if (val == 'false') {
return false;
}
// value is a number
return val;
};

View file

@ -1,48 +0,0 @@
require('ep_etherpad-lite/node_modules/npm').load({}, (er, npm) => {
process.chdir(`${npm.root}/..`);
// This script requires that you have modified your settings.json file
// to work with a real database. Please make a backup of your dirty.db
// file before using this script, just to be safe.
// It might be necessary to run the script using more memory:
// `node --max-old-space-size=4096 bin/migrateDirtyDBtoRealDB.js`
const settings = require('ep_etherpad-lite/node/utils/Settings');
let dirty = require('../src/node_modules/dirty');
const ueberDB = require('../src/node_modules/ueberdb2');
const log4js = require('../src/node_modules/log4js');
const dbWrapperSettings = {
cache: '0', // The cache slows things down when you're mostly writing.
writeInterval: 0, // Write directly to the database, don't buffer
};
const db = new ueberDB.database(settings.dbType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB'));
let i = 0;
let length = 0;
db.init(() => {
console.log('Waiting for dirtyDB to parse its file.');
dirty = dirty('var/dirty.db').on('load', () => {
dirty.forEach(() => {
length++;
});
console.log(`Found ${length} records, processing now.`);
dirty.forEach(async (key, value) => {
const error = await db.set(key, value);
console.log(`Wrote record ${i}`);
i++;
if (i === length) {
console.log('finished, just clearing up for a bit...');
setTimeout(() => {
process.exit(0);
}, 5000);
}
});
console.log('Please wait for all records to flush to database, then kill this process.');
});
console.log('done?');
});
});

View file

@ -1,469 +0,0 @@
/*
*
* Usage -- see README.md
*
* Normal usage: node bin/plugins/checkPlugins.js ep_whatever
* Auto fix the things it can: node bin/plugins/checkPlugins.js ep_whatever autofix
* Auto commit, push and publish(to npm) * highly dangerous:
node bin/plugins/checkPlugins.js ep_whatever autofix autocommit
*/
const fs = require('fs');
const {exec} = require('child_process');
// get plugin name & path from user input
const pluginName = process.argv[2];
if (!pluginName) {
console.error('no plugin name specified');
process.exit(1);
}
const pluginPath = `node_modules/${pluginName}`;
console.log(`Checking the plugin: ${pluginName}`);
// Should we autofix?
if (process.argv[3] && process.argv[3] === 'autofix') var autoFix = true;
// Should we update files where possible?
if (process.argv[5] && process.argv[5] === 'autoupdate') var autoUpdate = true;
// Should we automcommit and npm publish?!
if (process.argv[4] && process.argv[4] === 'autocommit') var autoCommit = true;
if (autoCommit) {
console.warn('Auto commit is enabled, I hope you know what you are doing...');
}
fs.readdir(pluginPath, (err, rootFiles) => {
// handling error
if (err) {
return console.log(`Unable to scan directory: ${err}`);
}
// rewriting files to lower case
const files = [];
// some files we need to know the actual file name. Not compulsory but might help in the future.
let readMeFileName;
let repository;
let hasAutoFixed = false;
for (let i = 0; i < rootFiles.length; i++) {
if (rootFiles[i].toLowerCase().indexOf('readme') !== -1) readMeFileName = rootFiles[i];
files.push(rootFiles[i].toLowerCase());
}
if (files.indexOf('.git') === -1) {
console.error('No .git folder, aborting');
process.exit(1);
}
// do a git pull...
var child_process = require('child_process');
try {
child_process.execSync('git pull ', {cwd: `${pluginPath}/`});
} catch (e) {
console.error('Error git pull', e);
}
try {
const path = `${pluginPath}/.github/workflows/npmpublish.yml`;
if (!fs.existsSync(path)) {
console.log('no .github/workflows/npmpublish.yml, create one and set npm secret to auto publish to npm on commit');
if (autoFix) {
const npmpublish =
fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, npmpublish);
hasAutoFixed = true;
console.log("If you haven't already, setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo");
} else {
console.log('Setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo');
}
} else {
// autopublish exists, we should check the version..
// checkVersion takes two file paths and checks for a version string in them.
const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V=');
const existingValue = parseInt(currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length));
const reqVersionFile = fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V=');
const reqValue = parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length));
if (!existingValue || (reqValue > existingValue)) {
const npmpublish =
fs.readFileSync('bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, npmpublish);
hasAutoFixed = true;
}
}
} catch (err) {
console.error(err);
}
try {
const path = `${pluginPath}/.github/workflows/backend-tests.yml`;
if (!fs.existsSync(path)) {
console.log('no .github/workflows/backend-tests.yml, create one and set npm secret to auto publish to npm on commit');
if (autoFix) {
const backendTests =
fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, backendTests);
hasAutoFixed = true;
}
} else {
// autopublish exists, we should check the version..
// checkVersion takes two file paths and checks for a version string in them.
const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V=');
const existingValue = parseInt(currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length));
const reqVersionFile = fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V=');
const reqValue = parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length));
if (!existingValue || (reqValue > existingValue)) {
const backendTests =
fs.readFileSync('bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, backendTests);
hasAutoFixed = true;
}
}
} catch (err) {
console.error(err);
}
if (files.indexOf('package.json') === -1) {
console.warn('no package.json, please create');
}
if (files.indexOf('package.json') !== -1) {
const packageJSON = fs.readFileSync(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'});
const parsedPackageJSON = JSON.parse(packageJSON);
if (autoFix) {
let updatedPackageJSON = false;
if (!parsedPackageJSON.funding) {
updatedPackageJSON = true;
parsedPackageJSON.funding = {
type: 'individual',
url: 'https://etherpad.org/',
};
}
if (updatedPackageJSON) {
hasAutoFixed = true;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
}
}
if (packageJSON.toLowerCase().indexOf('repository') === -1) {
console.warn('No repository in package.json');
if (autoFix) {
console.warn('Repository not detected in package.json. Please add repository section manually.');
}
} else {
// useful for creating README later.
repository = parsedPackageJSON.repository.url;
}
// include lint config
if (packageJSON.toLowerCase().indexOf('devdependencies') === -1 || !parsedPackageJSON.devDependencies.eslint) {
console.warn('Missing eslint reference in devDependencies');
if (autoFix) {
const devDependencies = {
'eslint': '^7.14.0',
'eslint-config-etherpad': '^1.0.13',
'eslint-plugin-mocha': '^8.0.0',
'eslint-plugin-node': '^11.1.0',
'eslint-plugin-prefer-arrow': '^1.2.2',
'eslint-plugin-promise': '^4.2.1',
};
hasAutoFixed = true;
parsedPackageJSON.devDependencies = devDependencies;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
const child_process = require('child_process');
try {
child_process.execSync('npm install', {cwd: `${pluginPath}/`});
hasAutoFixed = true;
} catch (e) {
console.error('Failed to create package-lock.json');
}
}
}
// include peer deps config
if (packageJSON.toLowerCase().indexOf('peerdependencies') === -1 || !parsedPackageJSON.peerDependencies) {
console.warn('Missing peer deps reference in package.json');
if (autoFix) {
const peerDependencies = {
'ep_etherpad-lite': '>=1.8.6',
};
hasAutoFixed = true;
parsedPackageJSON.peerDependencies = peerDependencies;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
const child_process = require('child_process');
try {
child_process.execSync('npm install --no-save ep_etherpad-lite@file:../../src', {cwd: `${pluginPath}/`});
hasAutoFixed = true;
} catch (e) {
console.error('Failed to create package-lock.json');
}
}
}
if (packageJSON.toLowerCase().indexOf('eslintconfig') === -1) {
console.warn('No esLintConfig in package.json');
if (autoFix) {
const eslintConfig = {
root: true,
extends: 'etherpad/plugin',
};
hasAutoFixed = true;
parsedPackageJSON.eslintConfig = eslintConfig;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
}
}
if (packageJSON.toLowerCase().indexOf('scripts') === -1) {
console.warn('No scripts in package.json');
if (autoFix) {
const scripts = {
'lint': 'eslint .',
'lint:fix': 'eslint --fix .',
};
hasAutoFixed = true;
parsedPackageJSON.scripts = scripts;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
}
}
if ((packageJSON.toLowerCase().indexOf('engines') === -1) || !parsedPackageJSON.engines.node) {
console.warn('No engines or node engine in package.json');
if (autoFix) {
const engines = {
node: '>=10.13.0',
};
hasAutoFixed = true;
parsedPackageJSON.engines = engines;
fs.writeFileSync(`${pluginPath}/package.json`, JSON.stringify(parsedPackageJSON, null, 2));
}
}
}
if (files.indexOf('package-lock.json') === -1) {
console.warn('package-lock.json file not found. Please run npm install in the plugin folder and commit the package-lock.json file.');
if (autoFix) {
var child_process = require('child_process');
try {
child_process.execSync('npm install', {cwd: `${pluginPath}/`});
console.log('Making package-lock.json');
hasAutoFixed = true;
} catch (e) {
console.error('Failed to create package-lock.json');
}
}
}
if (files.indexOf('readme') === -1 && files.indexOf('readme.md') === -1) {
console.warn('README.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing README.md file, please edit the README.md file further to include plugin specific details.');
let readme = fs.readFileSync('bin/plugins/lib/README.md', {encoding: 'utf8', flag: 'r'});
readme = readme.replace(/\[plugin_name\]/g, pluginName);
if (repository) {
const org = repository.split('/')[3];
const name = repository.split('/')[4];
readme = readme.replace(/\[org_name\]/g, org);
readme = readme.replace(/\[repo_url\]/g, name);
fs.writeFileSync(`${pluginPath}/README.md`, readme);
} else {
console.warn('Unable to find repository in package.json, aborting.');
}
}
}
if (files.indexOf('contributing') === -1 && files.indexOf('contributing.md') === -1) {
console.warn('CONTRIBUTING.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md file further to include plugin specific details.');
let contributing = fs.readFileSync('bin/plugins/lib/CONTRIBUTING.md', {encoding: 'utf8', flag: 'r'});
contributing = contributing.replace(/\[plugin_name\]/g, pluginName);
fs.writeFileSync(`${pluginPath}/CONTRIBUTING.md`, contributing);
}
}
if (files.indexOf('readme') !== -1 && files.indexOf('readme.md') !== -1) {
const readme = fs.readFileSync(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'});
if (readme.toLowerCase().indexOf('license') === -1) {
console.warn('No license section in README');
if (autoFix) {
console.warn('Please add License section to README manually.');
}
}
}
if (files.indexOf('license') === -1 && files.indexOf('license.md') === -1) {
console.warn('LICENSE.md file not found, please create');
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing missing LICENSE.md file, including Apache 2 license.');
exec('git config user.name', (error, name, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
let license = fs.readFileSync('bin/plugins/lib/LICENSE.md', {encoding: 'utf8', flag: 'r'});
license = license.replace('[yyyy]', new Date().getFullYear());
license = license.replace('[name of copyright owner]', name);
fs.writeFileSync(`${pluginPath}/LICENSE.md`, license);
});
}
}
let travisConfig = fs.readFileSync('bin/plugins/lib/travis.yml', {encoding: 'utf8', flag: 'r'});
travisConfig = travisConfig.replace(/\[plugin_name\]/g, pluginName);
if (files.indexOf('.travis.yml') === -1) {
console.warn('.travis.yml file not found, please create. .travis.yml is used for automatically CI testing Etherpad. It is useful to know if your plugin breaks another feature for example.');
// TODO: Make it check version of the .travis file to see if it needs an update.
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing missing .travis.yml file');
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
console.log('Travis file created, please sign into travis and enable this repository');
}
}
if (autoFix && autoUpdate) {
// checks the file versioning of .travis and updates it to the latest.
const existingConfig = fs.readFileSync(`${pluginPath}/.travis.yml`, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = existingConfig.indexOf('##ETHERPAD_TRAVIS_V=');
const existingValue = parseInt(existingConfig.substr(existingConfigLocation + 20, existingConfig.length));
const newConfigLocation = travisConfig.indexOf('##ETHERPAD_TRAVIS_V=');
const newValue = parseInt(travisConfig.substr(newConfigLocation + 20, travisConfig.length));
if (existingConfigLocation === -1) {
console.warn('no previous .travis.yml version found so writing new.');
// we will write the newTravisConfig to the location.
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
} else if (newValue > existingValue) {
console.log('updating .travis.yml');
fs.writeFileSync(`${pluginPath}/.travis.yml`, travisConfig);
hasAutoFixed = true;
}//
}
if (files.indexOf('.gitignore') === -1) {
console.warn(".gitignore file not found, please create. .gitignore files are useful to ensure files aren't incorrectly commited to a repository.");
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing missing .gitignore file');
const gitignore = fs.readFileSync('bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'});
fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore);
}
} else {
let gitignore =
fs.readFileSync(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'});
if (gitignore.indexOf('node_modules/') === -1) {
console.warn('node_modules/ missing from .gitignore');
if (autoFix) {
gitignore += 'node_modules/';
fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore);
hasAutoFixed = true;
}
}
}
// if we include templates but don't have translations...
if (files.indexOf('templates') !== -1 && files.indexOf('locales') === -1) {
console.warn('Translations not found, please create. Translation files help with Etherpad accessibility.');
}
if (files.indexOf('.ep_initialized') !== -1) {
console.warn('.ep_initialized found, please remove. .ep_initialized should never be commited to git and should only exist once the plugin has been executed one time.');
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing incorrectly existing .ep_initialized file');
fs.unlinkSync(`${pluginPath}/.ep_initialized`);
}
}
if (files.indexOf('npm-debug.log') !== -1) {
console.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to your repository.');
if (autoFix) {
hasAutoFixed = true;
console.log('Autofixing incorrectly existing npm-debug.log file');
fs.unlinkSync(`${pluginPath}/npm-debug.log`);
}
}
if (files.indexOf('static') !== -1) {
fs.readdir(`${pluginPath}/static`, (errRead, staticFiles) => {
if (staticFiles.indexOf('tests') === -1) {
console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
}
});
} else {
console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
}
// linting begins
if (autoFix) {
var lintCmd = 'npm run lint:fix';
} else {
var lintCmd = 'npm run lint';
}
try {
child_process.execSync(lintCmd, {cwd: `${pluginPath}/`});
console.log('Linting...');
if (autoFix) {
// todo: if npm run lint doesn't do anything no need for...
hasAutoFixed = true;
}
} catch (e) {
// it is gonna throw an error anyway
console.log('Manual linting probably required, check with: npm run lint');
}
// linting ends.
if (hasAutoFixed) {
console.log('Fixes applied, please check git diff then run the following command:\n\n');
// bump npm Version
if (autoCommit) {
// holy shit you brave.
console.log('Attempting autocommit and auto publish to npm');
// github should push to npm for us :)
exec(`cd node_modules/${pluginName} && git rm -rf node_modules --ignore-unmatch && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && git push && cd ../..`, (error, name, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log("I think she's got it! By George she's got it!");
process.exit(0);
});
} else {
console.log(`cd node_modules/${pluginName} && git add -A && git commit --allow-empty -m 'autofixes from Etherpad checkPlugins.js' && npm version patch && git add package.json && git commit --allow-empty -m 'bump version' && git push && npm publish && cd ../..`);
}
}
console.log('Finished');
});

View file

@ -1,70 +0,0 @@
language: node_js
node_js:
- "lts/*"
cache: false
services:
- docker
install:
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
#script:
# - "tests/frontend/travis/runner.sh"
env:
global:
- secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec="
- secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g="
jobs:
include:
- name: "Lint test package-lock"
install:
- "npm install lockfile-lint"
script:
- npx lockfile-lint --path package-lock.json --validate-https --allowed-hosts npm
- name: "Run the Backend tests"
before_install:
- sudo add-apt-repository -y ppa:libreoffice/ppa
- sudo apt-get update
- sudo apt-get -y install libreoffice
- sudo apt-get -y install libreoffice-pdfimport
install:
- "npm install"
- "mkdir [plugin_name]"
- "mv !([plugin_name]) [plugin_name]"
- "git clone https://github.com/ether/etherpad-lite.git etherpad"
- "cd etherpad"
- "mkdir -p node_modules"
- "mv ../[plugin_name] node_modules"
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
- "cd src && npm install && cd -"
script:
- "tests/frontend/travis/runnerBackend.sh"
- name: "Test the Frontend"
before_script:
- "tests/frontend/travis/sauce_tunnel.sh"
install:
- "npm install"
- "mkdir [plugin_name]"
- "mv !([plugin_name]) [plugin_name]"
- "git clone https://github.com/ether/etherpad-lite.git etherpad"
- "cd etherpad"
- "mkdir -p node_modules"
- "mv ../[plugin_name] node_modules"
- "bin/installDeps.sh"
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
script:
- "tests/frontend/travis/runner.sh"
notifications:
irc:
channels:
- "irc.freenode.org#etherpad-lite-dev"
##ETHERPAD_TRAVIS_V=9
## Travis configuration automatically created using bin/plugins/updateAllPluginsScript.sh

View file

@ -1,126 +0,0 @@
/*
This is a repair tool. It rebuilds an old pad at a new pad location up to a
known "good" revision.
*/
if (process.argv.length != 4 && process.argv.length != 5) {
console.error('Use: node bin/repairPad.js $PADID $REV [$NEWPADID]');
process.exit(1);
}
const npm = require('../src/node_modules/npm');
const async = require('../src/node_modules/async');
const ueberDB = require('../src/node_modules/ueberdb2');
const padId = process.argv[2];
const newRevHead = process.argv[3];
const newPadId = process.argv[4] || `${padId}-rebuilt`;
let db, oldPad, newPad, settings;
let AuthorManager, ChangeSet, Pad, PadManager;
async.series([
function (callback) {
npm.load({}, (err) => {
if (err) {
console.error(`Could not load NPM: ${err}`);
process.exit(1);
} else {
callback();
}
});
},
function (callback) {
// Get a handle into the database
db = require('../src/node/db/DB');
db.init(callback);
},
function (callback) {
PadManager = require('../src/node/db/PadManager');
Pad = require('../src/node/db/Pad').Pad;
// Get references to the original pad and to a newly created pad
// HACK: This is a standalone script, so we want to write everything
// out to the database immediately. The only problem with this is
// that a driver (like the mysql driver) can hardcode these values.
db.db.db.settings = {cache: 0, writeInterval: 0, json: true};
// Validate the newPadId if specified and that a pad with that ID does
// not already exist to avoid overwriting it.
if (!PadManager.isValidPadId(newPadId)) {
console.error('Cannot create a pad with that id as it is invalid');
process.exit(1);
}
PadManager.doesPadExists(newPadId, (err, exists) => {
if (exists) {
console.error('Cannot create a pad with that id as it already exists');
process.exit(1);
}
});
PadManager.getPad(padId, (err, pad) => {
oldPad = pad;
newPad = new Pad(newPadId);
callback();
});
},
function (callback) {
// Clone all Chat revisions
const chatHead = oldPad.chatHead;
for (var i = 0, curHeadNum = 0; i <= chatHead; i++) {
db.db.get(`pad:${padId}:chat:${i}`, (err, chat) => {
db.db.set(`pad:${newPadId}:chat:${curHeadNum++}`, chat);
console.log(`Created: Chat Revision: pad:${newPadId}:chat:${curHeadNum}`);
});
}
callback();
},
function (callback) {
// Rebuild Pad from revisions up to and including the new revision head
AuthorManager = require('../src/node/db/AuthorManager');
Changeset = require('ep_etherpad-lite/static/js/Changeset');
// Author attributes are derived from changesets, but there can also be
// non-author attributes with specific mappings that changesets depend on
// and, AFAICT, cannot be recreated any other way
newPad.pool.numToAttrib = oldPad.pool.numToAttrib;
for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) {
db.db.get(`pad:${padId}:revs:${curRevNum}`, (err, rev) => {
if (rev.meta) {
throw 'The specified revision number could not be found.';
}
const newRevNum = ++newPad.head;
const newRevId = `pad:${newPad.id}:revs:${newRevNum}`;
db.db.set(newRevId, rev);
AuthorManager.addPad(rev.meta.author, newPad.id);
newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool);
console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`);
if (newRevNum == newRevHead) {
callback();
}
});
}
},
function (callback) {
// Add saved revisions up to the new revision head
console.log(newPad.head);
const newSavedRevisions = [];
for (const i in oldPad.savedRevisions) {
savedRev = oldPad.savedRevisions[i];
if (savedRev.revNum <= newRevHead) {
newSavedRevisions.push(savedRev);
console.log(`Added: Saved Revision: ${savedRev.revNum}`);
}
}
newPad.savedRevisions = newSavedRevisions;
callback();
},
function (callback) {
// Save the source pad
db.db.set(`pad:${newPadId}`, newPad, (err) => {
console.log(`Created: Source Pad: pad:${newPadId}`);
newPad.saveToDatabase().then(() => callback(), callback);
});
},
], (err) => {
if (err) { throw err; } else {
console.info('finished');
process.exit(0);
}
});

View file

@ -1,67 +0,0 @@
'use strict';
const fs = require('fs');
const child_process = require('child_process');
const semver = require('../src/node_modules/semver');
/*
Usage
node bin/release.js patch
*/
const usage = 'node bin/release.js [patch/minor/major] -- example: "node bin/release.js patch"';
const release = process.argv[2];
if(!release) {
console.log(usage);
throw new Error('No release type included');
}
const changelog = fs.readFileSync('CHANGELOG.md', {encoding: 'utf8', flag: 'r'});
let packageJson = fs.readFileSync('./src/package.json', {encoding: 'utf8', flag: 'r'});
packageJson = JSON.parse(packageJson);
const currentVersion = packageJson.version;
const newVersion = semver.inc(currentVersion, release);
if(!newVersion) {
console.log(usage);
throw new Error('Unable to generate new version from input');
}
const changelogIncludesVersion = changelog.indexOf(newVersion) !== -1;
if(!changelogIncludesVersion) {
throw new Error('No changelog record for ', newVersion, ' - please create changelog record');
}
console.log('Okay looks good, lets create the package.json and package-lock.json');
packageJson.version = newVersion;
fs.writeFileSync('src/package.json', JSON.stringify(packageJson, null, 2));
// run npm version `release` where release is patch, minor or major
child_process.execSync('npm install --package-lock-only', {cwd: `src/`});
// run npm install --package-lock-only <-- required???
child_process.execSync(`git checkout -b release/${newVersion}`);
child_process.execSync(`git add src/package.json`);
child_process.execSync(`git add src/package-lock.json`);
child_process.execSync(`git commit -m 'bump version'`);
child_process.execSync(`git push origin release/${newVersion}`);
child_process.execSync(`make docs`);
child_process.execSync(`git clone git@github.com:ether/ether.github.com.git`);
child_process.execSync(`cp -R out/doc/ ether.github.com/doc/${newVersion}`);
console.log('Once merged into master please run the following commands');
console.log(`git tag -a ${newVersion} -m ${newVersion} && git push origin master`);
console.log(`cd ether.github.com && git add . && git commit -m '${newVersion} docs'`);
console.log(`Build the windows zip`)
console.log(`Visit https://github.com/ether/etherpad-lite/releases/new and create a new release with 'master' as the target and the version is ${newVersion}. Include the windows zip as an assett`)
console.log('Once the new docs are uploaded then modify the download link on etherpad.org and then pull master onto develop');
console.log('Finally go public with an announcement via our comms channels :)');

View file

@ -1,77 +0,0 @@
/*
* This is a repair tool. It extracts all datas of a pad, removes and inserts them again.
*/
console.warn('WARNING: This script must not be used while etherpad is running!');
if (process.argv.length != 3) {
console.error('Use: node bin/repairPad.js $PADID');
process.exit(1);
}
// get the padID
const padId = process.argv[2];
const npm = require('../src/node_modules/npm');
npm.load({}, async (er) => {
if (er) {
console.error(`Could not load NPM: ${er}`);
process.exit(1);
}
try {
// intialize database
const settings = require('../src/node/utils/Settings');
const db = require('../src/node/db/DB');
await db.init();
// get the pad
const padManager = require('../src/node/db/PadManager');
const pad = await padManager.getPad(padId);
// accumulate the required keys
const neededDBValues = [`pad:${padId}`];
// add all authors
neededDBValues.push(...pad.getAllAuthors().map((author) => 'globalAuthor:'));
// add all revisions
for (let rev = 0; rev <= pad.head; ++rev) {
neededDBValues.push(`pad:${padId}:revs:${rev}`);
}
// add all chat values
for (let chat = 0; chat <= pad.chatHead; ++chat) {
neededDBValues.push(`pad:${padId}:chat:${chat}`);
}
//
// NB: this script doesn't actually does what's documented
// since the `value` fields in the following `.forEach`
// block are just the array index numbers
//
// the script therefore craps out now before it can do
// any damage.
//
// See gitlab issue #3545
//
console.info('aborting [gitlab #3545]');
process.exit(1);
// now fetch and reinsert every key
neededDBValues.forEach((key, value) => {
console.log(`Key: ${key}, value: ${value}`);
db.remove(key);
db.set(key, value);
});
console.info('finished');
process.exit(0);
} catch (er) {
if (er.name === 'apierror') {
console.error(er);
} else {
console.trace(er);
}
}
});

View file

@ -3,10 +3,10 @@ You can easily embed your etherpad-lite into any webpage by using iframes. You c
Example: Example:
Cut and paste the following code into any webpage to embed a pad. The parameters below will hide the chat and the line numbers. Cut and paste the following code into any webpage to embed a pad. The parameters below will hide the chat and the line numbers and will auto-focus on Line 4.
``` ```
<iframe src='http://pad.test.de/p/PAD_NAME?showChat=false&showLineNumbers=false' width=600 height=400></iframe> <iframe src='http://pad.test.de/p/PAD_NAME#L4?showChat=false&showLineNumbers=false' width=600 height=400></iframe>
``` ```
## showLineNumbers ## showLineNumbers
@ -66,3 +66,10 @@ Example: `lang=ar` (translates the interface into Arabic)
Default: true Default: true
Displays pad text from right to left. Displays pad text from right to left.
## #L
* Int
Default: 0
Focuses pad at specific line number and places caret at beginning of this line
Special note: Is not a URL parameter but instead of a Hash value

View file

@ -1,5 +1,7 @@
# Client-side hooks # Client-side hooks
Most of these hooks are called during or in order to set up the formatting process.
Most of these hooks are called during or in order to set up the formatting
process.
## documentReady ## documentReady
Called from: src/templates/pad.html Called from: src/templates/pad.html
@ -11,6 +13,7 @@ nothing
This hook proxies the functionality of jQuery's `$(document).ready` event. This hook proxies the functionality of jQuery's `$(document).ready` event.
## aceDomLinePreProcessLineAttributes ## aceDomLinePreProcessLineAttributes
Called from: src/static/js/domline.js Called from: src/static/js/domline.js
Things in context: Things in context:
@ -18,15 +21,21 @@ Things in context:
1. domline - The current DOM line being processed 1. domline - The current DOM line being processed
2. cls - The class of the current block element (useful for styling) 2. cls - The class of the current block element (useful for styling)
This hook is called for elements in the DOM that have the "lineMarkerAttribute" set. You can add elements into this category with the aceRegisterBlockElements hook above. This hook is run BEFORE the numbered and ordered lists logic is applied. This hook is called for elements in the DOM that have the "lineMarkerAttribute"
set. You can add elements into this category with the aceRegisterBlockElements
hook above. This hook is run BEFORE the numbered and ordered lists logic is
applied.
The return value of this hook should have the following structure: The return value of this hook should have the following structure:
`{ preHtml: String, postHtml: String, processedMarker: Boolean }` `{ preHtml: String, postHtml: String, processedMarker: Boolean }`
The preHtml and postHtml values will be added to the HTML display of the element, and if processedMarker is true, the engine won't try to process it any more. The preHtml and postHtml values will be added to the HTML display of the
element, and if processedMarker is true, the engine won't try to process it any
more.
## aceDomLineProcessLineAttributes ## aceDomLineProcessLineAttributes
Called from: src/static/js/domline.js Called from: src/static/js/domline.js
Things in context: Things in context:
@ -34,15 +43,21 @@ Things in context:
1. domline - The current DOM line being processed 1. domline - The current DOM line being processed
2. cls - The class of the current block element (useful for styling) 2. cls - The class of the current block element (useful for styling)
This hook is called for elements in the DOM that have the "lineMarkerAttribute" set. You can add elements into this category with the aceRegisterBlockElements hook above. This hook is run AFTER the ordered and numbered lists logic is applied. This hook is called for elements in the DOM that have the "lineMarkerAttribute"
set. You can add elements into this category with the aceRegisterBlockElements
hook above. This hook is run AFTER the ordered and numbered lists logic is
applied.
The return value of this hook should have the following structure: The return value of this hook should have the following structure:
`{ preHtml: String, postHtml: String, processedMarker: Boolean }` `{ preHtml: String, postHtml: String, processedMarker: Boolean }`
The preHtml and postHtml values will be added to the HTML display of the element, and if processedMarker is true, the engine won't try to process it any more. The preHtml and postHtml values will be added to the HTML display of the
element, and if processedMarker is true, the engine won't try to process it any
more.
## aceCreateDomLine ## aceCreateDomLine
Called from: src/static/js/domline.js Called from: src/static/js/domline.js
Things in context: Things in context:
@ -50,43 +65,55 @@ Things in context:
1. domline - the current DOM line being processed 1. domline - the current DOM line being processed
2. cls - The class of the current element (useful for styling) 2. cls - The class of the current element (useful for styling)
This hook is called for any line being processed by the formatting engine, unless the aceDomLineProcessLineAttributes hook from above returned true, in which case this hook is skipped. This hook is called for any line being processed by the formatting engine,
unless the aceDomLineProcessLineAttributes hook from above returned true, in
which case this hook is skipped.
The return value of this hook should have the following structure: The return value of this hook should have the following structure:
`{ extraOpenTags: String, extraCloseTags: String, cls: String }` `{ extraOpenTags: String, extraCloseTags: String, cls: String }`
extraOpenTags and extraCloseTags will be added before and after the element in question, and cls will be the new class of the element going forward. extraOpenTags and extraCloseTags will be added before and after the element in
question, and cls will be the new class of the element going forward.
## acePostWriteDomLineHTML ## acePostWriteDomLineHTML
Called from: src/static/js/domline.js Called from: src/static/js/domline.js
Things in context: Things in context:
1. node - the DOM node that just got written to the page 1. node - the DOM node that just got written to the page
This hook is for right after a node has been fully formatted and written to the page. This hook is for right after a node has been fully formatted and written to the
page.
## aceAttribsToClasses ## aceAttribsToClasses
Called from: src/static/js/linestylefilter.js Called from: src/static/js/linestylefilter.js
Things in context: Things in context:
1. linestylefilter - the JavaScript object that's currently processing the ace attributes 1. linestylefilter - the JavaScript object that's currently processing the ace
attributes
2. key - the current attribute being processed 2. key - the current attribute being processed
3. value - the value of the attribute being processed 3. value - the value of the attribute being processed
This hook is called during the attribute processing procedure, and should be used to translate key, value pairs into valid HTML classes that can be inserted into the DOM. This hook is called during the attribute processing procedure, and should be
used to translate key, value pairs into valid HTML classes that can be inserted
into the DOM.
The return value for this function should be a list of classes, which will then be parsed into a valid class string. The return value for this function should be a list of classes, which will then
be parsed into a valid class string.
## aceAttribClasses ## aceAttribClasses
Called from: src/static/js/linestylefilter.js Called from: src/static/js/linestylefilter.js
Things in context: Things in context:
1. Attributes - Object of Attributes 1. Attributes - Object of Attributes
This hook is called when attributes are investigated on a line. It is useful if you want to add another attribute type or property type to a pad. This hook is called when attributes are investigated on a line. It is useful if
you want to add another attribute type or property type to a pad.
Example: Example:
``` ```
@ -97,32 +124,45 @@ exports.aceAttribClasses = function(hook_name, attr, cb){
``` ```
## aceGetFilterStack ## aceGetFilterStack
Called from: src/static/js/linestylefilter.js Called from: src/static/js/linestylefilter.js
Things in context: Things in context:
1. linestylefilter - the JavaScript object that's currently processing the ace attributes 1. linestylefilter - the JavaScript object that's currently processing the ace
attributes
2. browser - an object indicating which browser is accessing the page 2. browser - an object indicating which browser is accessing the page
This hook is called to apply custom regular expression filters to a set of styles. The one example available is the ep_linkify plugin, which adds internal links. They use it to find the telltale `[[ ]]` syntax that signifies internal links, and finding that syntax, they add in the internalHref attribute to be later used by the aceCreateDomLine hook (documented above). This hook is called to apply custom regular expression filters to a set of
styles. The one example available is the ep_linkify plugin, which adds internal
links. They use it to find the telltale `[[ ]]` syntax that signifies internal
links, and finding that syntax, they add in the internalHref attribute to be
later used by the aceCreateDomLine hook (documented above).
## aceEditorCSS ## aceEditorCSS
Called from: src/static/js/ace.js Called from: src/static/js/ace.js
Things in context: None Things in context: None
This hook is provided to allow custom CSS files to be loaded. The return value should be an array of resource urls or paths relative to the plugins directory. This hook is provided to allow custom CSS files to be loaded. The return value
should be an array of resource urls or paths relative to the plugins directory.
## aceInitInnerdocbodyHead ## aceInitInnerdocbodyHead
Called from: src/static/js/ace.js Called from: src/static/js/ace.js
Things in context: Things in context:
1. iframeHTML - the HTML of the editor iframe up to this point, in array format 1. iframeHTML - the HTML of the editor iframe up to this point, in array format
This hook is called during the creation of the editor HTML. The array should have lines of HTML added to it, giving the plugin author a chance to add in meta, script, link, and other tags that go into the `<head>` element of the editor HTML document. This hook is called during the creation of the editor HTML. The array should
have lines of HTML added to it, giving the plugin author a chance to add in
meta, script, link, and other tags that go into the `<head>` element of the
editor HTML document.
## aceEditEvent ## aceEditEvent
Called from: src/static/js/ace2_inner.js Called from: src/static/js/ace2_inner.js
Things in context: Things in context:
@ -130,16 +170,25 @@ Things in context:
1. callstack - a bunch of information about the current action 1. callstack - a bunch of information about the current action
2. editorInfo - information about the user who is making the change 2. editorInfo - information about the user who is making the change
3. rep - information about where the change is being made 3. rep - information about where the change is being made
4. documentAttributeManager - information about attributes in the document (this is a mystery to me) 4. documentAttributeManager - information about attributes in the document (this
is a mystery to me)
This hook is made available to edit the edit events that might occur when changes are made. Currently you can change the editor information, some of the meanings of the edit, and so on. You can also make internal changes (internal to your plugin) that use the information provided by the edit event. This hook is made available to edit the edit events that might occur when
changes are made. Currently you can change the editor information, some of the
meanings of the edit, and so on. You can also make internal changes (internal to
your plugin) that use the information provided by the edit event.
## aceRegisterNonScrollableEditEvents ## aceRegisterNonScrollableEditEvents
Called from: src/static/js/ace2_inner.js Called from: src/static/js/ace2_inner.js
Things in context: None Things in context: None
When aceEditEvent (documented above) finishes processing the event, it scrolls the viewport to make caret visible to the user, but if you don't want that behavior to happen you can use this hook to register which edit events should not scroll viewport. The return value of this hook should be a list of event names. When aceEditEvent (documented above) finishes processing the event, it scrolls
the viewport to make caret visible to the user, but if you don't want that
behavior to happen you can use this hook to register which edit events should
not scroll viewport. The return value of this hook should be a list of event
names.
Example: Example:
``` ```
@ -149,24 +198,32 @@ exports.aceRegisterNonScrollableEditEvents = function(){
``` ```
## aceRegisterBlockElements ## aceRegisterBlockElements
Called from: src/static/js/ace2_inner.js Called from: src/static/js/ace2_inner.js
Things in context: None Things in context: None
The return value of this hook will add elements into the "lineMarkerAttribute" category, making the aceDomLineProcessLineAttributes hook (documented below) call for those elements. The return value of this hook will add elements into the "lineMarkerAttribute"
category, making the aceDomLineProcessLineAttributes hook (documented below)
call for those elements.
## aceInitialized ## aceInitialized
Called from: src/static/js/ace2_inner.js Called from: src/static/js/ace2_inner.js
Things in context: Things in context:
1. editorInfo - information about the user who will be making changes through the interface, and a way to insert functions into the main ace object (see ep_headings) 1. editorInfo - information about the user who will be making changes through
the interface, and a way to insert functions into the main ace object (see
ep_headings)
2. rep - information about where the user's cursor is 2. rep - information about where the user's cursor is
3. documentAttributeManager - some kind of magic 3. documentAttributeManager - some kind of magic
This hook is for inserting further information into the ace engine, for later use in formatting hooks. This hook is for inserting further information into the ace engine, for later
use in formatting hooks.
## postAceInit ## postAceInit
Called from: src/static/js/pad.js Called from: src/static/js/pad.js
Things in context: Things in context:
@ -175,6 +232,7 @@ Things in context:
2. pad - the pad object of the current pad. 2. pad - the pad object of the current pad.
## postToolbarInit ## postToolbarInit
Called from: src/static/js/pad_editbar.js Called from: src/static/js/pad_editbar.js
Things in context: Things in context:
@ -189,30 +247,37 @@ Usage examples:
* [https://github.com/tiblu/ep_authorship_toggle]() * [https://github.com/tiblu/ep_authorship_toggle]()
## postTimesliderInit ## postTimesliderInit
Called from: src/static/js/timeslider.js Called from: src/static/js/timeslider.js
There doesn't appear to be any example available of this particular hook being used, but it gets fired after the timeslider is all set up. There doesn't appear to be any example available of this particular hook being
used, but it gets fired after the timeslider is all set up.
## goToRevisionEvent ## goToRevisionEvent
Called from: src/static/js/broadcast.js Called from: src/static/js/broadcast.js
Things in context: Things in context:
1. rev - The newRevision 1. rev - The newRevision
This hook gets fired both on timeslider load (as timeslider shows a new revision) and when the new revision is showed to a user. This hook gets fired both on timeslider load (as timeslider shows a new
There doesn't appear to be any example available of this particular hook being used. revision) and when the new revision is showed to a user. There doesn't appear to
be any example available of this particular hook being used.
## userJoinOrUpdate ## userJoinOrUpdate
Called from: src/static/js/pad_userlist.js Called from: src/static/js/pad_userlist.js
Things in context: Things in context:
1. info - the user information 1. info - the user information
This hook is called on the client side whenever a user joins or changes. This can be used to create notifications or an alternate user list. This hook is called on the client side whenever a user joins or changes. This
can be used to create notifications or an alternate user list.
## chatNewMessage ## chatNewMessage
Called from: src/static/js/chat.js Called from: src/static/js/chat.js
Things in context: Things in context:
@ -220,14 +285,18 @@ Things in context:
1. authorName - The user that wrote this message 1. authorName - The user that wrote this message
2. author - The authorID of the user that wrote the message 2. author - The authorID of the user that wrote the message
3. text - the message text 3. text - the message text
4. sticky (boolean) - if you want the gritter notification bubble to fade out on its own or just sit there 4. sticky (boolean) - if you want the gritter notification bubble to fade out on
its own or just sit there
5. timestamp - the timestamp of the chat message 5. timestamp - the timestamp of the chat message
6. timeStr - the timestamp as a formatted string 6. timeStr - the timestamp as a formatted string
7. duration - for how long in milliseconds should the gritter notification appear (0 to disable) 7. duration - for how long in milliseconds should the gritter notification
appear (0 to disable)
This hook is called on the client side whenever a chat message is received from the server. It can be used to create different notifications for chat messages. This hook is called on the client side whenever a chat message is received from
the server. It can be used to create different notifications for chat messages.
## collectContentPre ## collectContentPre
Called from: src/static/js/contentcollector.js Called from: src/static/js/contentcollector.js
Things in context: Things in context:
@ -238,16 +307,20 @@ Things in context:
4. styl - the style applied to the node (probably CSS) -- Note the typo 4. styl - the style applied to the node (probably CSS) -- Note the typo
5. cls - the HTML class string of the node 5. cls - the HTML class string of the node
This hook is called before the content of a node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. See, for example, the heading1 plugin for etherpad original. This hook is called before the content of a node is collected by the usual
methods. The cc object can be used to do a bunch of things that modify the
content of the pad. See, for example, the heading1 plugin for etherpad original.
E.g. if you need to apply an attribute to newly inserted characters, E.g. if you need to apply an attribute to newly inserted characters, call
call cc.doAttrib(state, "attributeName") which results in an attribute attributeName=true. cc.doAttrib(state, "attributeName") which results in an attribute
attributeName=true.
If you want to specify also a value, call cc.doAttrib(state, "attributeName::value") If you want to specify also a value, call cc.doAttrib(state,
which results in an attribute attributeName=value. "attributeName::value") which results in an attribute attributeName=value.
## collectContentImage ## collectContentImage
Called from: src/static/js/contentcollector.js Called from: src/static/js/contentcollector.js
Things in context: Things in context:
@ -259,7 +332,9 @@ Things in context:
5. cls - the HTML class string of the node 5. cls - the HTML class string of the node
6. node - the node being modified 6. node - the node being modified
This hook is called before the content of an image node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. This hook is called before the content of an image node is collected by the
usual methods. The cc object can be used to do a bunch of things that modify the
content of the pad.
Example: Example:
@ -271,6 +346,7 @@ exports.collectContentImage = function(name, context){
``` ```
## collectContentPost ## collectContentPost
Called from: src/static/js/contentcollector.js Called from: src/static/js/contentcollector.js
Things in context: Things in context:
@ -281,20 +357,29 @@ Things in context:
4. style - the style applied to the node (probably CSS) 4. style - the style applied to the node (probably CSS)
5. cls - the HTML class string of the node 5. cls - the HTML class string of the node
This hook is called after the content of a node is collected by the usual methods. The cc object can be used to do a bunch of things that modify the content of the pad. See, for example, the heading1 plugin for etherpad original. This hook is called after the content of a node is collected by the usual
methods. The cc object can be used to do a bunch of things that modify the
content of the pad. See, for example, the heading1 plugin for etherpad original.
## handleClientMessage_`name` ## handleClientMessage_`name`
Called from: `src/static/js/collab_client.js` Called from: `src/static/js/collab_client.js`
Things in context: Things in context:
1. payload - the data that got sent with the message (use it for custom message content) 1. payload - the data that got sent with the message (use it for custom message
content)
This hook gets called every time the client receives a message of type `name`. This can most notably be used with the new HTTP API call, "sendClientsMessage", which sends a custom message type to all clients connected to a pad. You can also use this to handle existing types. This hook gets called every time the client receives a message of type `name`.
This can most notably be used with the new HTTP API call, "sendClientsMessage",
which sends a custom message type to all clients connected to a pad. You can
also use this to handle existing types.
`collab_client.js` has a pretty extensive list of message types, if you want to take a look. `collab_client.js` has a pretty extensive list of message types, if you want to
take a look.
## aceStartLineAndCharForPoint-aceEndLineAndCharForPoint
##aceStartLineAndCharForPoint-aceEndLineAndCharForPoint
Called from: src/static/js/ace2_inner.js Called from: src/static/js/ace2_inner.js
Things in context: Things in context:
@ -306,10 +391,11 @@ Things in context:
5. point - the starting/ending element where the cursor highlights 5. point - the starting/ending element where the cursor highlights
6. documentAttributeManager - information about attributes in the document 6. documentAttributeManager - information about attributes in the document
This hook is provided to allow a plugin to turn DOM node selection into [line,char] selection. This hook is provided to allow a plugin to turn DOM node selection into
The return value should be an array of [line,char] [line,char] selection. The return value should be an array of [line,char]
## aceKeyEvent
##aceKeyEvent
Called from: src/static/js/ace2_inner.js Called from: src/static/js/ace2_inner.js
Things in context: Things in context:
@ -323,7 +409,8 @@ Things in context:
This hook is provided to allow a plugin to handle key events. This hook is provided to allow a plugin to handle key events.
The return value should be true if you have handled the event. The return value should be true if you have handled the event.
##collectContentLineText ## collectContentLineText
Called from: src/static/js/contentcollector.js Called from: src/static/js/contentcollector.js
Things in context: Things in context:
@ -333,10 +420,24 @@ Things in context:
3. tname - the tag name of this node currently being processed 3. tname - the tag name of this node currently being processed
4. text - the text for that line 4. text - the text for that line
This hook allows you to validate/manipulate the text before it's sent to the server side. This hook allows you to validate/manipulate the text before it's sent to the
The return value should be the validated/manipulated text. server side. To change the text, either:
* Set the `text` context property to the desired value and return `undefined`.
* (Deprecated) Return a string. If a hook function changes the `text` context
property, the return value is ignored. If no hook function changes `text` but
multiple hook functions return a string, the first one wins.
Example:
```
exports.collectContentLineText = (hookName, context) => {
context.text = tweakText(context.text);
};
```
## collectContentLineBreak
##collectContentLineBreak
Called from: src/static/js/contentcollector.js Called from: src/static/js/contentcollector.js
Things in context: Things in context:
@ -345,24 +446,27 @@ Things in context:
2. state - the current state of the change being made 2. state - the current state of the change being made
3. tname - the tag name of this node currently being processed 3. tname - the tag name of this node currently being processed
This hook is provided to allow whether the br tag should induce a new magic domline or not. This hook is provided to allow whether the br tag should induce a new magic
The return value should be either true(break the line) or false. domline or not. The return value should be either true(break the line) or false.
## disableAuthorColorsForThisLine
##disableAuthorColorsForThisLine
Called from: src/static/js/linestylefilter.js Called from: src/static/js/linestylefilter.js
Things in context: Things in context:
1. linestylefilter - the JavaScript object that's currently processing the ace attributes 1. linestylefilter - the JavaScript object that's currently processing the ace
attributes
2. text - the line text 2. text - the line text
3. class - line class 3. class - line class
This hook is provided to allow whether a given line should be deliniated with multiple authors. This hook is provided to allow whether a given line should be deliniated with
Multiple authors in one line cause the creation of magic span lines. This might not suit you and multiple authors. Multiple authors in one line cause the creation of magic span
now you can disable it and handle your own deliniation. lines. This might not suit you and now you can disable it and handle your own
The return value should be either true(disable) or false. deliniation. The return value should be either true(disable) or false.
## aceSetAuthorStyle ## aceSetAuthorStyle
Called from: src/static/js/ace2_inner.js Called from: src/static/js/ace2_inner.js
Things in context: Things in context:
@ -374,10 +478,12 @@ Things in context:
5. author - author info 5. author - author info
6. authorSelector - css selector for author span in inner ace 6. authorSelector - css selector for author span in inner ace
This hook is provided to allow author highlight style to be modified. This hook is provided to allow author highlight style to be modified. Registered
Registered hooks should return 1 if the plugin handles highlighting. If no plugin returns 1, the core will use the default background-based highlighting. hooks should return 1 if the plugin handles highlighting. If no plugin returns
1, the core will use the default background-based highlighting.
## aceSelectionChanged ## aceSelectionChanged
Called from: src/static/js/ace2_inner.js Called from: src/static/js/ace2_inner.js
Things in context: Things in context:

View file

@ -263,7 +263,7 @@ deletes a session
#### getSessionInfo(sessionID) #### getSessionInfo(sessionID)
* API >= 1 * API >= 1
returns informations about a session returns information about a session
*Example returns:* *Example returns:*
* `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}` * `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}`

View file

@ -168,6 +168,8 @@ For the editor container, you can also make it full width by adding `full-width-
| `IMPORT_MAX_FILE_SIZE` | maximum allowed file size when importing a pad, in bytes. | `52428800` (50 MB) | | `IMPORT_MAX_FILE_SIZE` | maximum allowed file size when importing a pad, in bytes. | `52428800` (50 MB) |
| `IMPORT_EXPORT_MAX_REQ_PER_IP` | maximum number of import/export calls per IP. | `10` | | `IMPORT_EXPORT_MAX_REQ_PER_IP` | maximum number of import/export calls per IP. | `10` |
| `IMPORT_EXPORT_RATE_LIMIT_WINDOW` | the call rate for import/export requests will be estimated in this time window (in milliseconds) | `90000` | | `IMPORT_EXPORT_RATE_LIMIT_WINDOW` | the call rate for import/export requests will be estimated in this time window (in milliseconds) | `90000` |
| `COMMIT_RATE_LIMIT_DURATION` | duration of the rate limit window for commits by individual users/IPs (in seconds) | `1` |
| `COMMIT_RATE_LIMIT_POINTS` | maximum number of changes per IP to allow during the rate limit window | `10` |
| `SUPPRESS_ERRORS_IN_PAD_TEXT` | Should we suppress errors from being visible in the default Pad Text? | `false` | | `SUPPRESS_ERRORS_IN_PAD_TEXT` | Should we suppress errors from being visible in the default Pad Text? | `false` |
| `REQUIRE_SESSION` | If this option is enabled, a user must have a session to access pads. This effectively allows only group pads to be accessed. | `false` | | `REQUIRE_SESSION` | If this option is enabled, a user must have a session to access pads. This effectively allows only group pads to be accessed. | `false` |
| `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` | | `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` |

View file

@ -11,5 +11,5 @@ heading.
Every `.html` file is generated based on the corresponding Every `.html` file is generated based on the corresponding
`.md` file in the `doc/api/` folder in the source tree. The `.md` file in the `doc/api/` folder in the source tree. The
documentation is generated using the `bin/doc/generate.js` program. documentation is generated using the `src/bin/doc/generate.js` program.
The HTML template is located at `doc/template.html`. The HTML template is located at `doc/template.html`.

View file

@ -95,7 +95,7 @@ For example, if you want to replace `Chat` with `Notes`, simply add...
## Customization for Administrators ## Customization for Administrators
As an Etherpad administrator, it is possible to overwrite core mesages as well as messages in plugins. These include error messages, labels, and user instructions. Whereas the localization in the source code is in separate files separated by locale, an administrator's custom localizations are in `settings.json` under the `customLocaleStrings` key, with each locale separated by a sub-key underneath. As an Etherpad administrator, it is possible to overwrite core messages as well as messages in plugins. These include error messages, labels, and user instructions. Whereas the localization in the source code is in separate files separated by locale, an administrator's custom localizations are in `settings.json` under the `customLocaleStrings` key, with each locale separated by a sub-key underneath.
For example, let's say you want to change the text on the "New Pad" button on Etherpad's home page. If you look in `locales/en.json` (or `locales/en-gb.json`) you'll see the key for this text is `"index.newPad"`. You could add the following to `settings.json`: For example, let's say you want to change the text on the "New Pad" button on Etherpad's home page. If you look in `locales/en.json` (or `locales/en-gb.json`) you'll see the key for this text is `"index.newPad"`. You could add the following to `settings.json`:

View file

@ -225,7 +225,7 @@ publish your plugin.
"author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>", "author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>",
"contributors": [], "contributors": [],
"dependencies": {"MODULE": "0.3.20"}, "dependencies": {"MODULE": "0.3.20"},
"engines": { "node": ">= 10.13.0"} "engines": { "node": "^10.17.0 || >=11.14.0"}
} }
``` ```

10704
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,105 +0,0 @@
{
"dependencies": {
"ep_etherpad-lite": "file:src"
},
"devDependencies": {
"eslint": "^7.15.0",
"eslint-config-etherpad": "^1.0.20",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-arrow": "^1.2.2",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0"
},
"eslintConfig": {
"ignorePatterns": [
"/src/",
"/tests/frontend/lib/"
],
"overrides": [
{
"files": [
"**/.eslintrc.js"
],
"extends": "etherpad/node"
},
{
"files": [
"**/*"
],
"excludedFiles": [
"**/.eslintrc.js",
"tests/frontend/**/*"
],
"extends": "etherpad/node"
},
{
"files": [
"tests/**/*"
],
"excludedFiles": [
"**/.eslintrc.js"
],
"extends": "etherpad/tests",
"rules": {
"mocha/no-exports": "off",
"mocha/no-top-level-hooks": "off"
}
},
{
"files": [
"tests/backend/**/*"
],
"excludedFiles": [
"**/.eslintrc.js"
],
"extends": "etherpad/tests/backend",
"overrides": [
{
"files": [
"tests/backend/**/*"
],
"excludedFiles": [
"tests/backend/specs/**/*"
],
"rules": {
"mocha/no-exports": "off",
"mocha/no-top-level-hooks": "off"
}
}
]
},
{
"files": [
"tests/frontend/**/*"
],
"excludedFiles": [
"**/.eslintrc.js"
],
"extends": "etherpad/tests/frontend",
"overrides": [
{
"files": [
"tests/frontend/**/*"
],
"excludedFiles": [
"tests/frontend/specs/**/*"
],
"rules": {
"mocha/no-exports": "off",
"mocha/no-top-level-hooks": "off"
}
}
]
}
],
"root": true
},
"scripts": {
"lint": "eslint ."
},
"engines": {
"node": ">=10.13.0"
}
}

View file

@ -171,7 +171,7 @@
* *
* *
* Database specific settings are dependent on dbType, and go in dbSettings. * Database specific settings are dependent on dbType, and go in dbSettings.
* Remember that since Etherpad 1.6.0 you can also store these informations in * Remember that since Etherpad 1.6.0 you can also store this information in
* credentials.json. * credentials.json.
* *
* For a complete list of the supported drivers, please refer to: * For a complete list of the supported drivers, please refer to:
@ -445,6 +445,17 @@
*/ */
"socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"], "socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"],
"socketIo": {
/*
* Maximum permitted client message size (in bytes). All messages from
* clients that are larger than this will be rejected. Large values make it
* possible to paste large amounts of text, and plugins may require a larger
* value to work properly, but increasing the value increases susceptibility
* to denial of service attacks (malicious clients can exhaust memory).
*/
"maxHttpBufferSize": 10000
},
/* /*
* Allow Load Testing tools to hit the Etherpad Instance. * Allow Load Testing tools to hit the Etherpad Instance.
* *
@ -486,6 +497,22 @@
*/ */
"importMaxFileSize": "${IMPORT_MAX_FILE_SIZE:52428800}", // 50 * 1024 * 1024 "importMaxFileSize": "${IMPORT_MAX_FILE_SIZE:52428800}", // 50 * 1024 * 1024
/*
* From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited
*
* The default is to allow at most 10 changes per IP in a 1 second window.
* After that the change is rejected.
*
* See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options
*/
"commitRateLimiting": {
// duration of the rate limit window (seconds)
"duration": "${COMMIT_RATE_LIMIT_DURATION:1}",
// maximum number of changes per IP to allow during the rate limit window
"points": "${COMMIT_RATE_LIMIT_POINTS:10}"
},
/* /*
* Toolbar buttons configuration. * Toolbar buttons configuration.
* *

View file

@ -162,7 +162,7 @@
* *
* *
* Database specific settings are dependent on dbType, and go in dbSettings. * Database specific settings are dependent on dbType, and go in dbSettings.
* Remember that since Etherpad 1.6.0 you can also store these informations in * Remember that since Etherpad 1.6.0 you can also store this information in
* credentials.json. * credentials.json.
* *
* For a complete list of the supported drivers, please refer to: * For a complete list of the supported drivers, please refer to:
@ -450,6 +450,17 @@
*/ */
"socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"], "socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"],
"socketIo": {
/*
* Maximum permitted client message size (in bytes). All messages from
* clients that are larger than this will be rejected. Large values make it
* possible to paste large amounts of text, and plugins may require a larger
* value to work properly, but increasing the value increases susceptibility
* to denial of service attacks (malicious clients can exhaust memory).
*/
"maxHttpBufferSize": 10000
},
/* /*
* Allow Load Testing tools to hit the Etherpad Instance. * Allow Load Testing tools to hit the Etherpad Instance.
* *
@ -492,7 +503,7 @@
"importMaxFileSize": 52428800, // 50 * 1024 * 1024 "importMaxFileSize": 52428800, // 50 * 1024 * 1024
/* /*
* From Etherpad 1.9.0 onwards, when Etherpad is in production mode commits from individual users are rate limited * From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited
* *
* The default is to allow at most 10 changes per IP in a 1 second window. * The default is to allow at most 10 changes per IP in a 1 second window.
* After that the change is rejected. * After that the change is rejected.
@ -503,7 +514,7 @@
// duration of the rate limit window (seconds) // duration of the rate limit window (seconds)
"duration": 1, "duration": 1,
// maximum number of chanes per IP to allow during the rate limit window // maximum number of changes per IP to allow during the rate limit window
"points": 10 "points": 10
}, },
@ -600,5 +611,8 @@
}, // logconfig }, // logconfig
/* Override any strings found in locale directories */ /* Override any strings found in locale directories */
"customLocaleStrings": {} "customLocaleStrings": {},
/* Disable Admin UI tests */
"enableAdminUITests": false
} }

View file

@ -14,7 +14,7 @@ rm -rf ${DIST}
mkdir -p ${DIST}/ mkdir -p ${DIST}/
rm -rf ${SRC} rm -rf ${SRC}
rsync -a bin/deb-src/ ${SRC}/ rsync -a src/bin/deb-src/ ${SRC}/
mkdir -p ${SYSROOT}/opt/ mkdir -p ${SYSROOT}/opt/
rsync --exclude '.git' -a . ${SYSROOT}/opt/etherpad/ --delete rsync --exclude '.git' -a . ${SYSROOT}/opt/etherpad/ --delete

View file

@ -32,7 +32,7 @@ rm -f etherpad-lite-win.zip
export NODE_ENV=production export NODE_ENV=production
log "do a normal unix install first..." log "do a normal unix install first..."
bin/installDeps.sh || exit 1 src/bin/installDeps.sh || exit 1
log "copy the windows settings template..." log "copy the windows settings template..."
cp settings.json.template settings.json cp settings.json.template settings.json
@ -43,8 +43,7 @@ rm -rf node_modules
mv node_modules_resolved node_modules mv node_modules_resolved node_modules
log "download windows node..." log "download windows node..."
cd bin wget "https://nodejs.org/dist/latest-erbium/win-x86/node.exe" -O node.exe
wget "https://nodejs.org/dist/latest-erbium/win-x86/node.exe" -O ../node.exe
log "remove git history to reduce folder size" log "remove git history to reduce folder size"
rm -rf .git/objects rm -rf .git/objects

88
src/bin/checkAllPads.js Normal file
View file

@ -0,0 +1,88 @@
'use strict';
/*
* This is a debug tool. It checks all revisions for data corruption
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
if (process.argv.length !== 2) throw new Error('Use: node src/bin/checkAllPads.js');
(async () => {
// initialize the database
require('../node/utils/Settings');
const db = require('../node/db/DB');
await db.init();
// load modules
const Changeset = require('../static/js/Changeset');
const padManager = require('../node/db/PadManager');
let revTestedCount = 0;
// get all pads
const res = await padManager.listAllPads();
for (const padId of res.padIDs) {
const pad = await padManager.getPad(padId);
// check if the pad has a pool
if (pad.pool == null) {
console.error(`[${pad.id}] Missing attribute pool`);
continue;
}
// create an array with key kevisions
// key revisions always save the full pad atext
const head = pad.getHeadRevisionNumber();
const keyRevisions = [];
for (let rev = 0; rev < head; rev += 100) {
keyRevisions.push(rev);
}
// run through all key revisions
for (const keyRev of keyRevisions) {
// create an array of revisions we need till the next keyRevision or the End
const revisionsNeeded = [];
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
revisionsNeeded.push(rev);
}
// this array will hold all revision changesets
const revisions = [];
// run through all needed revisions and get them from the database
for (const revNum of revisionsNeeded) {
const revision = await db.get(`pad:${pad.id}:revs:${revNum}`);
revisions[revNum] = revision;
}
// check if the revision exists
if (revisions[keyRev] == null) {
console.error(`[${pad.id}] Missing revision ${keyRev}`);
continue;
}
// check if there is a atext in the keyRevisions
let {meta: {atext} = {}} = revisions[keyRev];
if (atext == null) {
console.error(`[${pad.id}] Missing atext in revision ${keyRev}`);
continue;
}
const apool = pad.pool;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
try {
const cs = revisions[rev].changeset;
atext = Changeset.applyToAText(cs, atext, apool);
revTestedCount++;
} catch (e) {
console.error(`[${pad.id}] Bad changeset at revision ${rev} - ${e.message}`);
}
}
}
}
if (revTestedCount === 0) {
throw new Error('No revisions tested');
}
console.log(`Finished: Tested ${revTestedCount} revisions`);
})();

82
src/bin/checkPad.js Normal file
View file

@ -0,0 +1,82 @@
'use strict';
/*
* This is a debug tool. It checks all revisions for data corruption
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
if (process.argv.length !== 3) throw new Error('Use: node src/bin/checkPad.js $PADID');
// get the padID
const padId = process.argv[2];
let checkRevisionCount = 0;
(async () => {
// initialize database
require('../node/utils/Settings');
const db = require('../node/db/DB');
await db.init();
// load modules
const Changeset = require('../static/js/Changeset');
const padManager = require('../node/db/PadManager');
const exists = await padManager.doesPadExists(padId);
if (!exists) throw new Error('Pad does not exist');
// get the pad
const pad = await padManager.getPad(padId);
// create an array with key revisions
// key revisions always save the full pad atext
const head = pad.getHeadRevisionNumber();
const keyRevisions = [];
for (let rev = 0; rev < head; rev += 100) {
keyRevisions.push(rev);
}
// run through all key revisions
for (let keyRev of keyRevisions) {
keyRev = parseInt(keyRev);
// create an array of revisions we need till the next keyRevision or the End
const revisionsNeeded = [];
for (let rev = keyRev; rev <= keyRev + 100 && rev <= head; rev++) {
revisionsNeeded.push(rev);
}
// this array will hold all revision changesets
const revisions = [];
// run through all needed revisions and get them from the database
for (const revNum of revisionsNeeded) {
const revision = await db.get(`pad:${padId}:revs:${revNum}`);
revisions[revNum] = revision;
}
// check if the pad has a pool
if (pad.pool == null) throw new Error('Attribute pool is missing');
// check if there is an atext in the keyRevisions
let {meta: {atext} = {}} = revisions[keyRev] || {};
if (atext == null) {
console.error(`No atext in key revision ${keyRev}`);
continue;
}
const apool = pad.pool;
for (let rev = keyRev + 1; rev <= keyRev + 100 && rev <= head; rev++) {
checkRevisionCount++;
try {
const cs = revisions[rev].changeset;
atext = Changeset.applyToAText(cs, atext, apool);
} catch (e) {
console.error(`Bad changeset at revision ${rev} - ${e.message}`);
continue;
}
}
console.log(`Finished: Checked ${checkRevisionCount} revisions`);
}
})();

103
src/bin/checkPadDeltas.js Normal file
View file

@ -0,0 +1,103 @@
'use strict';
/*
* This is a debug tool. It checks all revisions for data corruption
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
if (process.argv.length !== 3) throw new Error('Use: node src/bin/checkPadDeltas.js $PADID');
// get the padID
const padId = process.argv[2];
const expect = require('../tests/frontend/lib/expect');
const diff = require('diff');
(async () => {
// initialize database
require('../node/utils/Settings');
const db = require('../node/db/DB');
await db.init();
// load modules
const Changeset = require('../static/js/Changeset');
const padManager = require('../node/db/PadManager');
const exists = await padManager.doesPadExists(padId);
if (!exists) throw new Error('Pad does not exist');
// get the pad
const pad = await padManager.getPad(padId);
// create an array with key revisions
// key revisions always save the full pad atext
const head = pad.getHeadRevisionNumber();
const keyRevisions = [];
for (let i = 0; i < head; i += 100) {
keyRevisions.push(i);
}
// create an array with all revisions
const revisions = [];
for (let i = 0; i <= head; i++) {
revisions.push(i);
}
let atext = Changeset.makeAText('\n');
// run through all revisions
for (const revNum of revisions) {
// console.log('Fetching', revNum)
const revision = await db.get(`pad:${padId}:revs:${revNum}`);
// check if there is a atext in the keyRevisions
const {meta: {atext: revAtext} = {}} = revision || {};
if (~keyRevisions.indexOf(revNum) && revAtext == null) {
console.error(`No atext in key revision ${revNum}`);
continue;
}
// try glue everything together
try {
// console.log("check revision ", revNum);
const cs = revision.changeset;
atext = Changeset.applyToAText(cs, atext, pad.pool);
} catch (e) {
console.error(`Bad changeset at revision ${revNum} - ${e.message}`);
continue;
}
// check things are working properly
if (~keyRevisions.indexOf(revNum)) {
try {
expect(revision.meta.atext.text).to.eql(atext.text);
expect(revision.meta.atext.attribs).to.eql(atext.attribs);
} catch (e) {
console.error(`Atext in key revision ${revNum} doesn't match computed one.`);
console.log(diff.diffChars(atext.text, revision.meta.atext.text).map((op) => {
if (!op.added && !op.removed) op.value = op.value.length;
return op;
}));
// console.error(e)
// console.log('KeyRev. :', revision.meta.atext)
// console.log('Computed:', atext)
continue;
}
}
}
// check final text is right...
if (pad.atext.text === atext.text) {
console.log('ok');
} else {
console.error('Pad AText doesn\'t match computed one! (Computed ',
atext.text.length, ', db', pad.atext.text.length, ')');
console.log(diff.diffChars(atext.text, pad.atext.text).map((op) => {
if (!op.added && !op.removed) {
op.value = op.value.length;
return op;
}
}));
}
})();

View file

@ -1,15 +1,11 @@
#!/bin/sh #!/bin/sh
# Move to the folder where ep-lite is installed # Move to the Etherpad base directory.
cd "$(dirname "$0")"/.. MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
cd "${MY_DIR}/../.." || exit 1
# Source constants and usefull functions # Source constants and useful functions
. bin/functions.sh . src/bin/functions.sh
#Was this script started in the bin folder? if yes move out
if [ -d "../bin" ]; then
cd "../"
fi
ignoreRoot=0 ignoreRoot=0
for ARG in "$@" for ARG in "$@"
@ -35,7 +31,7 @@ fi
rm -rf src/node_modules rm -rf src/node_modules
#Prepare the environment #Prepare the environment
bin/installDeps.sh "$@" || exit 1 src/bin/installDeps.sh "$@" || exit 1
#Move to the node folder and start #Move to the node folder and start
echo "Started Etherpad..." echo "Started Etherpad..."

View file

@ -134,7 +134,7 @@ function create_builds {
git clone $ETHER_WEB_REPO git clone $ETHER_WEB_REPO
echo "Creating windows build..." echo "Creating windows build..."
cd etherpad-lite cd etherpad-lite
bin/buildForWindows.sh src/bin/buildForWindows.sh
[[ $? != 0 ]] && echo "Aborting: Error creating build for windows" && exit 1 [[ $? != 0 ]] && echo "Aborting: Error creating build for windows" && exit 1
echo "Creating docs..." echo "Creating docs..."
make docs make docs

View file

@ -1,20 +1,24 @@
'use strict';
/* /*
* A tool for generating a test user session which can be used for debugging configs * A tool for generating a test user session which can be used for debugging configs
* that require sessions. * that require sessions.
*/ */
const m = (f) => `${__dirname}/../${f}`;
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const querystring = require('querystring'); const querystring = require('querystring');
const request = require(m('src/node_modules/request')); const settings = require('../node/utils/Settings');
const settings = require(m('src/node/utils/Settings')); const supertest = require('supertest');
const supertest = require(m('src/node_modules/supertest'));
(async () => { (async () => {
const api = supertest(`http://${settings.ip}:${settings.port}`); const api = supertest(`http://${settings.ip}:${settings.port}`);
const filePath = path.join(__dirname, '../APIKEY.txt'); const filePath = path.join(__dirname, '../../APIKEY.txt');
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'}); const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
let res; let res;

View file

@ -15,7 +15,7 @@ pre-start script
chown $EPUSER $EPLOGS ||true chown $EPUSER $EPLOGS ||true
chmod 0755 $EPLOGS ||true chmod 0755 $EPLOGS ||true
chown -R $EPUSER $EPHOME/var ||true chown -R $EPUSER $EPHOME/var ||true
$EPHOME/bin/installDeps.sh >> $EPLOGS/error.log || { stop; exit 1; } $EPHOME/src/bin/installDeps.sh >> $EPLOGS/error.log || { stop; exit 1; }
end script end script
script script

View file

@ -1,13 +1,14 @@
#!/bin/sh #!/bin/sh
# Move to the folder where ep-lite is installed # Move to the Etherpad base directory.
cd "$(dirname "$0")"/.. MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
cd "${MY_DIR}/../.." || exit 1
# Source constants and usefull functions # Source constants and useful functions
. bin/functions.sh . src/bin/functions.sh
# Prepare the environment # Prepare the environment
bin/installDeps.sh || exit 1 src/bin/installDeps.sh || exit 1
echo "If you are new to debugging Node.js with Chrome DevTools, take a look at this page:" echo "If you are new to debugging Node.js with Chrome DevTools, take a look at this page:"
echo "https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27" echo "https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27"

View file

@ -0,0 +1,47 @@
'use strict';
/*
* A tool for deleting ALL GROUP sessions Etherpad user sessions from the CLI,
* because sometimes a brick is required to fix a face.
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
const path = require('path');
const fs = require('fs');
const supertest = require('supertest');
// Set a delete counter which will increment on each delete attempt
// TODO: Check delete is successful before incrementing
let deleteCount = 0;
// get the API Key
const filePath = path.join(__dirname, '../../APIKEY.txt');
console.log('Deleting all group sessions, please be patient.');
(async () => {
const settings = require('../tests/container/loadSettings').loadSettings();
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
const api = supertest(`http://${settings.ip}:${settings.port}`);
const apiVersionResponse = await api.get('/api/');
const apiVersion = apiVersionResponse.body.currentVersion; // 1.12.5
const groupsResponse = await api.get(`/api/${apiVersion}/listAllGroups?apikey=${apikey}`);
const groups = groupsResponse.body.data.groupIDs; // ['whateverGroupID']
for (const groupID of groups) {
const sessionURI = `/api/${apiVersion}/listSessionsOfGroup?apikey=${apikey}&groupID=${groupID}`;
const sessionsResponse = await api.get(sessionURI);
const sessions = sessionsResponse.body.data;
for (const sessionID of Object.keys(sessions)) {
const deleteURI = `/api/${apiVersion}/deleteSession?apikey=${apikey}&sessionID=${sessionID}`;
await api.post(deleteURI); // delete
deleteCount++;
}
}
console.log(`Deleted ${deleteCount} sessions`);
})();

38
src/bin/deletePad.js Normal file
View file

@ -0,0 +1,38 @@
'use strict';
/*
* A tool for deleting pads from the CLI, because sometimes a brick is required
* to fix a window.
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
const settings = require('../tests/container/loadSettings').loadSettings();
const path = require('path');
const fs = require('fs');
const supertest = require('supertest');
const api = supertest(`http://${settings.ip}:${settings.port}`);
if (process.argv.length !== 3) throw new Error('Use: node deletePad.js $PADID');
// get the padID
const padId = process.argv[2];
// get the API Key
const filePath = path.join(__dirname, '../../APIKEY.txt');
const apikey = fs.readFileSync(filePath, {encoding: 'utf-8'});
(async () => {
let apiVersion = await api.get('/api/');
apiVersion = apiVersion.body.currentVersion;
if (!apiVersion) throw new Error('No version set in API');
// Now we know the latest API version, let's delete pad
const uri = `/api/${apiVersion}/deletePad?apikey=${apikey}&padID=${padId}`;
const deleteAttempt = await api.post(uri);
if (deleteAttempt.body.code === 1) throw new Error(`Error deleting pad ${deleteAttempt.body}`);
console.log('Deleted pad', deleteAttempt.body);
})();

View file

@ -72,5 +72,5 @@ Each type of heading has a description block.
Run the following from the etherpad-lite root directory: Run the following from the etherpad-lite root directory:
```sh ```sh
$ node bin/doc/generate doc/index.md --format=html --template=doc/template.html > out.html $ node src/bin/doc/generate doc/index.md --format=html --template=doc/template.html > out.html
``` ```

View file

@ -1,4 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // Permission is hereby granted, free of charge, to any person obtaining a
@ -20,7 +23,6 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE. // USE OR OTHER DEALINGS IN THE SOFTWARE.
const marked = require('marked');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -33,12 +35,12 @@ let template = null;
let inputFile = null; let inputFile = null;
args.forEach((arg) => { args.forEach((arg) => {
if (!arg.match(/^\-\-/)) { if (!arg.match(/^--/)) {
inputFile = arg; inputFile = arg;
} else if (arg.match(/^\-\-format=/)) { } else if (arg.match(/^--format=/)) {
format = arg.replace(/^\-\-format=/, ''); format = arg.replace(/^--format=/, '');
} else if (arg.match(/^\-\-template=/)) { } else if (arg.match(/^--template=/)) {
template = arg.replace(/^\-\-template=/, ''); template = arg.replace(/^--template=/, '');
} }
}); });
@ -56,11 +58,11 @@ fs.readFile(inputFile, 'utf8', (er, input) => {
}); });
const includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi; const includeExpr = /^@include\s+([A-Za-z0-9-_/]+)(?:\.)?([a-zA-Z]*)$/gmi;
const includeData = {}; const includeData = {};
function processIncludes(inputFile, input, cb) { const processIncludes = (inputFile, input, cb) => {
const includes = input.match(includeExpr); const includes = input.match(includeExpr);
if (includes === null) return cb(null, input); if (includes == null) return cb(null, input);
let errState = null; let errState = null;
console.error(includes); console.error(includes);
let incCount = includes.length; let incCount = includes.length;
@ -70,7 +72,7 @@ function processIncludes(inputFile, input, cb) {
let fname = include.replace(/^@include\s+/, ''); let fname = include.replace(/^@include\s+/, '');
if (!fname.match(/\.md$/)) fname += '.md'; if (!fname.match(/\.md$/)) fname += '.md';
if (includeData.hasOwnProperty(fname)) { if (Object.prototype.hasOwnProperty.call(includeData, fname)) {
input = input.split(include).join(includeData[fname]); input = input.split(include).join(includeData[fname]);
incCount--; incCount--;
if (incCount === 0) { if (incCount === 0) {
@ -94,10 +96,10 @@ function processIncludes(inputFile, input, cb) {
}); });
}); });
}); });
} };
function next(er, input) { const next = (er, input) => {
if (er) throw er; if (er) throw er;
switch (format) { switch (format) {
case 'json': case 'json':
@ -117,4 +119,4 @@ function next(er, input) {
default: default:
throw new Error(`Invalid format: ${format}`); throw new Error(`Invalid format: ${format}`);
} }
} };

View file

@ -1,3 +1,5 @@
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // Permission is hereby granted, free of charge, to any person obtaining a
@ -23,17 +25,17 @@ const fs = require('fs');
const marked = require('marked'); const marked = require('marked');
const path = require('path'); const path = require('path');
module.exports = toHTML;
function toHTML(input, filename, template, cb) { const toHTML = (input, filename, template, cb) => {
const lexed = marked.lexer(input); const lexed = marked.lexer(input);
fs.readFile(template, 'utf8', (er, template) => { fs.readFile(template, 'utf8', (er, template) => {
if (er) return cb(er); if (er) return cb(er);
render(lexed, filename, template, cb); render(lexed, filename, template, cb);
}); });
} };
module.exports = toHTML;
function render(lexed, filename, template, cb) { const render = (lexed, filename, template, cb) => {
// get the section // get the section
const section = getSection(lexed); const section = getSection(lexed);
@ -52,23 +54,23 @@ function render(lexed, filename, template, cb) {
// content has to be the last thing we do with // content has to be the last thing we do with
// the lexed tokens, because it's destructive. // the lexed tokens, because it's destructive.
content = marked.parser(lexed); const content = marked.parser(lexed);
template = template.replace(/__CONTENT__/g, content); template = template.replace(/__CONTENT__/g, content);
cb(null, template); cb(null, template);
}); });
} };
// just update the list item text in-place. // just update the list item text in-place.
// lists that come right after a heading are what we're after. // lists that come right after a heading are what we're after.
function parseLists(input) { const parseLists = (input) => {
let state = null; let state = null;
let depth = 0; let depth = 0;
const output = []; const output = [];
output.links = input.links; output.links = input.links;
input.forEach((tok) => { input.forEach((tok) => {
if (state === null) { if (state == null) {
if (tok.type === 'heading') { if (tok.type === 'heading') {
state = 'AFTERHEADING'; state = 'AFTERHEADING';
} }
@ -112,29 +114,27 @@ function parseLists(input) {
}); });
return output; return output;
} };
function parseListItem(text) { const parseListItem = (text) => {
text = text.replace(/\{([^\}]+)\}/, '<span class="type">$1</span>'); text = text.replace(/\{([^}]+)\}/, '<span class="type">$1</span>');
// XXX maybe put more stuff here? // XXX maybe put more stuff here?
return text; return text;
} };
// section is just the first heading // section is just the first heading
function getSection(lexed) { const getSection = (lexed) => {
const section = '';
for (let i = 0, l = lexed.length; i < l; i++) { for (let i = 0, l = lexed.length; i < l; i++) {
const tok = lexed[i]; const tok = lexed[i];
if (tok.type === 'heading') return tok.text; if (tok.type === 'heading') return tok.text;
} }
return ''; return '';
} };
function buildToc(lexed, filename, cb) { const buildToc = (lexed, filename, cb) => {
const indent = 0;
let toc = []; let toc = [];
let depth = 0; let depth = 0;
lexed.forEach((tok) => { lexed.forEach((tok) => {
@ -155,18 +155,18 @@ function buildToc(lexed, filename, cb) {
toc = marked.parse(toc.join('\n')); toc = marked.parse(toc.join('\n'));
cb(null, toc); cb(null, toc);
} };
const idCounters = {}; const idCounters = {};
function getId(text) { const getId = (text) => {
text = text.toLowerCase(); text = text.toLowerCase();
text = text.replace(/[^a-z0-9]+/g, '_'); text = text.replace(/[^a-z0-9]+/g, '_');
text = text.replace(/^_+|_+$/, ''); text = text.replace(/^_+|_+$/, '');
text = text.replace(/^([^a-z])/, '_$1'); text = text.replace(/^([^a-z])/, '_$1');
if (idCounters.hasOwnProperty(text)) { if (Object.prototype.hasOwnProperty.call(idCounters, text)) {
text += `_${++idCounters[text]}`; text += `_${++idCounters[text]}`;
} else { } else {
idCounters[text] = 0; idCounters[text] = 0;
} }
return text; return text;
} };

View file

@ -1,3 +1,4 @@
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // Permission is hereby granted, free of charge, to any person obtaining a
@ -26,7 +27,7 @@ module.exports = doJSON;
const marked = require('marked'); const marked = require('marked');
function doJSON(input, filename, cb) { const doJSON = (input, filename, cb) => {
const root = {source: filename}; const root = {source: filename};
const stack = [root]; const stack = [root];
let depth = 0; let depth = 0;
@ -40,7 +41,7 @@ function doJSON(input, filename, cb) {
// <!-- type = module --> // <!-- type = module -->
// This is for cases where the markdown semantic structure is lacking. // This is for cases where the markdown semantic structure is lacking.
if (type === 'paragraph' || type === 'html') { if (type === 'paragraph' || type === 'html') {
const metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g; const metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g;
text = text.replace(metaExpr, (_0, k, v) => { text = text.replace(metaExpr, (_0, k, v) => {
current[k.trim()] = v.trim(); current[k.trim()] = v.trim();
return ''; return '';
@ -146,7 +147,7 @@ function doJSON(input, filename, cb) {
} }
return cb(null, root); return cb(null, root);
} };
// go from something like this: // go from something like this:
@ -191,7 +192,7 @@ function doJSON(input, filename, cb) {
// desc: 'whether or not to send output to parent\'s stdio.', // desc: 'whether or not to send output to parent\'s stdio.',
// default: 'false' } ] } ] // default: 'false' } ] } ]
function processList(section) { const processList = (section) => {
const list = section.list; const list = section.list;
const values = []; const values = [];
let current; let current;
@ -203,13 +204,13 @@ function processList(section) {
if (type === 'space') return; if (type === 'space') return;
if (type === 'list_item_start') { if (type === 'list_item_start') {
if (!current) { if (!current) {
var n = {}; const n = {};
values.push(n); values.push(n);
current = n; current = n;
} else { } else {
current.options = current.options || []; current.options = current.options || [];
stack.push(current); stack.push(current);
var n = {}; const n = {};
current.options.push(n); current.options.push(n);
current = n; current = n;
} }
@ -247,11 +248,11 @@ function processList(section) {
switch (section.type) { switch (section.type) {
case 'ctor': case 'ctor':
case 'classMethod': case 'classMethod':
case 'method': case 'method': {
// each item is an argument, unless the name is 'return', // each item is an argument, unless the name is 'return',
// in which case it's the return value. // in which case it's the return value.
section.signatures = section.signatures || []; section.signatures = section.signatures || [];
var sig = {}; const sig = {};
section.signatures.push(sig); section.signatures.push(sig);
sig.params = values.filter((v) => { sig.params = values.filter((v) => {
if (v.name === 'return') { if (v.name === 'return') {
@ -262,11 +263,11 @@ function processList(section) {
}); });
parseSignature(section.textRaw, sig); parseSignature(section.textRaw, sig);
break; break;
}
case 'property': case 'property': {
// there should be only one item, which is the value. // there should be only one item, which is the value.
// copy the data up to the section. // copy the data up to the section.
var value = values[0] || {}; const value = values[0] || {};
delete value.name; delete value.name;
section.typeof = value.type; section.typeof = value.type;
delete value.type; delete value.type;
@ -274,20 +275,21 @@ function processList(section) {
section[k] = value[k]; section[k] = value[k];
}); });
break; break;
}
case 'event': case 'event': {
// event: each item is an argument. // event: each item is an argument.
section.params = values; section.params = values;
break; break;
}
} }
// section.listParsed = values;
delete section.list; delete section.list;
} };
// textRaw = "someobject.someMethod(a, [b=100], [c])" // textRaw = "someobject.someMethod(a, [b=100], [c])"
function parseSignature(text, sig) { const parseSignature = (text, sig) => {
let params = text.match(paramExpr); let params = text.match(paramExpr);
if (!params) return; if (!params) return;
params = params[1]; params = params[1];
@ -322,10 +324,10 @@ function parseSignature(text, sig) {
if (optional) param.optional = true; if (optional) param.optional = true;
if (def !== undefined) param.default = def; if (def !== undefined) param.default = def;
}); });
} };
function parseListItem(item) { const parseListItem = (item) => {
if (item.options) item.options.forEach(parseListItem); if (item.options) item.options.forEach(parseListItem);
if (!item.textRaw) return; if (!item.textRaw) return;
@ -341,7 +343,7 @@ function parseListItem(item) {
item.name = 'return'; item.name = 'return';
text = text.replace(retExpr, ''); text = text.replace(retExpr, '');
} else { } else {
const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/;
const name = text.match(nameExpr); const name = text.match(nameExpr);
if (name) { if (name) {
item.name = name[1]; item.name = name[1];
@ -358,7 +360,7 @@ function parseListItem(item) {
} }
text = text.trim(); text = text.trim();
const typeExpr = /^\{([^\}]+)\}/; const typeExpr = /^\{([^}]+)\}/;
const type = text.match(typeExpr); const type = text.match(typeExpr);
if (type) { if (type) {
item.type = type[1]; item.type = type[1];
@ -376,10 +378,10 @@ function parseListItem(item) {
text = text.replace(/^\s*-\s*/, ''); text = text.replace(/^\s*-\s*/, '');
text = text.trim(); text = text.trim();
if (text) item.desc = text; if (text) item.desc = text;
} };
function finishSection(section, parent) { const finishSection = (section, parent) => {
if (!section || !parent) { if (!section || !parent) {
throw new Error(`Invalid finishSection call\n${ throw new Error(`Invalid finishSection call\n${
JSON.stringify(section)}\n${ JSON.stringify(section)}\n${
@ -416,7 +418,7 @@ function finishSection(section, parent) {
ctor.signatures.forEach((sig) => { ctor.signatures.forEach((sig) => {
sig.desc = ctor.desc; sig.desc = ctor.desc;
}); });
sigs.push.apply(sigs, ctor.signatures); sigs.push(...ctor.signatures);
}); });
delete section.ctors; delete section.ctors;
} }
@ -479,50 +481,50 @@ function finishSection(section, parent) {
parent[plur] = parent[plur] || []; parent[plur] = parent[plur] || [];
parent[plur].push(section); parent[plur].push(section);
} };
// Not a general purpose deep copy. // Not a general purpose deep copy.
// But sufficient for these basic things. // But sufficient for these basic things.
function deepCopy(src, dest) { const deepCopy = (src, dest) => {
Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => { Object.keys(src).filter((k) => !Object.prototype.hasOwnProperty.call(dest, k)).forEach((k) => {
dest[k] = deepCopy_(src[k]); dest[k] = deepCopy_(src[k]);
}); });
} };
function deepCopy_(src) { const deepCopy_ = (src) => {
if (!src) return src; if (!src) return src;
if (Array.isArray(src)) { if (Array.isArray(src)) {
var c = new Array(src.length); const c = new Array(src.length);
src.forEach((v, i) => { src.forEach((v, i) => {
c[i] = deepCopy_(v); c[i] = deepCopy_(v);
}); });
return c; return c;
} }
if (typeof src === 'object') { if (typeof src === 'object') {
var c = {}; const c = {};
Object.keys(src).forEach((k) => { Object.keys(src).forEach((k) => {
c[k] = deepCopy_(src[k]); c[k] = deepCopy_(src[k]);
}); });
return c; return c;
} }
return src; return src;
} };
// these parse out the contents of an H# tag // these parse out the contents of an H# tag
const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i;
const classExpr = /^Class:\s*([^ ]+).*?$/i; const classExpr = /^Class:\s*([^ ]+).*?$/i;
const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; const propExpr = /^(?:property:?\s*)?[^.]+\.([^ .()]+)\s*?$/i;
const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; const braceExpr = /^(?:property:?\s*)?[^.[]+(\[[^\]]+\])\s*?$/i;
const classMethExpr = const classMethExpr =
/^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; /^class\s*method\s*:?[^.]+\.([^ .()]+)\([^)]*\)\s*?$/i;
const methExpr = const methExpr =
/^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; /^(?:method:?\s*)?(?:[^.]+\.)?([^ .()]+)\([^)]*\)\s*?$/i;
const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; const newExpr = /^new ([A-Z][a-z]+)\([^)]*\)\s*?$/;
var paramExpr = /\((.*)\);?$/; const paramExpr = /\((.*)\);?$/;
function newSection(tok) { const newSection = (tok) => {
const section = {}; const section = {};
// infer the type from the text. // infer the type from the text.
const text = section.textRaw = tok.text; const text = section.textRaw = tok.text;
@ -551,4 +553,4 @@ function newSection(tok) {
section.name = text; section.name = text;
} }
return section; return section;
} };

View file

@ -4,7 +4,7 @@
"description": "Internal tool for generating Node.js API docs", "description": "Internal tool for generating Node.js API docs",
"version": "0.0.0", "version": "0.0.0",
"engines": { "engines": {
"node": ">=0.6.10" "node": ">=10.17.0"
}, },
"dependencies": { "dependencies": {
"marked": "0.8.2" "marked": "0.8.2"

64
src/bin/extractPadData.js Normal file
View file

@ -0,0 +1,64 @@
'use strict';
/*
* This is a debug tool. It helps to extract all datas of a pad and move it from
* a productive environment and to a develop environment to reproduce bugs
* there. It outputs a dirtydb file
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
if (process.argv.length !== 3) throw new Error('Use: node extractPadData.js $PADID');
// get the padID
const padId = process.argv[2];
(async () => {
// initialize database
require('../node/utils/Settings');
const db = require('../node/db/DB');
await db.init();
// load extra modules
const dirtyDB = require('dirty');
const padManager = require('../node/db/PadManager');
// initialize output database
const dirty = dirtyDB(`${padId}.db`);
// Promise wrapped get and set function
const wrapped = db.db.db.wrappedDB;
const get = util.promisify(wrapped.get.bind(wrapped));
const set = util.promisify(dirty.set.bind(dirty));
// array in which required key values will be accumulated
const neededDBValues = [`pad:${padId}`];
// get the actual pad object
const pad = await padManager.getPad(padId);
// add all authors
neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`));
// add all revisions
for (let rev = 0; rev <= pad.head; ++rev) {
neededDBValues.push(`pad:${padId}:revs:${rev}`);
}
// add all chat values
for (let chat = 0; chat <= pad.chatHead; ++chat) {
neededDBValues.push(`pad:${padId}:chat:${chat}`);
}
for (const dbkey of neededDBValues) {
let dbvalue = await get(dbkey);
if (dbvalue && typeof dbvalue !== 'object') {
dbvalue = JSON.parse(dbvalue);
}
await set(dbkey, dbvalue);
}
console.log('finished');
})();

22
src/bin/fastRun.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
#
# Run Etherpad directly, assuming all the dependencies are already installed.
#
# Useful for developers, or users that know what they are doing. If you just
# upgraded Etherpad version, installed a new dependency, or are simply unsure
# of what to do, please execute bin/installDeps.sh once before running this
# script.
set -eu
# Move to the Etherpad base directory.
MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
cd "${MY_DIR}/../.." || exit 1
# Source constants and useful functions
. src/bin/functions.sh
echo "Running directly, without checking/installing dependencies"
# run Etherpad main class
node $(compute_node_args) "node_modules/ep_etherpad-lite/node/server.js" "$@"

100
src/bin/importSqlFile.js Normal file
View file

@ -0,0 +1,100 @@
'use strict';
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
const startTime = Date.now();
const log = (str) => {
console.log(`${(Date.now() - startTime) / 1000}\t${str}`);
};
const unescape = (val) => {
// value is a string
if (val.substr(0, 1) === "'") {
val = val.substr(0, val.length - 1).substr(1);
return val.replace(/\\[0nrbtZ\\'"]/g, (s) => {
switch (s) {
case '\\0': return '\0';
case '\\n': return '\n';
case '\\r': return '\r';
case '\\b': return '\b';
case '\\t': return '\t';
case '\\Z': return '\x1a';
default: return s.substr(1);
}
});
}
// value is a boolean or NULL
if (val === 'NULL') {
return null;
}
if (val === 'true') {
return true;
}
if (val === 'false') {
return false;
}
// value is a number
return val;
};
(async () => {
const fs = require('fs');
const log4js = require('log4js');
const settings = require('../node/utils/Settings');
const ueberDB = require('ueberdb2');
const dbWrapperSettings = {
cache: 0,
writeInterval: 100,
json: false, // data is already json encoded
};
const db = new ueberDB.database( // eslint-disable-line new-cap
settings.dbType,
settings.dbSettings,
dbWrapperSettings,
log4js.getLogger('ueberDB'));
const sqlFile = process.argv[2];
// stop if the settings file is not set
if (!sqlFile) throw new Error('Use: node importSqlFile.js $SQLFILE');
log('initializing db');
await util.promisify(db.init.bind(db))();
log('done');
log('open output file...');
const lines = fs.readFileSync(sqlFile, 'utf8').split('\n');
const count = lines.length;
let keyNo = 0;
process.stdout.write(`Start importing ${count} keys...\n`);
lines.forEach((l) => {
if (l.substr(0, 27) === 'REPLACE INTO store VALUES (') {
const pos = l.indexOf("', '");
const key = l.substr(28, pos - 28);
let value = l.substr(pos + 3);
value = value.substr(0, value.length - 2);
console.log(`key: ${key} val: ${value}`);
console.log(`unval: ${unescape(value)}`);
db.set(key, unescape(value), null);
keyNo++;
if (keyNo % 1000 === 0) {
process.stdout.write(` ${keyNo}/${count}\n`);
}
}
});
process.stdout.write('\n');
process.stdout.write('done. waiting for db to finish transaction. ' +
'depended on dbms this may take some time..\n');
await util.promisify(db.close.bind(db))();
log(`finished, imported ${keyNo} keys.`);
})();

View file

@ -1,10 +1,11 @@
#!/bin/sh #!/bin/sh
# Move to the folder where ep-lite is installed # Move to the Etherpad base directory.
cd "$(dirname "$0")"/.. MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
cd "${MY_DIR}/../.." || exit 1
# Source constants and usefull functions # Source constants and useful functions
. bin/functions.sh . src/bin/functions.sh
# Is node installed? # Is node installed?
# Not checking io.js, default installation creates a symbolic link to node # Not checking io.js, default installation creates a symbolic link to node
@ -39,7 +40,7 @@ log "Ensure that all dependencies are up to date... If this is the first time y
cd node_modules cd node_modules
[ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite [ -e ep_etherpad-lite ] || ln -s ../src ep_etherpad-lite
cd ep_etherpad-lite cd ep_etherpad-lite
npm ci npm ci --no-optional
) || { ) || {
rm -rf src/node_modules rm -rf src/node_modules
exit 1 exit 1

View file

@ -0,0 +1,56 @@
'use strict';
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
(async () => {
// This script requires that you have modified your settings.json file
// to work with a real database. Please make a backup of your dirty.db
// file before using this script, just to be safe.
// It might be necessary to run the script using more memory:
// `node --max-old-space-size=4096 src/bin/migrateDirtyDBtoRealDB.js`
const dirtyDb = require('dirty');
const log4js = require('log4js');
const settings = require('../node/utils/Settings');
const ueberDB = require('ueberdb2');
const util = require('util');
const dbWrapperSettings = {
cache: '0', // The cache slows things down when you're mostly writing.
writeInterval: 0, // Write directly to the database, don't buffer
};
const db = new ueberDB.database( // eslint-disable-line new-cap
settings.dbType,
settings.dbSettings,
dbWrapperSettings,
log4js.getLogger('ueberDB'));
await db.init();
console.log('Waiting for dirtyDB to parse its file.');
const dirty = dirtyDb(`${__dirname}/../../var/dirty.db`);
const length = await new Promise((resolve) => { dirty.once('load', resolve); });
console.log(`Found ${length} records, processing now.`);
const p = [];
let numWritten = 0;
dirty.forEach((key, value) => {
let bcb, wcb;
p.push(new Promise((resolve, reject) => {
bcb = (err) => { if (err != null) return reject(err); };
wcb = (err) => {
if (err != null) return reject(err);
if (++numWritten % 100 === 0) console.log(`Wrote record ${numWritten} of ${length}`);
resolve();
};
}));
db.set(key, value, bcb, wcb);
});
await Promise.all(p);
console.log(`Wrote all ${numWritten} records`);
await util.promisify(db.close.bind(db))();
console.log('Finished.');
})();

View file

@ -2,28 +2,33 @@ The files in this folder are for Plugin developers.
# Get suggestions to improve your Plugin # Get suggestions to improve your Plugin
This code will check your plugin for known usual issues and some suggestions for improvements. No changes will be made to your project. This code will check your plugin for known usual issues and some suggestions for
improvements. No changes will be made to your project.
``` ```
node bin/plugins/checkPlugin.js $PLUGIN_NAME$ node src/bin/plugins/checkPlugin.js $PLUGIN_NAME$
``` ```
# Basic Example: # Basic Example:
``` ```
node bin/plugins/checkPlugin.js ep_webrtc node src/bin/plugins/checkPlugin.js ep_webrtc
``` ```
## Autofixing - will autofix any issues it can ## Autofixing - will autofix any issues it can
``` ```
node bin/plugins/checkPlugins.js ep_whatever autofix node src/bin/plugins/checkPlugin.js ep_whatever autofix
``` ```
## Autocommitting, push, npm minor patch and npm publish (highly dangerous) ## Autocommitting, push, npm minor patch and npm publish (highly dangerous)
``` ```
node bin/plugins/checkPlugins.js ep_whatever autofix autocommit node src/bin/plugins/checkPlugin.js ep_whatever autocommit
``` ```
# All the plugins # All the plugins
Replace johnmclear with your github username Replace johnmclear with your github username
``` ```
@ -33,19 +38,15 @@ GHUSER=johnmclear; curl "https://api.github.com/users/$GHUSER/repos?per_page=100
cd .. cd ..
# autofixes and autocommits /pushes & npm publishes # autofixes and autocommits /pushes & npm publishes
for dir in `ls node_modules`; for dir in node_modules/ep_*; do
do dir=${dir#node_modules/}
# echo $0 [ "$dir" != ep_etherpad-lite ] || continue
if [[ $dir == *"ep_"* ]]; then node src/bin/plugins/checkPlugin.js "$dir" autocommit
if [[ $dir != "ep_etherpad-lite" ]]; then
node bin/plugins/checkPlugin.js $dir autofix autocommit
fi
fi
# echo $dir
done done
``` ```
# Automating update of ether organization plugins # Automating update of ether organization plugins
``` ```
getCorePlugins.sh getCorePlugins.sh
updateCorePlugins.sh updateCorePlugins.sh

458
src/bin/plugins/checkPlugin.js Executable file
View file

@ -0,0 +1,458 @@
'use strict';
/*
* Usage -- see README.md
*
* Normal usage: node src/bin/plugins/checkPlugin.js ep_whatever
* Auto fix the things it can: node src/bin/plugins/checkPlugin.js ep_whatever autofix
* Auto commit, push and publish to npm (highly dangerous):
* node src/bin/plugins/checkPlugin.js ep_whatever autocommit
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
const fs = require('fs');
const childProcess = require('child_process');
// get plugin name & path from user input
const pluginName = process.argv[2];
if (!pluginName) throw new Error('no plugin name specified');
const pluginPath = `node_modules/${pluginName}`;
console.log(`Checking the plugin: ${pluginName}`);
const optArgs = process.argv.slice(3);
const autoCommit = optArgs.indexOf('autocommit') !== -1;
const autoFix = autoCommit || optArgs.indexOf('autofix') !== -1;
const execSync = (cmd, opts = {}) => (childProcess.execSync(cmd, {
cwd: `${pluginPath}/`,
...opts,
}) || '').toString().replace(/\n+$/, '');
const writePackageJson = (obj) => {
let s = JSON.stringify(obj, null, 2);
if (s.length && s.slice(s.length - 1) !== '\n') s += '\n';
return fs.writeFileSync(`${pluginPath}/package.json`, s);
};
const updateDeps = (parsedPackageJson, key, wantDeps) => {
const {[key]: deps = {}} = parsedPackageJson;
let changed = false;
for (const [pkg, verInfo] of Object.entries(wantDeps)) {
const {ver, overwrite = true} = typeof verInfo === 'string' ? {ver: verInfo} : verInfo;
if (deps[pkg] === ver) continue;
if (deps[pkg] == null) {
console.warn(`Missing dependency in ${key}: '${pkg}': '${ver}'`);
} else {
if (!overwrite) continue;
console.warn(`Dependency mismatch in ${key}: '${pkg}': '${ver}' (current: ${deps[pkg]})`);
}
if (autoFix) {
deps[pkg] = ver;
changed = true;
}
}
if (changed) {
parsedPackageJson[key] = deps;
writePackageJson(parsedPackageJson);
}
};
const prepareRepo = () => {
let branch = execSync('git symbolic-ref HEAD');
if (branch !== 'refs/heads/master' && branch !== 'refs/heads/main') {
throw new Error('master/main must be checked out');
}
branch = branch.replace(/^refs\/heads\//, '');
execSync('git rev-parse --verify -q HEAD^0 || ' +
`{ echo "Error: no commits on ${branch}" >&2; exit 1; }`);
execSync('git rev-parse --verify @{u}'); // Make sure there's a remote tracking branch.
const modified = execSync('git diff-files --name-status');
if (modified !== '') throw new Error(`working directory has modifications:\n${modified}`);
const untracked = execSync('git ls-files -o --exclude-standard');
if (untracked !== '') throw new Error(`working directory has untracked files:\n${untracked}`);
const indexStatus = execSync('git diff-index --cached --name-status HEAD');
if (indexStatus !== '') throw new Error(`uncommitted staged changes to files:\n${indexStatus}`);
execSync('git pull --ff-only', {stdio: 'inherit'});
if (execSync('git rev-list @{u}...') !== '') throw new Error('repo contains unpushed commits');
if (autoCommit) {
execSync('git config --get user.name');
execSync('git config --get user.email');
}
};
if (autoCommit) {
console.warn('Auto commit is enabled, I hope you know what you are doing...');
}
fs.readdir(pluginPath, (err, rootFiles) => {
// handling error
if (err) {
return console.log(`Unable to scan directory: ${err}`);
}
// rewriting files to lower case
const files = [];
// some files we need to know the actual file name. Not compulsory but might help in the future.
let readMeFileName;
let repository;
for (let i = 0; i < rootFiles.length; i++) {
if (rootFiles[i].toLowerCase().indexOf('readme') !== -1) readMeFileName = rootFiles[i];
files.push(rootFiles[i].toLowerCase());
}
if (files.indexOf('.git') === -1) throw new Error('No .git folder, aborting');
prepareRepo();
try {
const path = `${pluginPath}/.github/workflows/npmpublish.yml`;
if (!fs.existsSync(path)) {
console.log('no .github/workflows/npmpublish.yml');
console.log('create one and set npm secret to auto publish to npm on commit');
if (autoFix) {
const npmpublish =
fs.readFileSync('src/bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, npmpublish);
console.log("If you haven't already, setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo");
} else {
console.log('Setup autopublish for this plugin https://github.com/ether/etherpad-lite/wiki/Plugins:-Automatically-publishing-to-npm-on-commit-to-Github-Repo');
}
} else {
// autopublish exists, we should check the version..
// checkVersion takes two file paths and checks for a version string in them.
const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V=');
const existingValue = parseInt(
currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length));
const reqVersionFile =
fs.readFileSync('src/bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V=');
const reqValue =
parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length));
if (!existingValue || (reqValue > existingValue)) {
const npmpublish =
fs.readFileSync('src/bin/plugins/lib/npmpublish.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, npmpublish);
}
}
} catch (err) {
console.error(err);
}
try {
const path = `${pluginPath}/.github/workflows/backend-tests.yml`;
if (!fs.existsSync(path)) {
console.log('no .github/workflows/backend-tests.yml');
console.log('create one and set npm secret to auto publish to npm on commit');
if (autoFix) {
const backendTests =
fs.readFileSync('src/bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, backendTests);
}
} else {
// autopublish exists, we should check the version..
// checkVersion takes two file paths and checks for a version string in them.
const currVersionFile = fs.readFileSync(path, {encoding: 'utf8', flag: 'r'});
const existingConfigLocation = currVersionFile.indexOf('##ETHERPAD_NPM_V=');
const existingValue = parseInt(
currVersionFile.substr(existingConfigLocation + 17, existingConfigLocation.length));
const reqVersionFile =
fs.readFileSync('src/bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
const reqConfigLocation = reqVersionFile.indexOf('##ETHERPAD_NPM_V=');
const reqValue =
parseInt(reqVersionFile.substr(reqConfigLocation + 17, reqConfigLocation.length));
if (!existingValue || (reqValue > existingValue)) {
const backendTests =
fs.readFileSync('src/bin/plugins/lib/backend-tests.yml', {encoding: 'utf8', flag: 'r'});
fs.mkdirSync(`${pluginPath}/.github/workflows`, {recursive: true});
fs.writeFileSync(path, backendTests);
}
}
} catch (err) {
console.error(err);
}
if (files.indexOf('package.json') === -1) {
console.warn('no package.json, please create');
}
if (files.indexOf('package.json') !== -1) {
const packageJSON =
fs.readFileSync(`${pluginPath}/package.json`, {encoding: 'utf8', flag: 'r'});
const parsedPackageJSON = JSON.parse(packageJSON);
if (autoFix) {
let updatedPackageJSON = false;
if (!parsedPackageJSON.funding) {
updatedPackageJSON = true;
parsedPackageJSON.funding = {
type: 'individual',
url: 'https://etherpad.org/',
};
}
if (updatedPackageJSON) {
writePackageJson(parsedPackageJSON);
}
}
if (packageJSON.toLowerCase().indexOf('repository') === -1) {
console.warn('No repository in package.json');
if (autoFix) {
console.warn('Repository not detected in package.json. Add repository section.');
}
} else {
// useful for creating README later.
repository = parsedPackageJSON.repository.url;
}
updateDeps(parsedPackageJSON, 'devDependencies', {
'eslint': '^7.18.0',
'eslint-config-etherpad': '^1.0.24',
'eslint-plugin-eslint-comments': '^3.2.0',
'eslint-plugin-mocha': '^8.0.0',
'eslint-plugin-node': '^11.1.0',
'eslint-plugin-prefer-arrow': '^1.2.3',
'eslint-plugin-promise': '^4.2.1',
'eslint-plugin-you-dont-need-lodash-underscore': '^6.10.0',
});
updateDeps(parsedPackageJSON, 'peerDependencies', {
// Some plugins require a newer version of Etherpad so don't overwrite if already set.
'ep_etherpad-lite': {ver: '>=1.8.6', overwrite: false},
});
if (packageJSON.toLowerCase().indexOf('eslintconfig') === -1) {
console.warn('No esLintConfig in package.json');
if (autoFix) {
const eslintConfig = {
root: true,
extends: 'etherpad/plugin',
};
parsedPackageJSON.eslintConfig = eslintConfig;
writePackageJson(parsedPackageJSON);
}
}
if (packageJSON.toLowerCase().indexOf('scripts') === -1) {
console.warn('No scripts in package.json');
if (autoFix) {
const scripts = {
'lint': 'eslint .',
'lint:fix': 'eslint --fix .',
};
parsedPackageJSON.scripts = scripts;
writePackageJson(parsedPackageJSON);
}
}
if ((packageJSON.toLowerCase().indexOf('engines') === -1) || !parsedPackageJSON.engines.node) {
console.warn('No engines or node engine in package.json');
if (autoFix) {
const engines = {
node: '^10.17.0 || >=11.14.0',
};
parsedPackageJSON.engines = engines;
writePackageJson(parsedPackageJSON);
}
}
}
if (files.indexOf('package-lock.json') === -1) {
console.warn('package-lock.json not found');
if (!autoFix) {
console.warn('Run npm install in the plugin folder and commit the package-lock.json file.');
}
}
if (files.indexOf('readme') === -1 && files.indexOf('readme.md') === -1) {
console.warn('README.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing README.md file');
console.log('please edit the README.md file further to include plugin specific details.');
let readme = fs.readFileSync('src/bin/plugins/lib/README.md', {encoding: 'utf8', flag: 'r'});
readme = readme.replace(/\[plugin_name\]/g, pluginName);
if (repository) {
const org = repository.split('/')[3];
const name = repository.split('/')[4];
readme = readme.replace(/\[org_name\]/g, org);
readme = readme.replace(/\[repo_url\]/g, name);
fs.writeFileSync(`${pluginPath}/README.md`, readme);
} else {
console.warn('Unable to find repository in package.json, aborting.');
}
}
}
if (files.indexOf('contributing') === -1 && files.indexOf('contributing.md') === -1) {
console.warn('CONTRIBUTING.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing CONTRIBUTING.md file, please edit the CONTRIBUTING.md ' +
'file further to include plugin specific details.');
let contributing =
fs.readFileSync('src/bin/plugins/lib/CONTRIBUTING.md', {encoding: 'utf8', flag: 'r'});
contributing = contributing.replace(/\[plugin_name\]/g, pluginName);
fs.writeFileSync(`${pluginPath}/CONTRIBUTING.md`, contributing);
}
}
if (readMeFileName) {
let readme =
fs.readFileSync(`${pluginPath}/${readMeFileName}`, {encoding: 'utf8', flag: 'r'});
if (readme.toLowerCase().indexOf('license') === -1) {
console.warn('No license section in README');
if (autoFix) {
console.warn('Please add License section to README manually.');
}
}
// eslint-disable-next-line max-len
const publishBadge = `![Publish Status](https://github.com/ether/${pluginName}/workflows/Node.js%20Package/badge.svg)`;
// eslint-disable-next-line max-len
const testBadge = `![Backend Tests Status](https://github.com/ether/${pluginName}/workflows/Backend%20tests/badge.svg)`;
if (readme.toLowerCase().indexOf('travis') !== -1) {
console.warn('Remove Travis badges');
}
if (readme.indexOf('workflows/Node.js%20Package/badge.svg') === -1) {
console.warn('No Github workflow badge detected');
if (autoFix) {
readme = `${publishBadge} ${testBadge}\n\n${readme}`;
// write readme to file system
fs.writeFileSync(`${pluginPath}/${readMeFileName}`, readme);
console.log('Wrote Github workflow badges to README');
}
}
}
if (files.indexOf('license') === -1 && files.indexOf('license.md') === -1) {
console.warn('LICENSE.md file not found, please create');
if (autoFix) {
console.log('Autofixing missing LICENSE.md file, including Apache 2 license.');
let license =
fs.readFileSync('src/bin/plugins/lib/LICENSE.md', {encoding: 'utf8', flag: 'r'});
license = license.replace('[yyyy]', new Date().getFullYear());
license = license.replace('[name of copyright owner]', execSync('git config user.name'));
fs.writeFileSync(`${pluginPath}/LICENSE.md`, license);
}
}
if (files.indexOf('.gitignore') === -1) {
console.warn('.gitignore file not found, please create. .gitignore files are useful to ' +
"ensure files aren't incorrectly commited to a repository.");
if (autoFix) {
console.log('Autofixing missing .gitignore file');
const gitignore =
fs.readFileSync('src/bin/plugins/lib/gitignore', {encoding: 'utf8', flag: 'r'});
fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore);
}
} else {
let gitignore =
fs.readFileSync(`${pluginPath}/.gitignore`, {encoding: 'utf8', flag: 'r'});
if (gitignore.indexOf('node_modules/') === -1) {
console.warn('node_modules/ missing from .gitignore');
if (autoFix) {
gitignore += 'node_modules/';
fs.writeFileSync(`${pluginPath}/.gitignore`, gitignore);
}
}
}
// if we include templates but don't have translations...
if (files.indexOf('templates') !== -1 && files.indexOf('locales') === -1) {
console.warn('Translations not found, please create. ' +
'Translation files help with Etherpad accessibility.');
}
if (files.indexOf('.ep_initialized') !== -1) {
console.warn(
'.ep_initialized found, please remove. .ep_initialized should never be commited to git ' +
'and should only exist once the plugin has been executed one time.');
if (autoFix) {
console.log('Autofixing incorrectly existing .ep_initialized file');
fs.unlinkSync(`${pluginPath}/.ep_initialized`);
}
}
if (files.indexOf('npm-debug.log') !== -1) {
console.warn('npm-debug.log found, please remove. npm-debug.log should never be commited to ' +
'your repository.');
if (autoFix) {
console.log('Autofixing incorrectly existing npm-debug.log file');
fs.unlinkSync(`${pluginPath}/npm-debug.log`);
}
}
if (files.indexOf('static') !== -1) {
fs.readdir(`${pluginPath}/static`, (errRead, staticFiles) => {
if (staticFiles.indexOf('tests') === -1) {
console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
}
});
} else {
console.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
}
// Install dependencies so we can run ESLint. This should also create or update package-lock.json
// if autoFix is enabled.
const npmInstall = `npm install${autoFix ? '' : ' --no-package-lock'}`;
execSync(npmInstall, {stdio: 'inherit'});
// The ep_etherpad-lite peer dep must be installed last otherwise `npm install` will nuke it. An
// absolute path to etherpad-lite/src is used here so that pluginPath can be a symlink.
execSync(
`${npmInstall} --no-save ep_etherpad-lite@file:${__dirname}/../../`, {stdio: 'inherit'});
// linting begins
try {
console.log('Linting...');
const lintCmd = autoFix ? 'npx eslint --fix .' : 'npx eslint';
execSync(lintCmd, {stdio: 'inherit'});
} catch (e) {
// it is gonna throw an error anyway
console.log('Manual linting probably required, check with: npm run lint');
}
// linting ends.
if (autoFix) {
const unchanged = JSON.parse(execSync(
'untracked=$(git ls-files -o --exclude-standard) || exit 1; ' +
'git diff-files --quiet && [ -z "$untracked" ] && echo true || echo false'));
if (!unchanged) {
// Display a diff of changes. Git doesn't diff untracked files, so they must be added to the
// index. Use a temporary index file to avoid modifying Git's default index file.
execSync('git read-tree HEAD; git add -A && git diff-index -p --cached HEAD && echo ""', {
env: {...process.env, GIT_INDEX_FILE: '.git/checkPlugin.index'},
stdio: 'inherit',
});
fs.unlinkSync(`${pluginPath}/.git/checkPlugin.index`);
const cmd = [
'git add -A',
'git commit -m "autofixes from Etherpad checkPlugin.js"',
'git push',
].join(' && ');
if (autoCommit) {
console.log('Attempting autocommit and auto publish to npm');
execSync(cmd, {stdio: 'inherit'});
} else {
console.log('Fixes applied. Check the above git diff then run the following command:');
console.log(`(cd node_modules/${pluginName} && ${cmd})`);
}
} else {
console.log('No changes.');
}
}
console.log('Finished');
});

View file

@ -113,7 +113,9 @@ Documentation should be kept up-to-date. This means, whenever you add a new API
You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet. You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet.
## Testing ## Testing
Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `<yourdomainhere>/tests/frontend`.
Front-end tests are found in the `src/tests/frontend/` folder in the repository.
Run them by pointing your browser to `<yourdomainhere>/tests/frontend`.
Back-end tests can be run from the `src` directory, via `npm test`. Back-end tests can be run from the `src` directory, via `npm test`.

View file

@ -30,7 +30,7 @@ jobs:
repository: ether/etherpad-lite repository: ether/etherpad-lite
- name: Install all dependencies and symlink for ep_etherpad-lite - name: Install all dependencies and symlink for ep_etherpad-lite
run: bin/installDeps.sh run: src/bin/installDeps.sh
# clone this repository into node_modules/ep_plugin-name # clone this repository into node_modules/ep_plugin-name
- name: Checkout plugin repository - name: Checkout plugin repository
@ -45,7 +45,7 @@ jobs:
# configures some settings and runs npm run test # configures some settings and runs npm run test
- name: Run the backend tests - name: Run the backend tests
run: tests/frontend/travis/runnerBackend.sh run: src/tests/frontend/travis/runnerBackend.sh
##ETHERPAD_NPM_V=1 ##ETHERPAD_NPM_V=1
## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh ## NPM configuration automatically created using src/bin/plugins/updateAllPluginsScript.sh

View file

@ -64,10 +64,20 @@ jobs:
- run: git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - run: git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
- run: npm ci - run: npm ci
- run: npm version patch - run: npm version patch
- run: git push --follow-tags
# `npm publish` must come after `git push` otherwise there is a race
# condition: If two PRs are merged back-to-back then master/main will be
# updated with the commits from the second PR before the first PR's
# workflow has a chance to push the commit generated by `npm version
# patch`. This causes the first PR's `git push` step to fail after the
# package has already been published, which in turn will cause all future
# workflow runs to fail because they will all attempt to use the same
# already-used version number. By running `npm publish` after `git push`,
# back-to-back merges will cause the first merge's workflow to fail but
# the second's will succeed.
- run: npm publish - run: npm publish
env: env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- run: git push --follow-tags
##ETHERPAD_NPM_V=1 ##ETHERPAD_NPM_V=2
## NPM configuration automatically created using bin/plugins/updateAllPluginsScript.sh ## NPM configuration automatically created using src/bin/plugins/updateAllPluginsScript.sh

View file

@ -4,7 +4,7 @@ do
echo $dir echo $dir
if [[ $dir == *"ep_"* ]]; then if [[ $dir == *"ep_"* ]]; then
if [[ $dir != "ep_etherpad-lite" ]]; then if [[ $dir != "ep_etherpad-lite" ]]; then
# node bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate # node src/bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate
cd node_modules/$dir cd node_modules/$dir
git commit -m "Automatic update: bump update to re-run latest Etherpad tests" --allow-empty git commit -m "Automatic update: bump update to re-run latest Etherpad tests" --allow-empty
git push origin master git push origin master

View file

@ -10,7 +10,7 @@ do
# echo $0 # echo $0
if [[ $dir == *"ep_"* ]]; then if [[ $dir == *"ep_"* ]]; then
if [[ $dir != "ep_etherpad-lite" ]]; then if [[ $dir != "ep_etherpad-lite" ]]; then
node bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate node src/bin/plugins/checkPlugin.js $dir autofix autocommit autoupdate
fi fi
fi fi
# echo $dir # echo $dir

View file

@ -5,5 +5,5 @@ set -e
for dir in node_modules/ep_*; do for dir in node_modules/ep_*; do
dir=${dir#node_modules/} dir=${dir#node_modules/}
[ "$dir" != ep_etherpad-lite ] || continue [ "$dir" != ep_etherpad-lite ] || continue
node bin/plugins/checkPlugin.js "$dir" autofix autocommit autoupdate node src/bin/plugins/checkPlugin.js "$dir" autofix autocommit autoupdate
done done

84
src/bin/rebuildPad.js Normal file
View file

@ -0,0 +1,84 @@
'use strict';
/*
This is a repair tool. It rebuilds an old pad at a new pad location up to a
known "good" revision.
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
if (process.argv.length !== 4 && process.argv.length !== 5) {
throw new Error('Use: node src/bin/repairPad.js $PADID $REV [$NEWPADID]');
}
const padId = process.argv[2];
const newRevHead = process.argv[3];
const newPadId = process.argv[4] || `${padId}-rebuilt`;
(async () => {
const db = require('../node/db/DB');
await db.init();
const PadManager = require('../node/db/PadManager');
const Pad = require('../node/db/Pad').Pad;
// Validate the newPadId if specified and that a pad with that ID does
// not already exist to avoid overwriting it.
if (!PadManager.isValidPadId(newPadId)) {
throw new Error('Cannot create a pad with that id as it is invalid');
}
const exists = await PadManager.doesPadExist(newPadId);
if (exists) throw new Error('Cannot create a pad with that id as it already exists');
const oldPad = await PadManager.getPad(padId);
const newPad = new Pad(newPadId);
// Clone all Chat revisions
const chatHead = oldPad.chatHead;
await Promise.all([...Array(chatHead + 1).keys()].map(async (i) => {
const chat = await db.get(`pad:${padId}:chat:${i}`);
await db.set(`pad:${newPadId}:chat:${i}`, chat);
console.log(`Created: Chat Revision: pad:${newPadId}:chat:${i}`);
}));
// Rebuild Pad from revisions up to and including the new revision head
const AuthorManager = require('../node/db/AuthorManager');
const Changeset = require('../static/js/Changeset');
// Author attributes are derived from changesets, but there can also be
// non-author attributes with specific mappings that changesets depend on
// and, AFAICT, cannot be recreated any other way
newPad.pool.numToAttrib = oldPad.pool.numToAttrib;
for (let curRevNum = 0; curRevNum <= newRevHead; curRevNum++) {
const rev = await db.get(`pad:${padId}:revs:${curRevNum}`);
if (!rev || !rev.meta) throw new Error('The specified revision number could not be found.');
const newRevNum = ++newPad.head;
const newRevId = `pad:${newPad.id}:revs:${newRevNum}`;
await Promise.all([
db.set(newRevId, rev),
AuthorManager.addPad(rev.meta.author, newPad.id),
]);
newPad.atext = Changeset.applyToAText(rev.changeset, newPad.atext, newPad.pool);
console.log(`Created: Revision: pad:${newPad.id}:revs:${newRevNum}`);
}
// Add saved revisions up to the new revision head
console.log(newPad.head);
const newSavedRevisions = [];
for (const savedRev of oldPad.savedRevisions) {
if (savedRev.revNum <= newRevHead) {
newSavedRevisions.push(savedRev);
console.log(`Added: Saved Revision: ${savedRev.revNum}`);
}
}
newPad.savedRevisions = newSavedRevisions;
// Save the source pad
await db.set(`pad:${newPadId}`, newPad);
console.log(`Created: Source Pad: pad:${newPadId}`);
await newPad.saveToDatabase();
await db.shutdown();
console.info('finished');
})();

75
src/bin/release.js Normal file
View file

@ -0,0 +1,75 @@
'use strict';
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
const fs = require('fs');
const childProcess = require('child_process');
const semver = require('semver');
/*
Usage
node src/bin/release.js patch
*/
const usage =
'node src/bin/release.js [patch/minor/major] -- example: "node src/bin/release.js patch"';
const release = process.argv[2];
if (!release) {
console.log(usage);
throw new Error('No release type included');
}
const changelog = fs.readFileSync('CHANGELOG.md', {encoding: 'utf8', flag: 'r'});
let packageJson = fs.readFileSync('./src/package.json', {encoding: 'utf8', flag: 'r'});
packageJson = JSON.parse(packageJson);
const currentVersion = packageJson.version;
const newVersion = semver.inc(currentVersion, release);
if (!newVersion) {
console.log(usage);
throw new Error('Unable to generate new version from input');
}
const changelogIncludesVersion = changelog.indexOf(newVersion) !== -1;
if (!changelogIncludesVersion) {
throw new Error(`No changelog record for ${newVersion}, please create changelog record`);
}
console.log('Okay looks good, lets create the package.json and package-lock.json');
packageJson.version = newVersion;
fs.writeFileSync('src/package.json', JSON.stringify(packageJson, null, 2));
// run npm version `release` where release is patch, minor or major
childProcess.execSync('npm install --package-lock-only', {cwd: 'src/'});
// run npm install --package-lock-only <-- required???
childProcess.execSync(`git checkout -b release/${newVersion}`);
childProcess.execSync('git add src/package.json');
childProcess.execSync('git add src/package-lock.json');
childProcess.execSync('git commit -m "bump version"');
childProcess.execSync(`git push origin release/${newVersion}`);
childProcess.execSync('make docs');
childProcess.execSync('git clone git@github.com:ether/ether.github.com.git');
childProcess.execSync(`cp -R out/doc/ ether.github.com/doc/v${newVersion}`);
console.log('Once merged into master please run the following commands');
console.log(`git tag -a ${newVersion} -m ${newVersion} && git push origin master`);
console.log(`cd ether.github.com && git add . && git commit -m '${newVersion} docs'`);
console.log('Build the windows zip');
console.log('Visit https://github.com/ether/etherpad-lite/releases/new and create a new release ' +
`with 'master' as the target and the version is ${newVersion}. Include the windows ` +
'zip as an asset');
console.log(`Once the new docs are uploaded then modify the download
link on etherpad.org and then pull master onto develop`);
console.log('Finally go public with an announcement via our comms channels :)');

56
src/bin/repairPad.js Normal file
View file

@ -0,0 +1,56 @@
'use strict';
/*
* This is a repair tool. It extracts all datas of a pad, removes and inserts them again.
*/
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { throw err; });
console.warn('WARNING: This script must not be used while etherpad is running!');
if (process.argv.length !== 3) throw new Error('Use: node src/bin/repairPad.js $PADID');
// get the padID
const padId = process.argv[2];
let valueCount = 0;
(async () => {
// initialize database
require('../node/utils/Settings');
const db = require('../node/db/DB');
await db.init();
// get the pad
const padManager = require('../node/db/PadManager');
const pad = await padManager.getPad(padId);
// accumulate the required keys
const neededDBValues = [`pad:${padId}`];
// add all authors
neededDBValues.push(...pad.getAllAuthors().map((author) => `globalAuthor:${author}`));
// add all revisions
for (let rev = 0; rev <= pad.head; ++rev) {
neededDBValues.push(`pad:${padId}:revs:${rev}`);
}
// add all chat values
for (let chat = 0; chat <= pad.chatHead; ++chat) {
neededDBValues.push(`pad:${padId}:chat:${chat}`);
}
// now fetch and reinsert every key
for (const key of neededDBValues) {
const value = await db.get(key);
// if it isn't a globalAuthor value which we want to ignore..
// console.log(`Key: ${key}, value: ${JSON.stringify(value)}`);
await db.remove(key);
await db.set(key, value);
valueCount++;
}
console.info(`Finished: Replaced ${valueCount} values in the database`);
})();

View file

@ -1,10 +1,11 @@
#!/bin/sh #!/bin/sh
# Move to the folder where ep-lite is installed # Move to the Etherpad base directory.
cd "$(dirname "$0")"/.. MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
cd "${MY_DIR}/../.." || exit 1
# Source constants and usefull functions # Source constants and useful functions
. bin/functions.sh . src/bin/functions.sh
ignoreRoot=0 ignoreRoot=0
for ARG in "$@"; do for ARG in "$@"; do
@ -26,7 +27,7 @@ EOF
fi fi
# Prepare the environment # Prepare the environment
bin/installDeps.sh "$@" || exit 1 src/bin/installDeps.sh "$@" || exit 1
# Move to the node folder and start # Move to the node folder and start
log "Starting Etherpad..." log "Starting Etherpad..."

View file

@ -23,8 +23,9 @@ fatal() { error "$@"; exit 1; }
LAST_EMAIL_SEND=0 LAST_EMAIL_SEND=0
# Move to the folder where ep-lite is installed # Move to the Etherpad base directory.
cd "$(dirname "$0")"/.. MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1
try cd "${MY_DIR}/../.."
# Check if a logfile parameter is set # Check if a logfile parameter is set
LOG="$1" LOG="$1"
@ -39,7 +40,7 @@ while true; do
[ -w "${LOG}" ] || fatal "Logfile '${LOG}' is not writeable" [ -w "${LOG}" ] || fatal "Logfile '${LOG}' is not writeable"
# Start the application # Start the application
bin/run.sh "$@" >>${LOG} 2>>${LOG} src/bin/run.sh "$@" >>${LOG} 2>>${LOG}
TIME_FMT=$(date +%Y-%m-%dT%H:%M:%S%z) TIME_FMT=$(date +%Y-%m-%dT%H:%M:%S%z)

View file

@ -7,6 +7,13 @@
"Wizardist" "Wizardist"
] ]
}, },
"admin.page-title": "Адміністрацыйная панэль — Etherpad",
"admin_plugins": "Кіраўнік плагінаў",
"admin_plugins.available": "Даступныя плагіны",
"admin_plugins.available_not-found": "Плагіны ня знойдзеныя.",
"admin_plugins.available_fetching": "Атрымліваем…",
"admin_plugins.available_install.value": "Усталяваць",
"admin_settings.page-title": "Налады — Etherpad",
"index.newPad": "Стварыць", "index.newPad": "Стварыць",
"index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:", "index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:",
"index.openPad": "адкрыць існы Нататнік з назваю:", "index.openPad": "адкрыць існы Нататнік з назваю:",
@ -17,7 +24,7 @@
"pad.toolbar.ol.title": "Упарадкаваны сьпіс (Ctrl+Shift+N)", "pad.toolbar.ol.title": "Упарадкаваны сьпіс (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Неўпарадкаваны сьпіс (Ctrl+Shift+L)", "pad.toolbar.ul.title": "Неўпарадкаваны сьпіс (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Водступ (TAB)", "pad.toolbar.indent.title": "Водступ (TAB)",
"pad.toolbar.unindent.title": "Выступ (Shift+TAB)", "pad.toolbar.unindent.title": "Водступ (Shift+TAB)",
"pad.toolbar.undo.title": "Скасаваць(Ctrl-Z)", "pad.toolbar.undo.title": "Скасаваць(Ctrl-Z)",
"pad.toolbar.redo.title": "Вярнуць (Ctrl-Y)", "pad.toolbar.redo.title": "Вярнуць (Ctrl-Y)",
"pad.toolbar.clearAuthorship.title": "Прыбраць колер дакумэнту (Ctrl+Shift+C)", "pad.toolbar.clearAuthorship.title": "Прыбраць колер дакумэнту (Ctrl+Shift+C)",
@ -42,6 +49,7 @@
"pad.settings.fontType": "Тып шрыфту:", "pad.settings.fontType": "Тып шрыфту:",
"pad.settings.fontType.normal": "Звычайны", "pad.settings.fontType.normal": "Звычайны",
"pad.settings.language": "Мова:", "pad.settings.language": "Мова:",
"pad.settings.about": "Пра",
"pad.importExport.import_export": "Імпарт/Экспарт", "pad.importExport.import_export": "Імпарт/Экспарт",
"pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты", "pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты",
"pad.importExport.importSuccessful": "Пасьпяхова!", "pad.importExport.importSuccessful": "Пасьпяхова!",
@ -54,7 +62,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)", "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.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.connected": "Падлучыліся.",
"pad.modals.reconnecting": "Перападлучэньне да вашага дакумэнта...", "pad.modals.reconnecting": "Перападлучэньне да вашага дакумэнта",
"pad.modals.forcereconnect": "Прымусовае перападлучэньне", "pad.modals.forcereconnect": "Прымусовае перападлучэньне",
"pad.modals.reconnecttimer": "Спрабуем перападключыцца праз", "pad.modals.reconnecttimer": "Спрабуем перападключыцца праз",
"pad.modals.cancel": "Скасаваць", "pad.modals.cancel": "Скасаваць",

View file

@ -10,11 +10,46 @@
"Leanes", "Leanes",
"Mormegil", "Mormegil",
"Peldrjan", "Peldrjan",
"Quinn" "Quinn",
"Spotter"
] ]
}, },
"admin.page-title": "Ovládací panel Správce - Etherpad",
"admin_plugins": "Správce zásuvných moodulů",
"admin_plugins.available": "Dostupné zásuvné moduly",
"admin_plugins.available_not-found": "Nejsou žádné zásuvné moduly",
"admin_plugins.available_fetching": "Načítání...",
"admin_plugins.available_install.value": "Instalovat",
"admin_plugins.available_search.placeholder": "Vyhledat zásuvné moduly k instalaci",
"admin_plugins.description": "Popis",
"admin_plugins.installed": "Nainstalované zásuvné moduly",
"admin_plugins.installed_fetching": "Načítání instalovaných zásuvných modulů...",
"admin_plugins.installed_nothing": "Dosud jste nenainstalovali žádné zásuvné moduly.",
"admin_plugins.installed_uninstall.value": "Odinstalovat",
"admin_plugins.last-update": "Poslední aktualizace",
"admin_plugins.name": "Název",
"admin_plugins.page-title": "Správce zásuvných modulů - Etherpad",
"admin_plugins.version": "Verze",
"admin_plugins_info": "Informace o řešení problému",
"admin_plugins_info.hooks": "Instalované hooks",
"admin_plugins_info.hooks_client": "hooks na straně klienta",
"admin_plugins_info.hooks_server": "hooks na straně serveru",
"admin_plugins_info.parts": "Nainstalované součásti",
"admin_plugins_info.plugins": "Nainstalované zásuvné moduly",
"admin_plugins_info.page-title": "Informace o zásuvných modulech - Etherpad",
"admin_plugins_info.version": "Verze Etherpad",
"admin_plugins_info.version_latest": "Poslední dostupná verze",
"admin_plugins_info.version_number": "Číslo verze",
"admin_settings": "Nastavení",
"admin_settings.current": "Aktuální konfugurace",
"admin_settings.current_example-devel": "Příklad ukázkové vývojové šablony",
"admin_settings.current_example-prod": "Příklad šablony nastavení výroby",
"admin_settings.current_restart.value": "Restartovat Etherpad",
"admin_settings.current_save.value": "Uložit nastavení",
"admin_settings.page-title": "Nastavení - Etherpad",
"index.newPad": "Založ nový Pad", "index.newPad": "Založ nový Pad",
"index.createOpenPad": "nebo vytvoř/otevři Pad s názvem:", "index.createOpenPad": "nebo vytvoř/otevři Pad s názvem:",
"index.openPad": "otevřít existující Pad se jménem:",
"pad.toolbar.bold.title": "Tučný text (Ctrl-B)", "pad.toolbar.bold.title": "Tučný text (Ctrl-B)",
"pad.toolbar.italic.title": "Kurzíva (Ctrl-I)", "pad.toolbar.italic.title": "Kurzíva (Ctrl-I)",
"pad.toolbar.underline.title": "Podtržené písmo (Ctrl-U)", "pad.toolbar.underline.title": "Podtržené písmo (Ctrl-U)",
@ -35,7 +70,7 @@
"pad.colorpicker.save": "Uložit", "pad.colorpicker.save": "Uložit",
"pad.colorpicker.cancel": "Zrušit", "pad.colorpicker.cancel": "Zrušit",
"pad.loading": "Načítání...", "pad.loading": "Načítání...",
"pad.noCookie": "Nelze nalézt cookie. Povolte prosím cookie ve Vašem prohlížeči.", "pad.noCookie": "Soubor cookie nebyl nalezen. Povolte prosím cookies ve svém prohlížeči! Vaše relace a nastavení se mezi návštěvami neuloží. Může to být způsobeno tím, že je Etherpad v některých prohlížečích zahrnut do iFrame. Zkontrolujte, zda je Etherpad ve stejné subdoméně / doméně jako nadřazený iFrame",
"pad.permissionDenied": "Nemáte oprávnění pro přístup k tomuto Padu", "pad.permissionDenied": "Nemáte oprávnění pro přístup k tomuto Padu",
"pad.settings.padSettings": "Nastavení Padu", "pad.settings.padSettings": "Nastavení Padu",
"pad.settings.myView": "Vlastní pohled", "pad.settings.myView": "Vlastní pohled",
@ -47,6 +82,8 @@
"pad.settings.fontType": "Typ písma:", "pad.settings.fontType": "Typ písma:",
"pad.settings.fontType.normal": "Normální", "pad.settings.fontType.normal": "Normální",
"pad.settings.language": "Jazyk:", "pad.settings.language": "Jazyk:",
"pad.settings.about": "O projektu",
"pad.settings.poweredBy": "Běží na",
"pad.importExport.import_export": "Import/Export", "pad.importExport.import_export": "Import/Export",
"pad.importExport.import": "Nahrát libovolný textový soubor nebo dokument", "pad.importExport.import": "Nahrát libovolný textový soubor nebo dokument",
"pad.importExport.importSuccessful": "Úspěšně!", "pad.importExport.importSuccessful": "Úspěšně!",
@ -57,9 +94,9 @@
"pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF", "pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Importovat můžeš pouze prostý text nebo HTML formátování. Pro pokročilejší funkce importu, prosím, nainstaluj „<a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">AbiWord</a>.", "pad.importExport.abiword.innerHTML": "Importovat lze pouze z formátů prostého textu nebo HTML. Pokročilejší funkce pro import naleznete v <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instalaci AbiWord nebo LibreOffice</a>.",
"pad.modals.connected": "Připojeno.", "pad.modals.connected": "Připojeno.",
"pad.modals.reconnecting": "Znovupřipojování k Padu…", "pad.modals.reconnecting": "Opětovné připojení k Padu...",
"pad.modals.forcereconnect": "Vynutit znovupřipojení", "pad.modals.forcereconnect": "Vynutit znovupřipojení",
"pad.modals.reconnecttimer": "Zkouším se znovu připojit", "pad.modals.reconnecttimer": "Zkouším se znovu připojit",
"pad.modals.cancel": "Zrušit", "pad.modals.cancel": "Zrušit",
@ -81,6 +118,10 @@
"pad.modals.corruptPad.cause": "To může být kvůli špatné konfiguraci serveru, nebo kvůli jinému neočekávanému chování. Kontaktujte prosím správce služby.", "pad.modals.corruptPad.cause": "To může být kvůli špatné konfiguraci serveru, nebo kvůli jinému neočekávanému chování. Kontaktujte prosím správce služby.",
"pad.modals.deleted": "Odstraněno.", "pad.modals.deleted": "Odstraněno.",
"pad.modals.deleted.explanation": "Tento Pad byl odebrán.", "pad.modals.deleted.explanation": "Tento Pad byl odebrán.",
"pad.modals.rateLimited": "Rychlost je omezená.",
"pad.modals.rateLimited.explanation": "Na tento Pad jste poslali příliš mnoho zpráv, takže vás odpojil.",
"pad.modals.rejected.explanation": "Server odmítl zprávu odeslanou vaším prohlížečem.",
"pad.modals.rejected.cause": "Server mohl být aktualizován, když jste sledovali podložku, nebo možná došlo k chybě v Etherpadu. Zkuste stránku znovu načíst.",
"pad.modals.disconnected": "Byl jste odpojen.", "pad.modals.disconnected": "Byl jste odpojen.",
"pad.modals.disconnected.explanation": "Připojení k serveru bylo přerušeno", "pad.modals.disconnected.explanation": "Připojení k serveru bylo přerušeno",
"pad.modals.disconnected.cause": "Server může být nedostupný. Upozorněte administrátora služby, pokud se to bude opakovat.", "pad.modals.disconnected.cause": "Server může být nedostupný. Upozorněte administrátora služby, pokud se to bude opakovat.",
@ -93,6 +134,7 @@
"pad.chat.loadmessages": "Načíst více zpráv", "pad.chat.loadmessages": "Načíst více zpráv",
"pad.chat.stick.title": "Přichytit chat k obrazovce", "pad.chat.stick.title": "Přichytit chat k obrazovce",
"pad.chat.writeMessage.placeholder": "Zde napište zprávu", "pad.chat.writeMessage.placeholder": "Zde napište zprávu",
"timeslider.followContents": "Sledovat aktualizace obsahu Padu",
"timeslider.pageTitle": "Časová osa {{appTitle}}", "timeslider.pageTitle": "Časová osa {{appTitle}}",
"timeslider.toolbar.returnbutton": "Návrat do Padu", "timeslider.toolbar.returnbutton": "Návrat do Padu",
"timeslider.toolbar.authors": "Autoři:", "timeslider.toolbar.authors": "Autoři:",
@ -131,5 +173,6 @@
"pad.impexp.uploadFailed": "Nahrávání selhalo, zkuste to znovu", "pad.impexp.uploadFailed": "Nahrávání selhalo, zkuste to znovu",
"pad.impexp.importfailed": "Import selhal", "pad.impexp.importfailed": "Import selhal",
"pad.impexp.copypaste": "Vložte prosím kopii", "pad.impexp.copypaste": "Vložte prosím kopii",
"pad.impexp.exportdisabled": "Export do formátu {{type}} je zakázán. Kontaktujte svého administrátora pro zjištění detailů." "pad.impexp.exportdisabled": "Export do formátu {{type}} je zakázán. Kontaktujte svého administrátora pro zjištění detailů.",
"pad.impexp.maxFileSize": "Soubor je příliš velký. Požádejte svého správce webu o zvýšení povolené velikosti souboru pro import"
} }

View file

@ -11,24 +11,42 @@
"Sebastian Wallroth", "Sebastian Wallroth",
"Thargon", "Thargon",
"Tim.krieger", "Tim.krieger",
"Wikinaut" "Wikinaut",
"Zunkelty"
] ]
}, },
"admin.page-title": "Admin Dashboard - Etherpad",
"admin_plugins": "Plugins verwalten", "admin_plugins": "Plugins verwalten",
"admin_plugins.available": "Verfügbare Plugins", "admin_plugins.available": "Verfügbare Plugins",
"admin_plugins.available_not-found": "Keine Plugins gefunden.", "admin_plugins.available_not-found": "Keine Plugins gefunden.",
"admin_plugins.available_fetching": "Wird abgerufen...",
"admin_plugins.available_install.value": "Installieren", "admin_plugins.available_install.value": "Installieren",
"admin_plugins.available_search.placeholder": "Suche nach Plugins zum Installieren",
"admin_plugins.description": "Beschreibung", "admin_plugins.description": "Beschreibung",
"admin_plugins.installed": "Installierte Plugins",
"admin_plugins.installed_fetching": "Rufe installierte Plugins ab...",
"admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.", "admin_plugins.installed_nothing": "Du hast bisher noch keine Plugins installiert.",
"admin_plugins.installed_uninstall.value": "Deinstallieren",
"admin_plugins.last-update": "Letze Aktualisierung", "admin_plugins.last-update": "Letze Aktualisierung",
"admin_plugins.name": "Name", "admin_plugins.name": "Name",
"admin_plugins.page-title": "Plugin Manager - Etherpad",
"admin_plugins.version": "Version", "admin_plugins.version": "Version",
"admin_plugins_info": "Hilfestellung",
"admin_plugins_info.hooks": "Installierte Hooks", "admin_plugins_info.hooks": "Installierte Hooks",
"admin_plugins_info.hooks_client": "Client-seitige Hooks",
"admin_plugins_info.hooks_server": "Server-seitige Hooks",
"admin_plugins_info.parts": "Installierte Teile",
"admin_plugins_info.plugins": "Installierte Plugins", "admin_plugins_info.plugins": "Installierte Plugins",
"admin_plugins_info.page-title": "Plugin Informationen - Etherpad",
"admin_plugins_info.version": "Etherpad Version",
"admin_plugins_info.version_latest": "Neueste Version",
"admin_plugins_info.version_number": "Versionsnummer", "admin_plugins_info.version_number": "Versionsnummer",
"admin_settings": "Einstellungen", "admin_settings": "Einstellungen",
"admin_settings.current": "Derzeitige Konfiguration", "admin_settings.current": "Derzeitige Konfiguration",
"admin_settings.current_example-devel": "Beispielhafte Entwicklungseinstellungs-Templates",
"admin_settings.current_restart.value": "Etherpad neustarten",
"admin_settings.current_save.value": "Einstellungen speichern", "admin_settings.current_save.value": "Einstellungen speichern",
"admin_settings.page-title": "Einstellungen - Etherpad",
"index.newPad": "Neues Pad", "index.newPad": "Neues Pad",
"index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:", "index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:",
"index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:", "index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:",
@ -78,7 +96,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Du kannst nur aus reinen Text- oder HTML-Formaten importieren. Für umfangreichere Importfunktionen <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">muss AbiWord oder LibreOffice auf dem Server installiert werden</a>.", "pad.importExport.abiword.innerHTML": "Du kannst nur aus reinen Text- oder HTML-Formaten importieren. Für umfangreichere Importfunktionen <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">muss AbiWord oder LibreOffice auf dem Server installiert werden</a>.",
"pad.modals.connected": "Verbunden.", "pad.modals.connected": "Verbunden.",
"pad.modals.reconnecting": "Wiederherstellen der Verbindung …", "pad.modals.reconnecting": "Dein Pad wird neu verbunden...",
"pad.modals.forcereconnect": "Erneutes Verbinden erzwingen", "pad.modals.forcereconnect": "Erneutes Verbinden erzwingen",
"pad.modals.reconnecttimer": "Versuche Neuverbindung in", "pad.modals.reconnecttimer": "Versuche Neuverbindung in",
"pad.modals.cancel": "Abbrechen", "pad.modals.cancel": "Abbrechen",
@ -102,6 +120,7 @@
"pad.modals.deleted.explanation": "Dieses Pad wurde entfernt.", "pad.modals.deleted.explanation": "Dieses Pad wurde entfernt.",
"pad.modals.rateLimited": "Begrenzte Rate.", "pad.modals.rateLimited": "Begrenzte Rate.",
"pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.", "pad.modals.rateLimited.explanation": "Sie haben zu viele Nachrichten an dieses Pad gesendet, so dass die Verbindung unterbrochen wurde.",
"pad.modals.rejected.explanation": "Der Server hat eine Nachricht abgelehnt, die von deinem Browser gesendet wurde.",
"pad.modals.disconnected": "Ihre Verbindung wurde getrennt.", "pad.modals.disconnected": "Ihre Verbindung wurde getrennt.",
"pad.modals.disconnected.explanation": "Die Verbindung zum Server wurde unterbrochen.", "pad.modals.disconnected.explanation": "Die Verbindung zum Server wurde unterbrochen.",
"pad.modals.disconnected.cause": "Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtige den Dienstadministrator, falls dies weiterhin passiert.", "pad.modals.disconnected.cause": "Möglicherweise ist der Server nicht erreichbar. Bitte benachrichtige den Dienstadministrator, falls dies weiterhin passiert.",

View file

@ -11,7 +11,7 @@
] ]
}, },
"admin.page-title": "Panoyê İdarekari - Etherpad", "admin.page-title": "Panoyê İdarekari - Etherpad",
"admin_plugins": "İdarekarê Dekerdeki", "admin_plugins": "Gıredayışê raverberi",
"admin_plugins.available": "Mewcud Dekerdeki", "admin_plugins.available": "Mewcud Dekerdeki",
"admin_plugins.available_not-found": "Dekerdek nevineya", "admin_plugins.available_not-found": "Dekerdek nevineya",
"admin_plugins.available_fetching": "Aniyeno...", "admin_plugins.available_fetching": "Aniyeno...",
@ -132,15 +132,15 @@
"pad.chat.writeMessage.placeholder": "Mesacê xo tiya bınusne", "pad.chat.writeMessage.placeholder": "Mesacê xo tiya bınusne",
"timeslider.followContents": "Rocaney zerrekê padi taqib bıkerê", "timeslider.followContents": "Rocaney zerrekê padi taqib bıkerê",
"timeslider.pageTitle": ızagê zemani {{appTitle}}", "timeslider.pageTitle": ızagê zemani {{appTitle}}",
"timeslider.toolbar.returnbutton": "Peyser şo ped", "timeslider.toolbar.returnbutton": "Peyser şo bloknot",
"timeslider.toolbar.authors": "Nuştoği:", "timeslider.toolbar.authors": "Nuştoği:",
"timeslider.toolbar.authorsList": "Nuşti çıniyê", "timeslider.toolbar.authorsList": "Nuştekari çıniyê",
"timeslider.toolbar.exportlink.title": "Teberdayış", "timeslider.toolbar.exportlink.title": "Teberdayış",
"timeslider.exportCurrent": "Versiyonê enewki teber de:", "timeslider.exportCurrent": "Versiyonê enewki teber de:",
"timeslider.version": "Versiyonê {{version}}", "timeslider.version": "Versiyonê {{version}}",
"timeslider.saved": "{{day}} {{month}}, {{year}} de biyo qeyd", "timeslider.saved": "{{day}} {{month}}, {{year}} de biyo qeyd",
"timeslider.playPause": "Zerrekê bloknoti kayfi/vındarn", "timeslider.playPause": "Zerrekê bloknoti kayfi/vındarn",
"timeslider.backRevision": "Peyser şo revizyona ena bloknoter", "timeslider.backRevision": "Peyser şo çımraviyarnayışê na bloknoti",
"timeslider.forwardRevision": "Ena bloknot de şo revizyonê bini", "timeslider.forwardRevision": "Ena bloknot de şo revizyonê bini",
"timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
"timeslider.month.january": "Çele", "timeslider.month.january": "Çele",

View file

@ -27,6 +27,10 @@
"admin_plugins.page-title": "Plugin-en kudeaketa - Etherpad", "admin_plugins.page-title": "Plugin-en kudeaketa - Etherpad",
"admin_plugins.version": "Bertsioa", "admin_plugins.version": "Bertsioa",
"admin_plugins_info": "Arazoak konpontzeko informazioa", "admin_plugins_info": "Arazoak konpontzeko informazioa",
"admin_plugins_info.hooks": "Instalatutako kakoak",
"admin_plugins_info.hooks_client": "Bezeroaren aldeko kakoak",
"admin_plugins_info.hooks_server": "Zerbitzari aldeko kakoak",
"admin_plugins_info.parts": "Instalatutako atalaka",
"admin_plugins_info.plugins": "Instalatutako plugin-ak", "admin_plugins_info.plugins": "Instalatutako plugin-ak",
"admin_plugins_info.page-title": "Plugin-en informazioa - Etherpad", "admin_plugins_info.page-title": "Plugin-en informazioa - Etherpad",
"admin_plugins_info.version": "Etherpad bertsioa", "admin_plugins_info.version": "Etherpad bertsioa",
@ -34,6 +38,8 @@
"admin_plugins_info.version_number": "Bertsio-zenbakia", "admin_plugins_info.version_number": "Bertsio-zenbakia",
"admin_settings": "Ezarpenak", "admin_settings": "Ezarpenak",
"admin_settings.current": "Oraingo konfigurazioa", "admin_settings.current": "Oraingo konfigurazioa",
"admin_settings.current_example-devel": "Adibiderako garapenerako ezarpenen txantiloia",
"admin_settings.current_example-prod": "Adibiderako lanerako ezarpenen txantiloia",
"admin_settings.current_restart.value": "Berrabiarazi Etherpad", "admin_settings.current_restart.value": "Berrabiarazi Etherpad",
"admin_settings.current_save.value": "Gorde Ezarpenak", "admin_settings.current_save.value": "Gorde Ezarpenak",
"admin_settings.page-title": "Ezarpenak - Etherpad", "admin_settings.page-title": "Ezarpenak - Etherpad",
@ -108,8 +114,10 @@
"pad.modals.corruptPad.cause": "Baliteke zerbitzari okerreko konfigurazioagatik edo beste ustekabeko portaera batengatik izatea. Jarri harremanetan zerbitzu-administratzailearekin.", "pad.modals.corruptPad.cause": "Baliteke zerbitzari okerreko konfigurazioagatik edo beste ustekabeko portaera batengatik izatea. Jarri harremanetan zerbitzu-administratzailearekin.",
"pad.modals.deleted": "Ezabatua.", "pad.modals.deleted": "Ezabatua.",
"pad.modals.deleted.explanation": "Pad hau ezabatu da.", "pad.modals.deleted.explanation": "Pad hau ezabatu da.",
"pad.modals.rateLimited": "Baloratzea Mugatuta.",
"pad.modals.rateLimited.explanation": "Pad honetara mezu gehiegi bidali dituzu eta ondorioz deskonektatu zaizu.", "pad.modals.rateLimited.explanation": "Pad honetara mezu gehiegi bidali dituzu eta ondorioz deskonektatu zaizu.",
"pad.modals.rejected.explanation": "Zerbitzariak zure nabigatzailetik bidali den mezu bat baztertu du.", "pad.modals.rejected.explanation": "Zerbitzariak zure nabigatzailetik bidali den mezu bat baztertu du.",
"pad.modals.rejected.cause": "Baliteke pad-a ikusten ari zinen bitartean zerbitzaria eguneratu izana, edo bestela Etherpad-en arazo bat egon liteke. Orria freskatzen saiatu zaitez.",
"pad.modals.disconnected": "Deskonektatua izan zara.", "pad.modals.disconnected": "Deskonektatua izan zara.",
"pad.modals.disconnected.explanation": "Zerbitzariarekiko konexioa galdu da", "pad.modals.disconnected.explanation": "Zerbitzariarekiko konexioa galdu da",
"pad.modals.disconnected.cause": "Baliteke zerbitzaria eskuragarri ez egotea. Mesedez, jakinarazi zerbitzuko administratzaileari honek gertatzen jarraitzen badu.", "pad.modals.disconnected.cause": "Baliteke zerbitzaria eskuragarri ez egotea. Mesedez, jakinarazi zerbitzuko administratzaileari honek gertatzen jarraitzen badu.",
@ -122,6 +130,7 @@
"pad.chat.loadmessages": "Kargatu mezu gehiago", "pad.chat.loadmessages": "Kargatu mezu gehiago",
"pad.chat.stick.title": "Itsatsi txata pantailan", "pad.chat.stick.title": "Itsatsi txata pantailan",
"pad.chat.writeMessage.placeholder": "Idatzi hemen zure mezua", "pad.chat.writeMessage.placeholder": "Idatzi hemen zure mezua",
"timeslider.followContents": "Jarraitu pad-aren edukien eguneratzeak",
"timeslider.pageTitle": "{{appTitle}} Denbora-lerroa", "timeslider.pageTitle": "{{appTitle}} Denbora-lerroa",
"timeslider.toolbar.returnbutton": "Itzuli pad-era", "timeslider.toolbar.returnbutton": "Itzuli pad-era",
"timeslider.toolbar.authors": "Egileak:", "timeslider.toolbar.authors": "Egileak:",

View file

@ -18,6 +18,8 @@
"VezonThunder" "VezonThunder"
] ]
}, },
"admin_plugins.available": "Saatavilla olevat liitännäiset",
"admin_plugins.available_install.value": "Lataa",
"admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia", "admin_plugins.available_search.placeholder": "Etsi asennettavia laajennuksia",
"admin_plugins.description": "Kuvaus", "admin_plugins.description": "Kuvaus",
"admin_plugins.installed": "Asennetut laajennukset", "admin_plugins.installed": "Asennetut laajennukset",
@ -41,6 +43,8 @@
"admin_settings": "Asetukset", "admin_settings": "Asetukset",
"admin_settings.current": "Nykyinen kokoonpano", "admin_settings.current": "Nykyinen kokoonpano",
"admin_settings.current_example-devel": "Esimerkki kehitysasetusten mallista", "admin_settings.current_example-devel": "Esimerkki kehitysasetusten mallista",
"admin_settings.current_save.value": "Tallenna Asetukset",
"admin_settings.page-title": "asetukset - Etherpad",
"index.newPad": "Uusi muistio", "index.newPad": "Uusi muistio",
"index.createOpenPad": "tai luo tai avaa muistio nimellä:", "index.createOpenPad": "tai luo tai avaa muistio nimellä:",
"pad.toolbar.bold.title": "Lihavointi (Ctrl-B)", "pad.toolbar.bold.title": "Lihavointi (Ctrl-B)",
@ -89,7 +93,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Tuonti on tuettu vain HTML- ja raakatekstitiedostoista. Monipuoliset tuontiominaisuudet ovat käytettävissä <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">asentamalla AbiWordin tai LibreOfficen</a>.", "pad.importExport.abiword.innerHTML": "Tuonti on tuettu vain HTML- ja raakatekstitiedostoista. Monipuoliset tuontiominaisuudet ovat käytettävissä <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">asentamalla AbiWordin tai LibreOfficen</a>.",
"pad.modals.connected": "Yhdistetty.", "pad.modals.connected": "Yhdistetty.",
"pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon uudelleen...", "pad.modals.reconnecting": "Muodostetaan yhteyttä muistioon uudelleen",
"pad.modals.forcereconnect": "Pakota yhdistämään uudelleen", "pad.modals.forcereconnect": "Pakota yhdistämään uudelleen",
"pad.modals.reconnecttimer": "Yritetään yhdistää uudelleen", "pad.modals.reconnecttimer": "Yritetään yhdistää uudelleen",
"pad.modals.cancel": "Peruuta", "pad.modals.cancel": "Peruuta",

View file

@ -30,29 +30,29 @@
] ]
}, },
"admin.page-title": "Tableau de bord administrateur — Etherpad", "admin.page-title": "Tableau de bord administrateur — Etherpad",
"admin_plugins": "Gestionnaire de compléments", "admin_plugins": "Gestionnaire de greffons",
"admin_plugins.available": "Compléments disponibles", "admin_plugins.available": "Greffons disponibles",
"admin_plugins.available_not-found": "Aucun complément trouvé.", "admin_plugins.available_not-found": "Aucun greffon trouvé.",
"admin_plugins.available_fetching": "Récupération", "admin_plugins.available_fetching": "Récupération en cours...",
"admin_plugins.available_install.value": "Installer", "admin_plugins.available_install.value": "Installer",
"admin_plugins.available_search.placeholder": "Rechercher des compléments à installer", "admin_plugins.available_search.placeholder": "Rechercher des greffons à installer",
"admin_plugins.description": "Description", "admin_plugins.description": "Description",
"admin_plugins.installed": "Compléments installés", "admin_plugins.installed": "Greffons installés",
"admin_plugins.installed_fetching": "Récupération des compléments installés…", "admin_plugins.installed_fetching": "Récupération des greffons installés en cours...",
"admin_plugins.installed_nothing": "Vous navez pas encore installé de complément.", "admin_plugins.installed_nothing": "Vous navez encore installé aucun greffon.",
"admin_plugins.installed_uninstall.value": "Désinstaller", "admin_plugins.installed_uninstall.value": "Désinstaller",
"admin_plugins.last-update": "Dernière mise à jour", "admin_plugins.last-update": "Dernière mise à jour",
"admin_plugins.name": "Nom", "admin_plugins.name": "Nom",
"admin_plugins.page-title": "Gestionnaire de compléments — Etherpad", "admin_plugins.page-title": "Gestionnaire de greffons — Etherpad",
"admin_plugins.version": "Version", "admin_plugins.version": "Version",
"admin_plugins_info": "Information de résolution de problème", "admin_plugins_info": "Informations de résolution de problème",
"admin_plugins_info.hooks": "Crochets installés", "admin_plugins_info.hooks": "Crochets installés",
"admin_plugins_info.hooks_client": "Crochets côté client", "admin_plugins_info.hooks_client": "Crochets côté client",
"admin_plugins_info.hooks_server": "Crochets côté serveur", "admin_plugins_info.hooks_server": "Crochets côté serveur",
"admin_plugins_info.parts": "Parties installées", "admin_plugins_info.parts": "Parties installées",
"admin_plugins_info.plugins": "Compléments installés", "admin_plugins_info.plugins": "Greffons installés",
"admin_plugins_info.page-title": "Information de complément — Etherpad", "admin_plugins_info.page-title": "Informations du greffon — Etherpad",
"admin_plugins_info.version": "Version Etherpad", "admin_plugins_info.version": "Version dEtherpad",
"admin_plugins_info.version_latest": "Dernière version disponible", "admin_plugins_info.version_latest": "Dernière version disponible",
"admin_plugins_info.version_number": "Numéro de version", "admin_plugins_info.version_number": "Numéro de version",
"admin_settings": "Paramètres", "admin_settings": "Paramètres",
@ -64,7 +64,7 @@
"admin_settings.page-title": "Paramètres — Etherpad", "admin_settings.page-title": "Paramètres — Etherpad",
"index.newPad": "Nouveau bloc-notes", "index.newPad": "Nouveau bloc-notes",
"index.createOpenPad": "ou créer/ouvrir un bloc-notes intitulé:", "index.createOpenPad": "ou créer/ouvrir un bloc-notes intitulé:",
"index.openPad": "ouvrir un Pad existant avec le nom :", "index.openPad": "ouvrir un bloc-note existant avec le nom:",
"pad.toolbar.bold.title": "Gras (Ctrl+B)", "pad.toolbar.bold.title": "Gras (Ctrl+B)",
"pad.toolbar.italic.title": "Italique (Ctrl+I)", "pad.toolbar.italic.title": "Italique (Ctrl+I)",
"pad.toolbar.underline.title": "Souligné (Ctrl+U)", "pad.toolbar.underline.title": "Souligné (Ctrl+U)",
@ -76,7 +76,7 @@
"pad.toolbar.undo.title": "Annuler (Ctrl+Z)", "pad.toolbar.undo.title": "Annuler (Ctrl+Z)",
"pad.toolbar.redo.title": "Rétablir (Ctrl+Y)", "pad.toolbar.redo.title": "Rétablir (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Effacer le surlignage par auteur (Ctrl+Shift+C)", "pad.toolbar.clearAuthorship.title": "Effacer le surlignage par auteur (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Importer de/Exporter vers un format de fichier différent", "pad.toolbar.import_export.title": "Importer/Exporter des formats de fichiers différents",
"pad.toolbar.timeslider.title": "Historique dynamique", "pad.toolbar.timeslider.title": "Historique dynamique",
"pad.toolbar.savedRevision.title": "Enregistrer la révision", "pad.toolbar.savedRevision.title": "Enregistrer la révision",
"pad.toolbar.settings.title": "Paramètres", "pad.toolbar.settings.title": "Paramètres",
@ -85,7 +85,7 @@
"pad.colorpicker.save": "Enregistrer", "pad.colorpicker.save": "Enregistrer",
"pad.colorpicker.cancel": "Annuler", "pad.colorpicker.cancel": "Annuler",
"pad.loading": "Chargement...", "pad.loading": "Chargement...",
"pad.noCookie": "Un cookie na pas pu être trouvé. Veuillez autoriser les fichiers témoins (ou cookies) dans votre navigateur ! Votre session et vos paramètres ne seront pas enregistrés entre les visites. Cela peut être dû au fait quEtehrpad est inclus dans un iFrame dans certains navigateurs. Veuillez vous assurer que Etherpad est dans le même sous-domaine/domaine que son iFrame parent", "pad.noCookie": "Un fichier témoin (ou ''cookie'') na pas pu être trouvé. Veuillez autoriser les fichiers témoins dans votre navigateur! Votre session et vos paramètres ne seront pas enregistrés entre les visites. Cela peut être dû au fait quEtherpad est inclus dans un ''iFrame'' dans certains navigateurs. Veuillez vous assurer quEtherpad est dans le même sous-domaine/domaine que son ''iFrame'' parent.",
"pad.permissionDenied": "Vous nêtes pas autorisé à accéder à ce bloc-notes", "pad.permissionDenied": "Vous nêtes pas autorisé à accéder à ce bloc-notes",
"pad.settings.padSettings": "Paramètres du bloc-notes", "pad.settings.padSettings": "Paramètres du bloc-notes",
"pad.settings.myView": "Ma vue", "pad.settings.myView": "Ma vue",
@ -94,11 +94,11 @@
"pad.settings.colorcheck": "Surlignage par auteur", "pad.settings.colorcheck": "Surlignage par auteur",
"pad.settings.linenocheck": "Numéros de lignes", "pad.settings.linenocheck": "Numéros de lignes",
"pad.settings.rtlcheck": "Le contenu doit-il être lu de droite à gauche?", "pad.settings.rtlcheck": "Le contenu doit-il être lu de droite à gauche?",
"pad.settings.fontType": "Police:", "pad.settings.fontType": "Type de police:",
"pad.settings.fontType.normal": "Normal", "pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Langue:", "pad.settings.language": "Langue:",
"pad.settings.about": "À propos", "pad.settings.about": "À propos",
"pad.settings.poweredBy": "Fourni par", "pad.settings.poweredBy": "Propulsé par",
"pad.importExport.import_export": "Importer/Exporter", "pad.importExport.import_export": "Importer/Exporter",
"pad.importExport.import": "Charger un texte ou un document", "pad.importExport.import": "Charger un texte ou un document",
"pad.importExport.importSuccessful": "Réussi!", "pad.importExport.importSuccessful": "Réussi!",
@ -111,13 +111,13 @@
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Vous ne pouvez importer que des formats texte brut ou HTML. Pour des fonctionnalités dimportation plus évoluées, veuillez <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installer AbiWord ou LibreOffice</a>.", "pad.importExport.abiword.innerHTML": "Vous ne pouvez importer que des formats texte brut ou HTML. Pour des fonctionnalités dimportation plus évoluées, veuillez <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installer AbiWord ou LibreOffice</a>.",
"pad.modals.connected": "Connecté.", "pad.modals.connected": "Connecté.",
"pad.modals.reconnecting": "Reconnexion à votre bloc-notes...", "pad.modals.reconnecting": "Reconnexion à votre bloc-notes en cours...",
"pad.modals.forcereconnect": "Forcer la reconnexion", "pad.modals.forcereconnect": "Forcer la reconnexion",
"pad.modals.reconnecttimer": "Essai de reconnexion", "pad.modals.reconnecttimer": "Essai de reconnexion",
"pad.modals.cancel": "Annuler", "pad.modals.cancel": "Annuler",
"pad.modals.userdup": "Ouvert dans une autre fenêtre", "pad.modals.userdup": "Ouvert dans une autre fenêtre",
"pad.modals.userdup.explanation": "Ce bloc-notes semble être ouvert dans plusieurs fenêtres sur cet ordinateur.", "pad.modals.userdup.explanation": "Ce bloc-notes semble être ouvert dans plusieurs fenêtres sur cet ordinateur.",
"pad.modals.userdup.advice": "Se reconnecter en utilisant cette fenêtre.", "pad.modals.userdup.advice": "Se reconnecter en utilisant plutôt cette fenêtre.",
"pad.modals.unauth": "Non autorisé", "pad.modals.unauth": "Non autorisé",
"pad.modals.unauth.explanation": "Vos autorisations ont été changées lors de laffichage de cette page. Essayez de vous reconnecter.", "pad.modals.unauth.explanation": "Vos autorisations ont été changées lors de laffichage de cette page. Essayez de vous reconnecter.",
"pad.modals.looping.explanation": "Nous éprouvons des problèmes de communication au serveur de synchronisation.", "pad.modals.looping.explanation": "Nous éprouvons des problèmes de communication au serveur de synchronisation.",
@ -133,10 +133,10 @@
"pad.modals.corruptPad.cause": "Cela peut être dû à une mauvaise configuration du serveur ou à un autre comportement inattendu. Veuillez contacter ladministrateur du service.", "pad.modals.corruptPad.cause": "Cela peut être dû à une mauvaise configuration du serveur ou à un autre comportement inattendu. Veuillez contacter ladministrateur du service.",
"pad.modals.deleted": "Supprimé.", "pad.modals.deleted": "Supprimé.",
"pad.modals.deleted.explanation": "Ce bloc-notes a été supprimé.", "pad.modals.deleted.explanation": "Ce bloc-notes a été supprimé.",
"pad.modals.rateLimited": "Taux limité.", "pad.modals.rateLimited": "Flot limité.",
"pad.modals.rateLimited.explanation": "Vous avez envoyé trop de messages à ce bloc, il vous a donc déconnecté.", "pad.modals.rateLimited.explanation": "Vous avez envoyé trop de messages à ce bloc-notes, il vous a donc déconnecté.",
"pad.modals.rejected.explanation": "Le serveur a rejeté un message qui a été envoyé par votre navigateur.", "pad.modals.rejected.explanation": "Le serveur a rejeté un message qui a été envoyé par votre navigateur.",
"pad.modals.rejected.cause": "Le serveur peut avoir été mis à jour pendant que vous regardiez le bloc, ou il y a peut-être un bogue dans Etherpad. Essayez de recharger la page.", "pad.modals.rejected.cause": "Le serveur peut avoir été mis à jour pendant que vous regardiez le bloc-notes, ou il y a peut-être une anomalie dans Etherpad. Essayez de recharger la page.",
"pad.modals.disconnected": "Vous avez été déconnecté.", "pad.modals.disconnected": "Vous avez été déconnecté.",
"pad.modals.disconnected.explanation": "La connexion au serveur a échoué.", "pad.modals.disconnected.explanation": "La connexion au serveur a échoué.",
"pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer ladministrateur du service.", "pad.modals.disconnected.cause": "Il se peut que le serveur soit indisponible. Si le problème persiste, veuillez en informer ladministrateur du service.",
@ -149,7 +149,7 @@
"pad.chat.loadmessages": "Charger davantage de messages", "pad.chat.loadmessages": "Charger davantage de messages",
"pad.chat.stick.title": "Ancrer la discussion sur lécran", "pad.chat.stick.title": "Ancrer la discussion sur lécran",
"pad.chat.writeMessage.placeholder": "Entrez votre message ici", "pad.chat.writeMessage.placeholder": "Entrez votre message ici",
"timeslider.followContents": "Suivre les mises à jour de contenu du bloc", "timeslider.followContents": "Suivre les mises à jour de contenu du bloc-notes",
"timeslider.pageTitle": "Historique dynamique de {{appTitle}}", "timeslider.pageTitle": "Historique dynamique de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Retourner au bloc-notes", "timeslider.toolbar.returnbutton": "Retourner au bloc-notes",
"timeslider.toolbar.authors": "Auteurs:", "timeslider.toolbar.authors": "Auteurs:",
@ -158,7 +158,7 @@
"timeslider.exportCurrent": "Exporter la version actuelle sous:", "timeslider.exportCurrent": "Exporter la version actuelle sous:",
"timeslider.version": "Version {{version}}", "timeslider.version": "Version {{version}}",
"timeslider.saved": "Enregistré le {{day}} {{month}} {{year}}", "timeslider.saved": "Enregistré le {{day}} {{month}} {{year}}",
"timeslider.playPause": "Lecture / Pause des contenus du bloc-notes", "timeslider.playPause": "Lecture/Pause des contenus du bloc-notes",
"timeslider.backRevision": "Reculer dune révision dans ce bloc-notes", "timeslider.backRevision": "Reculer dune révision dans ce bloc-notes",
"timeslider.forwardRevision": "Avancer dune révision dans ce bloc-notes", "timeslider.forwardRevision": "Avancer dune révision dans ce bloc-notes",
"timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}",
@ -184,10 +184,10 @@
"pad.impexp.importing": "Import en cours...", "pad.impexp.importing": "Import en cours...",
"pad.impexp.confirmimport": "Importer un fichier écrasera le contenu actuel du bloc-notes. Êtes-vous sûr de vouloir le faire?", "pad.impexp.confirmimport": "Importer un fichier écrasera le contenu actuel du bloc-notes. Êtes-vous sûr de vouloir le faire?",
"pad.impexp.convertFailed": "Nous ne pouvons pas importer ce fichier. Veuillez utiliser un autre format de document ou faire manuellement un copier/coller du texte brut", "pad.impexp.convertFailed": "Nous ne pouvons pas importer ce fichier. Veuillez utiliser un autre format de document ou faire manuellement un copier/coller du texte brut",
"pad.impexp.padHasData": "Nous navons pas pu importer ce fichier parce que ce bloc-notes a déjà été modifié; veuillez limporter vers un nouveau bloc-notes", "pad.impexp.padHasData": "Nous navons pas pu importer ce fichier parce que ce bloc-notes a déjà été modifié; veuillez limporter vers un nouveau bloc-notes.",
"pad.impexp.uploadFailed": "Le téléversement a échoué, veuillez réessayer", "pad.impexp.uploadFailed": "Le téléversement a échoué, veuillez réessayer.",
"pad.impexp.importfailed": "Échec de limportation", "pad.impexp.importfailed": "Échec de limport",
"pad.impexp.copypaste": "Veuillez copier-coller", "pad.impexp.copypaste": "Veuillez copier-coller",
"pad.impexp.exportdisabled": "Lexportation au format {{type}} est désactivée. Veuillez contacter votre administrateur système pour plus de détails.", "pad.impexp.exportdisabled": "Lexportation au format {{type}} est désactivée. Veuillez contacter votre administrateur système pour plus de détails.",
"pad.impexp.maxFileSize": "Fichier trop gros. Contactez votre administrateur de site pour augmenter la taille maximale des fichiers importés" "pad.impexp.maxFileSize": "Fichier trop gros. Contactez votre administrateur de site pour augmenter la taille maximale des fichiers importés."
} }

View file

@ -35,8 +35,5 @@
"timeslider.month.october": "oktober", "timeslider.month.october": "oktober",
"timeslider.month.november": "novimber", "timeslider.month.november": "novimber",
"timeslider.month.december": "desimber", "timeslider.month.december": "desimber",
"pad.userlist.unnamed": "sûnder namme", "pad.userlist.unnamed": "sûnder namme"
"pad.userlist.guest": "Gast",
"pad.userlist.deny": "Wegerje",
"pad.userlist.approve": "Goedkarre"
} }

View file

@ -2,12 +2,47 @@
"@metadata": { "@metadata": {
"authors": [ "authors": [
"Elisardojm", "Elisardojm",
"Ghose",
"Toliño" "Toliño"
] ]
}, },
"admin.page-title": "Panel de administración - Etherpad",
"admin_plugins": "Xestor de complementos",
"admin_plugins.available": "Complementos dispoñibles",
"admin_plugins.available_not-found": "Non se atopan complementos.",
"admin_plugins.available_fetching": "Obtendo...",
"admin_plugins.available_install.value": "Instalar",
"admin_plugins.available_search.placeholder": "Buscar complementos para instalar",
"admin_plugins.description": "Descrición",
"admin_plugins.installed": "Complementos instalados",
"admin_plugins.installed_fetching": "Obtendo os complementos instalados...",
"admin_plugins.installed_nothing": "Aínda non instalaches ningún complemento.",
"admin_plugins.installed_uninstall.value": "Desinstalar",
"admin_plugins.last-update": "Última actualización",
"admin_plugins.name": "Nome",
"admin_plugins.page-title": "Xestos de complementos - Etherpad",
"admin_plugins.version": "Versión",
"admin_plugins_info": "Información para resolver 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",
"admin_plugins_info.parts": "Partes instaladas",
"admin_plugins_info.plugins": "Complementos instalados",
"admin_plugins_info.page-title": "Información do complemento - Etherpad",
"admin_plugins_info.version": "Versión de Etherpad",
"admin_plugins_info.version_latest": "Última versión dispoñible",
"admin_plugins_info.version_number": "Número da versión",
"admin_settings": "Axustes",
"admin_settings.current": "Configuración actual",
"admin_settings.current_example-devel": "Modelo de exemplo dos axustes de desenvolvemento",
"admin_settings.current_example-prod": "Modelo de exemplo dos axustes en produción",
"admin_settings.current_restart.value": "Reiniciar Etherpad",
"admin_settings.current_save.value": "Gardar axustes",
"admin_settings.page-title": "Axustes - Etherpad",
"index.newPad": "Novo documento", "index.newPad": "Novo documento",
"index.createOpenPad": "ou cree/abra un documento co nome:", "index.createOpenPad": "ou crea/abre un documento co nome:",
"pad.toolbar.bold.title": "Negra (Ctrl-B)", "index.openPad": "abrir un Pad existente co nome:",
"pad.toolbar.bold.title": "Resaltado (Ctrl-B)",
"pad.toolbar.italic.title": "Cursiva (Ctrl-I)", "pad.toolbar.italic.title": "Cursiva (Ctrl-I)",
"pad.toolbar.underline.title": "Subliñar (Ctrl-U)", "pad.toolbar.underline.title": "Subliñar (Ctrl-U)",
"pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)", "pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)",
@ -17,28 +52,30 @@
"pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)", "pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)",
"pad.toolbar.undo.title": "Desfacer (Ctrl-Z)", "pad.toolbar.undo.title": "Desfacer (Ctrl-Z)",
"pad.toolbar.redo.title": "Refacer (Ctrl-Y)", "pad.toolbar.redo.title": "Refacer (Ctrl-Y)",
"pad.toolbar.clearAuthorship.title": "Limpar as cores de identificación dos autores (Ctrl+Shift+C)", "pad.toolbar.clearAuthorship.title": "Eliminar as cores que identifican ás autoras (Ctrl+Shift+C)",
"pad.toolbar.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro", "pad.toolbar.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro",
"pad.toolbar.timeslider.title": "Liña do tempo", "pad.toolbar.timeslider.title": "Liña do tempo",
"pad.toolbar.savedRevision.title": "Gardar a revisión", "pad.toolbar.savedRevision.title": "Gardar a revisión",
"pad.toolbar.settings.title": "Configuracións", "pad.toolbar.settings.title": "Axustes",
"pad.toolbar.embed.title": "Compartir e incorporar este documento", "pad.toolbar.embed.title": "Compartir e incorporar este documento",
"pad.toolbar.showusers.title": "Mostrar os usuarios deste documento", "pad.toolbar.showusers.title": "Mostrar as usuarias deste documento",
"pad.colorpicker.save": "Gardar", "pad.colorpicker.save": "Gardar",
"pad.colorpicker.cancel": "Cancelar", "pad.colorpicker.cancel": "Cancelar",
"pad.loading": "Cargando...", "pad.loading": "Cargando...",
"pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!", "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilita as cookies no teu navegador! A túa sesión e axustes non se gardarán entre visitas. Esto podería deberse a que Etherpad está incluído nalgún iFrame nalgúns navegadores. Asegúrate de que Etherpad está no mesmo subdominio/dominio que o iFrame pai",
"pad.permissionDenied": "Non ten permiso para acceder a este documento", "pad.permissionDenied": "Non tes permiso para acceder a este documento",
"pad.settings.padSettings": "Configuracións do documento", "pad.settings.padSettings": "Configuracións do documento",
"pad.settings.myView": "A miña vista", "pad.settings.myView": "A miña vista",
"pad.settings.stickychat": "Chat sempre visible", "pad.settings.stickychat": "Chat sempre visible",
"pad.settings.chatandusers": "Mostrar o chat e os usuarios", "pad.settings.chatandusers": "Mostrar o chat e os usuarios",
"pad.settings.colorcheck": "Cores de identificación", "pad.settings.colorcheck": "Cores de identificación",
"pad.settings.linenocheck": "Números de liña", "pad.settings.linenocheck": "Números de liña",
"pad.settings.rtlcheck": "Quere ler o contido da dereita á esquerda?", "pad.settings.rtlcheck": "Queres ler o contido da dereita á esquerda?",
"pad.settings.fontType": "Tipo de letra:", "pad.settings.fontType": "Tipo de letra:",
"pad.settings.fontType.normal": "Normal", "pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Lingua:", "pad.settings.language": "Lingua:",
"pad.settings.about": "Acerca de",
"pad.settings.poweredBy": "Grazas a",
"pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import_export": "Importar/Exportar",
"pad.importExport.import": "Cargar un ficheiro de texto ou documento", "pad.importExport.import": "Cargar un ficheiro de texto ou documento",
"pad.importExport.importSuccessful": "Correcto!", "pad.importExport.importSuccessful": "Correcto!",
@ -49,9 +86,9 @@
"pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF", "pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Só pode 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\">instale AbiWord</a>.", "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.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.forcereconnect": "Forzar a reconexión",
"pad.modals.reconnecttimer": "Intentarase reconectar en", "pad.modals.reconnecttimer": "Intentarase reconectar en",
"pad.modals.cancel": "Cancelar", "pad.modals.cancel": "Cancelar",
@ -73,6 +110,10 @@
"pad.modals.corruptPad.cause": "Isto pode deberse a unha cofiguración errónea do servidor ou algún outro comportamento inesperado. Póñase en contacto co administrador do servizo.", "pad.modals.corruptPad.cause": "Isto pode deberse a unha cofiguración errónea do servidor ou algún outro comportamento inesperado. Póñase en contacto co administrador do servizo.",
"pad.modals.deleted": "Borrado.", "pad.modals.deleted": "Borrado.",
"pad.modals.deleted.explanation": "Este documento foi eliminado.", "pad.modals.deleted.explanation": "Este documento foi eliminado.",
"pad.modals.rateLimited": "Taxa limitada.",
"pad.modals.rateLimited.explanation": "Enviaches demasiadas mensaxes a este documento polo que te desconectamos.",
"pad.modals.rejected.explanation": "O servidor rexeitou unha mensaxe que o teu navegador enviou.",
"pad.modals.rejected.cause": "O servidor podería ter sido actualizado mentras ollabas o documento, ou pode que sexa un fallo de Etherpad. Intenta recargar a páxina.",
"pad.modals.disconnected": "Foi desconectado.", "pad.modals.disconnected": "Foi desconectado.",
"pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor", "pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor",
"pad.modals.disconnected.cause": "O servidor non está dispoñible. Póñase en contacto co administrador do servizo se o problema continúa.", "pad.modals.disconnected.cause": "O servidor non está dispoñible. Póñase en contacto co administrador do servizo se o problema continúa.",
@ -83,6 +124,9 @@
"pad.chat": "Chat", "pad.chat": "Chat",
"pad.chat.title": "Abrir o chat deste documento.", "pad.chat.title": "Abrir o chat deste documento.",
"pad.chat.loadmessages": "Cargar máis mensaxes", "pad.chat.loadmessages": "Cargar máis mensaxes",
"pad.chat.stick.title": "Pegar a conversa á pantalla",
"pad.chat.writeMessage.placeholder": "Escribe aquí a túa mensaxe",
"timeslider.followContents": "Segue as actualizacións do contido",
"timeslider.pageTitle": "Liña do tempo de {{appTitle}}", "timeslider.pageTitle": "Liña do tempo de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Volver ao documento", "timeslider.toolbar.returnbutton": "Volver ao documento",
"timeslider.toolbar.authors": "Autores:", "timeslider.toolbar.authors": "Autores:",
@ -112,7 +156,7 @@
"pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo", "pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo",
"pad.userlist.entername": "Insira o seu nome", "pad.userlist.entername": "Insira o seu nome",
"pad.userlist.unnamed": "anónimo", "pad.userlist.unnamed": "anónimo",
"pad.editbar.clearcolors": "Quere limpar as cores de identificación dos autores en todo o documento?", "pad.editbar.clearcolors": "Eliminar as cores relativas aos autores en todo o documento? Non se poderán recuperar",
"pad.impexp.importbutton": "Importar agora", "pad.impexp.importbutton": "Importar agora",
"pad.impexp.importing": "Importando...", "pad.impexp.importing": "Importando...",
"pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?", "pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?",
@ -121,5 +165,6 @@
"pad.impexp.uploadFailed": "Houbo un erro ao cargar o ficheiro; inténteo de novo", "pad.impexp.uploadFailed": "Houbo un erro ao cargar o ficheiro; inténteo de novo",
"pad.impexp.importfailed": "Fallou a importación", "pad.impexp.importfailed": "Fallou a importación",
"pad.impexp.copypaste": "Copie e pegue", "pad.impexp.copypaste": "Copie e pegue",
"pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles." "pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles.",
"pad.impexp.maxFileSize": "Ficheiro demasiado granda. Contacta coa administración para aumentar o tamaño permitido para importacións"
} }

View file

@ -45,9 +45,6 @@
"timeslider.month.december": "ડિસેમ્બર", "timeslider.month.december": "ડિસેમ્બર",
"pad.userlist.entername": "તમારું નામ દાખલ કરો", "pad.userlist.entername": "તમારું નામ દાખલ કરો",
"pad.userlist.unnamed": "અનામી", "pad.userlist.unnamed": "અનામી",
"pad.userlist.guest": "મહેમાન",
"pad.userlist.deny": "નકારો",
"pad.userlist.approve": "મંજૂર",
"pad.impexp.importbutton": "આયાત કરો", "pad.impexp.importbutton": "આયાત કરો",
"pad.impexp.importing": "આયાત કરે છે..." "pad.impexp.importing": "આયાત કરે છે..."
} }

Some files were not shown because too many files have changed in this diff Show more