diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..6313b56c5
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index dd84ea782..d003a4787 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -23,6 +23,12 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
+**Server (please complete the following information):**
+ - Etherpad version:
+ - OS: [e.g., Ubuntu 20.04]
+ - Node.js version (`node --version`):
+ - npm version (`npm --version`):
+
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml
index a9c8c602e..a7c254042 100644
--- a/.github/workflows/backend-tests.yml
+++ b/.github/workflows/backend-tests.yml
@@ -4,22 +4,27 @@ name: "Backend tests"
on: [push, pull_request]
jobs:
- withoutplugins:
+ withoutpluginsLinux:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
- name: without plugins
+ name: Linux without plugins
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [10, 12, 14, 15]
+
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
- node-version: 12
+ node-version: ${{ matrix.node }}
- name: Install libreoffice
run: |
@@ -33,22 +38,27 @@ jobs:
- name: Run the backend tests
run: cd src && npm test
- withplugins:
+ withpluginsLinux:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
- name: with Plugins
+ name: Linux with Plugins
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [10, 12, 14, 15]
+
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
- node-version: 12
+ node-version: ${{ matrix.node }}
- name: Install libreoffice
run: |
@@ -57,8 +67,10 @@ jobs:
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
- name: Install Etherpad plugins
+ # The --legacy-peer-deps flag is required to work around a bug in npm v7:
+ # https://github.com/npm/cli/issues/2199
run: >
- npm install
+ npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace
@@ -85,3 +97,89 @@ jobs:
- name: Run the backend tests
run: cd src && npm test
+
+ withoutpluginsWindows:
+ # run on pushes to any branch
+ # run on PRs from external forks
+ if: |
+ (github.event_name != 'pull_request')
+ || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
+ name: Windows without plugins
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 12
+
+ - name: Install all dependencies and symlink for ep_etherpad-lite
+ run: |
+ cd src
+ npm ci --no-optional
+
+ - name: Fix up the settings.json
+ run: |
+ powershell -Command "(gc settings.json.template) -replace '\"max\": 10', '\"max\": 10000' | Out-File -encoding ASCII settings.json.holder"
+ powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
+
+ - name: Run the backend tests
+ run: cd src && npm test
+
+ withpluginsWindows:
+ # run on pushes to any branch
+ # run on PRs from external forks
+ if: |
+ (github.event_name != 'pull_request')
+ || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
+ name: Windows with Plugins
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 12
+
+ - name: Install Etherpad plugins
+ # The --legacy-peer-deps flag is required to work around a bug in npm v7:
+ # https://github.com/npm/cli/issues/2199
+ run: >
+ npm install --no-save --legacy-peer-deps
+ 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
+ run: |
+ cd src
+ npm ci --no-optional
+
+ - name: Fix up the settings.json
+ run: |
+ powershell -Command "(gc settings.json.template) -replace '\"max\": 10', '\"max\": 10000' | Out-File -encoding ASCII settings.json.holder"
+ powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
+
+ - name: Run the backend tests
+ run: cd src && npm test
diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml
index e42aa3bb2..f2e2215aa 100644
--- a/.github/workflows/frontend-admin-tests.yml
+++ b/.github/workflows/frontend-admin-tests.yml
@@ -7,30 +7,42 @@ jobs:
name: with plugins
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [10, 12, 14, 15]
+
steps:
+ - name: Generate Sauce Labs strings
+ id: sauce_strings
+ run: |
+ printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }} - Node ${{ matrix.node }}'
+ printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}-node${{ matrix.node }}'
+
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
- node-version: 12
+ node-version: ${{ matrix.node }}
- - 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 etherpad plugins
+ # We intentionally install an old ep_align version to test upgrades to the minor version number.
+ # The --legacy-peer-deps flag is required to work around a bug in npm v7:
+ # https://github.com/npm/cli/issues/2199
+ run: npm install --no-save --legacy-peer-deps ep_align@0.2.27
+ # 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
- # 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
+ # Nuke plugin tests
- name: Install etherpad plugins
run: rm -Rf node_modules/ep_align/static/tests/*
@@ -38,8 +50,8 @@ jobs:
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: Create settings.json
+ run: cp 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"
@@ -47,12 +59,19 @@ jobs:
- 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
+ - uses: saucelabs/sauce-connect-action@v1.1.2
+ with:
+ username: ${{ secrets.SAUCE_USERNAME }}
+ accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
+ tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }}
+
- 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 }}
+ SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
+ TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
src/tests/frontend/travis/adminrunner.sh
diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml
index 00c8dd6b5..1b57f6514 100644
--- a/.github/workflows/frontend-tests.yml
+++ b/.github/workflows/frontend-tests.yml
@@ -8,6 +8,12 @@ jobs:
runs-on: ubuntu-latest
steps:
+ - name: Generate Sauce Labs strings
+ id: sauce_strings
+ run: |
+ printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}'
+ printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}'
+
- name: Checkout repository
uses: actions/checkout@v2
@@ -15,14 +21,6 @@ jobs:
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
@@ -30,15 +28,26 @@ jobs:
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: Create settings.json
+ run: cp settings.json.template settings.json
+
+ - name: Disable import/export rate limiting
+ run: |
+ sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json
+
+ - uses: saucelabs/sauce-connect-action@v1.1.2
+ with:
+ username: ${{ secrets.SAUCE_USERNAME }}
+ accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
+ tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }}
- name: Run the frontend 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 }}
+ SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
+ TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
src/tests/frontend/travis/runner.sh
@@ -48,6 +57,12 @@ jobs:
runs-on: ubuntu-latest
steps:
+ - name: Generate Sauce Labs strings
+ id: sauce_strings
+ run: |
+ printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}'
+ printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}'
+
- name: Checkout repository
uses: actions/checkout@v2
@@ -55,17 +70,11 @@ jobs:
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 Etherpad plugins
+ # The --legacy-peer-deps flag is required to work around a bug in npm v7:
+ # https://github.com/npm/cli/issues/2199
run: >
- npm install
+ npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace
@@ -94,22 +103,30 @@ jobs:
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: Create settings.json
+ run: cp 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: Disable import/export rate limiting
+ run: |
+ sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json
# 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
run: rm src/tests/frontend/specs/*
+ - uses: saucelabs/sauce-connect-action@v1.1.2
+ with:
+ username: ${{ secrets.SAUCE_USERNAME }}
+ accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
+ tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }}
+
- name: Run the frontend 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 }}
+ SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
+ TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
src/tests/frontend/travis/runner.sh
diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml
index 98379dfe8..787ef1ddd 100644
--- a/.github/workflows/load-test.yml
+++ b/.github/workflows/load-test.yml
@@ -51,8 +51,10 @@ jobs:
run: sudo npm install -g etherpad-load-test
- name: Install etherpad plugins
+ # The --legacy-peer-deps flag is required to work around a bug in npm v7:
+ # https://github.com/npm/cli/issues/2199
run: >
- npm install
+ npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace
diff --git a/.github/workflows/windows-installer.yml b/.github/workflows/windows-installer.yml
new file mode 100644
index 000000000..37d86a9a6
--- /dev/null
+++ b/.github/workflows/windows-installer.yml
@@ -0,0 +1,61 @@
+name: "Windows Installer"
+
+# any branch is useful for testing before a PR is submitted
+on: [push, pull_request]
+
+jobs:
+ build:
+ # run on pushes to any branch
+ # run on PRs from external forks
+ if: |
+ (github.event_name != 'pull_request')
+ || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
+
+ name: Build Zip & Exe
+ runs-on: windows-latest
+
+ steps:
+ - uses: msys2/setup-msys2@v2
+ with:
+ path-type: inherit
+ install: >-
+ zip
+
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 12
+
+ - name: Install all dependencies and symlink for ep_etherpad-lite
+ shell: msys2 {0}
+ run: src/bin/installDeps.sh
+
+ - name: Run the backend tests
+ shell: msys2 {0}
+ run: cd src && npm test
+
+ - name: Build the .zip
+ shell: msys2 {0}
+ run: src/bin/buildForWindows.sh
+
+ - name: Extract the .zip into folder
+ run: 7z x etherpad-lite-win.zip -oetherpad-lite-new
+
+ - name: Grab nsis config
+ run: git clone https://github.com/ether/etherpad_nsis.git
+
+ - name: Create installer
+ uses: joncloud/makensis-action@v3.4
+ with:
+ script-file: 'etherpad_nsis/etherpad.nsi'
+
+ - name: Check something..
+ run: ls etherpad_nsis
+
+ - name: Archive production artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: etherpad-server-windows.exe
+ path: etherpad_nsis/etherpad-server-windows.exe
diff --git a/.github/workflows/windows-zip.yml b/.github/workflows/windows-zip.yml
new file mode 100644
index 000000000..6e5f80bd4
--- /dev/null
+++ b/.github/workflows/windows-zip.yml
@@ -0,0 +1,75 @@
+name: "Windows Zip"
+
+# any branch is useful for testing before a PR is submitted
+on: [push, pull_request]
+
+jobs:
+ build:
+ # run on pushes to any branch
+ # run on PRs from external forks
+ if: |
+ (github.event_name != 'pull_request')
+ || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
+ name: Build
+ runs-on: windows-latest
+
+ steps:
+ - uses: msys2/setup-msys2@v2
+ with:
+ path-type: inherit
+ install: >-
+ zip
+
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 12
+
+ - name: Install all dependencies and symlink for ep_etherpad-lite
+ shell: msys2 {0}
+ run: src/bin/installDeps.sh
+
+ - name: Run the backend tests
+ shell: msys2 {0}
+ run: cd src && npm test
+
+ - name: Build the .zip
+ shell: msys2 {0}
+ run: src/bin/buildForWindows.sh
+
+ - name: Archive production artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: etherpad-lite-win.zip
+ path: etherpad-lite-win.zip
+
+
+ deploy:
+ # run on pushes to any branch
+ # run on PRs from external forks
+ if: |
+ (github.event_name != 'pull_request')
+ || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
+ name: Deploy
+ needs: build
+ runs-on: windows-latest
+
+ steps:
+ - name: Download zip
+ uses: actions/download-artifact@v2
+ with:
+ name: etherpad-lite-win.zip
+
+ - name: Extract Etherpad
+ run: 7z x etherpad-lite-win.zip -oetherpad
+
+ - name: list
+ run: dir etherpad
+
+ - name: Run Etherpad
+ run: |
+ cd etherpad
+ node node_modules\ep_etherpad-lite\node\server.js &
+ curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test
diff --git a/.lgtm.yml b/.lgtm.yml
new file mode 100644
index 000000000..9dd0f16b6
--- /dev/null
+++ b/.lgtm.yml
@@ -0,0 +1,9 @@
+extraction:
+ javascript:
+ index:
+ exclude:
+ - src/static/js/vendors
+ python:
+ index:
+ exclude:
+ - /
diff --git a/.travis.yml b/.travis.yml
index 99aece0b2..44a8693bb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -28,8 +28,10 @@ _install_libreoffice: &install_libreoffice >-
sudo apt-get update &&
sudo apt-get -y install libreoffice libreoffice-pdfimport
+# The --legacy-peer-deps flag is required to work around a bug in npm v7:
+# https://github.com/npm/cli/issues/2199
_install_plugins: &install_plugins >-
- npm install
+ npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a17488f32..0bb356c9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,26 @@
+# 1.8.10
+
+### Security Patches
+
+* Resolve potential ReDoS vulnerability in your project - GHSL-2020-359
+
+### Compatibility changes
+
+* JSONP API has been removed in favor of using the mature OpenAPI implementation.
+* Node 14 is now required for Docker Deployments
+
+### Notable fixes
+
+* Various performance and stability fixes
+
+### Notable enhancements
+
+* Improved line number alignment and user experience around line anchors
+* Notification to admin console if a plugin is missing during user file import
+* Beautiful loading and reconnecting animation
+* Additional code quality improvements
+* Dependency updates
+
# 1.8.9
### Notable fixes
diff --git a/Dockerfile b/Dockerfile
index f0fa3ceb7..29e2b5abc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@
#
# Author: muxator
-FROM node:10-buster-slim
+FROM node:14-buster-slim
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
# plugins to install while building the container. By default no plugins are
@@ -74,4 +74,4 @@ COPY --chown=etherpad:0 ./settings.json.docker /opt/etherpad-lite/settings.json
RUN chmod -R g=u .
EXPOSE 9001
-CMD ["node", "--experimental-worker", "src/node/server.js"]
+CMD ["node", "src/node/server.js"]
diff --git a/README.md b/README.md
index d2dcfbe20..02eeac3f6 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,32 @@
# A real-time collaborative editor for the web
-
-
-[![Travis (.com)](https://api.travis-ci.com/ether/etherpad-lite.svg?branch=develop)](https://travis-ci.com/github/ether/etherpad-lite)
![Demo Etherpad Animated Jif](doc/images/etherpad_demo.gif "Etherpad in action")
# About
Etherpad is a real-time collaborative editor [scalable to thousands of simultaneous real time users](http://scale.etherpad.org/). It provides [full data export](https://github.com/ether/etherpad-lite/wiki/Understanding-Etherpad's-Full-Data-Export-capabilities) capabilities, and runs on _your_ server, under _your_ control.
-**[Try it out](https://video.etherpad.com)**
+# Try it out
+Etherpad is extremely flexible providing you the means to modify it to solve whatever problem your community has. We provide some demo instances for you try different experiences available within Etherpad. Pad content is automatically removed after 24 hours.
+
+* [Rich Editing](https://rich.etherpad.com) - A full rich text WYSIWYG editor.
+* [Minimalist editor](https://minimalist.etherpad.com) - A minimalist editor that can be embedded within your tool.
+* [Dark Mode](https://dark.etherpad.com) - Theme settings to have Etherpad start in dark mode, ideal for using Etherpad at night or for long durations.
+* [Images](https://image.etherpad.com) - Plugins to improve provide Image support within a pad.
+* [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio chat in a pad.
+* [Collaboration++](https://collab.etherpad.com) - Plugins to improve the really-real time collaboration experience, suitable for busy pads.
+* [Document Analysis](https://analysis.etherpad.com) - Plugins to improve author and document analysis during and post creation.
+
+# Project Status
+
+### Code Quality
+[![Code Quality](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml) [![Total alerts](https://img.shields.io/lgtm/alerts/g/ether/etherpad-lite.svg?logo=lgtm&logoWidth=18&color=%2344b492)](https://lgtm.com/projects/g/ether/etherpad-lite/alerts/) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ether/etherpad-lite.svg?logo=lgtm&logoWidth=18&color=%2344b492)](https://lgtm.com/projects/g/ether/etherpad-lite/context:javascript) [![package.lock](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml)
+
+### Testing
+[![Backend tests](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml) [![Simulated Load](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml) [![Rate Limit](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml) [![Windows Zip](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml) [![Docker file](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml)
+[![Frontend admin tests](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) [![Frontend tests](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) [![Windows Installer](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml)
+
+### Engagement
+ ![Discord](https://img.shields.io/discord/741309013593030667?color=%2344b492) ![Etherpad plugins](https://img.shields.io/endpoint?url=https%3A%2F%2Fstatic.etherpad.org%2Fshields.json&color=%2344b492 "Etherpad plugins") ![Languages](https://img.shields.io/static/v1?label=Languages&message=105&color=%2344b492) ![Translation Coverage](https://img.shields.io/static/v1?label=Languages&message=98%&color=%2344b492)
# Installation
@@ -43,7 +61,7 @@ start with `src/bin/run.sh` will update the dependencies.
## Windows
### Prebuilt Windows package
-This package runs on any Windows machine, but for development purposes, please do a manual install.
+This package runs on any Windows machine. You can perform a manual installation via git for development purposes, but as this uses symlinks which performs unreliably on Windows, please stick to the prebuilt package if possible.
1. [Download the latest Windows package](https://etherpad.org/#download)
2. Extract the folder
@@ -105,7 +123,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:
```
-npm install ep_headings2 ep_markdown ep_comments_page ep_align ep_font_color ep_webrtc ep_embedded_hyperlinks2
+npm install --no-save --legacy-peer-deps ep_headings2 ep_markdown ep_comments_page ep_align ep_font_color ep_webrtc ep_embedded_hyperlinks2
```
## Customize the style with skin variants
diff --git a/doc/api/http_api.md b/doc/api/http_api.md
index 0cfc85a07..9cab7f56b 100644
--- a/doc/api/http_api.md
+++ b/doc/api/http_api.md
@@ -134,13 +134,6 @@ Authentication works via a token that is sent with each request as a post parame
All functions will also be available through a node module accessible from other node.js applications.
-### JSONP
-
-The API provides _JSONP_ support to allow requests from a server in a different domain.
-Simply add `&jsonp=?` to the API call.
-
-Example usage: https://api.jquery.com/jQuery.getJSON/
-
## API Methods
### Groups
@@ -636,4 +629,3 @@ get stats of the etherpad instance
*Example returns*
* `{"code":0,"message":"ok","data":{"totalPads":3,"totalSessions": 2,"totalActivePads": 1}}`
-
diff --git a/doc/assets/style.css b/doc/assets/style.css
index 95034c90a..8c6720fa8 100644
--- a/doc/assets/style.css
+++ b/doc/assets/style.css
@@ -1,62 +1,35 @@
-html {
- border-top: solid green 5pt;
-}
-
-body.apidoc {
- width: 60%;
- min-width: 10cm;
+body{
+ border-top: solid #44b492 5pt;
+ line-height:150%;
+ font-family: 'Quicksand',sans-serif;
+ color: #313b4a;
+ max-width:800px;
margin: 0 auto;
+ padding: 20px;
}
-#header {
- padding: 1pc 0;
- color: #111;
+a{
+ color: #555;
}
-a,
-a:active {
- color: #272;
-}
-a:focus,
-a:hover {
- color: #050;
+h1{
+ color: #44b492;
+ line-height:100%;
}
-#apicontent a.mark,
-#apicontent a.mark:active {
- float: right;
- color: #BBB;
- font-size: 0.7cm;
- text-decoration: none;
-}
-#apicontent a.mark:focus,
-#apicontent a.mark:hover {
- color: #AAA;
+a:hover{
+ color: #44b492;
}
-#apicontent code {
- padding: 1px;
- background-color: #EEE;
- border-radius: 4px;
- border: 1px solid #DDD;
-}
-#apicontent pre>code {
- display: block;
- overflow: auto;
- padding: 5px;
+pre{
+ background-color: #e0e0e0;
+ padding:20px;
}
-table, th, td {
- text-align: left;
- border: 1px solid gray;
- border-collapse: collapse;
+code{
+ background-color: #e0e0e0;
}
-th {
- padding: 0.5em;
- background: #EEE;
-}
-
-td {
- padding: 0.5em;
+img {
+ max-width: 100%;
}
diff --git a/doc/plugins.md b/doc/plugins.md
index d8239c68a..1ed0c4f12 100644
--- a/doc/plugins.md
+++ b/doc/plugins.md
@@ -7,8 +7,8 @@ execute its own functionality based on these events.
Publicly available plugins can be found in the npm registry (see
). Etherpad's naming convention for plugins is to prefix your
plugins with `ep_`. So, e.g. it's `ep_flubberworms`. Thus you can install
-plugins from npm, using `npm install ep_flubberworm` in Etherpad's root
-directory.
+plugins from npm, using `npm install --no-save --legacy-peer-deps
+ep_flubberworm` in Etherpad's root directory.
You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will
list all installed plugins and those available on npm. It even provides
diff --git a/src/bin/dirty-db-cleaner.py b/src/bin/dirty-db-cleaner.py
deleted file mode 100755
index dc77cd0da..000000000
--- a/src/bin/dirty-db-cleaner.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env PYTHONUNBUFFERED=1 python
-#
-# Created by Bjarni R. Einarsson, placed in the public domain. Go wild!
-#
-import json
-import os
-import sys
-
-try:
- dirtydb_input = sys.argv[1]
- dirtydb_output = '%s.new' % dirtydb_input
- assert(os.path.exists(dirtydb_input))
- assert(not os.path.exists(dirtydb_output))
-except:
- print()
- print('Usage: %s /path/to/dirty.db' % sys.argv[0])
- print()
- print('Note: Will create a file named dirty.db.new in the same folder,')
- print(' please make sure permissions are OK and a file by that')
- print(' name does not exist already. This script works by omitting')
- print(' duplicate lines from the dirty.db file, keeping only the')
- print(' last (latest) instance. No revision data should be lost,')
- print(' but be careful, make backups. If it breaks you get to keep')
- print(' both pieces!')
- print()
- sys.exit(1)
-
-dirtydb = {}
-lines = 0
-with open(dirtydb_input, 'r') as fd:
- print('Reading %s' % dirtydb_input)
- for line in fd:
- lines += 1
- try:
- data = json.loads(line)
- dirtydb[data['key']] = line
- except:
- print("Skipping invalid JSON!")
- if lines % 10000 == 0:
- sys.stderr.write('.')
-print()
-print('OK, found %d unique keys in %d lines' % (len(dirtydb), lines))
-
-with open(dirtydb_output, 'w') as fd:
- for data in list(dirtydb.values()):
- fd.write(data)
-
-print('Wrote data to %s. All done!' % dirtydb_output)
diff --git a/src/bin/doc/html.js b/src/bin/doc/html.js
index 2c38aec23..f4be7ce13 100644
--- a/src/bin/doc/html.js
+++ b/src/bin/doc/html.js
@@ -137,6 +137,12 @@ const getSection = (lexed) => {
const buildToc = (lexed, filename, cb) => {
let toc = [];
let depth = 0;
+
+ marked.setOptions({
+ headerIds: true,
+ headerPrefix: `${filename}_`,
+ });
+
lexed.forEach((tok) => {
if (tok.type !== 'heading') return;
if (tok.depth - depth > 1) {
diff --git a/src/bin/doc/package-lock.json b/src/bin/doc/package-lock.json
deleted file mode 100644
index 2058974ec..000000000
--- a/src/bin/doc/package-lock.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "node-doc-generator",
- "version": "0.0.0",
- "lockfileVersion": 1,
- "requires": true,
- "dependencies": {
- "marked": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/marked/-/marked-1.1.1.tgz",
- "integrity": "sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw=="
- }
- }
-}
diff --git a/src/bin/doc/package.json b/src/bin/doc/package.json
index f484a5c64..a1a83423c 100644
--- a/src/bin/doc/package.json
+++ b/src/bin/doc/package.json
@@ -7,7 +7,7 @@
"node": ">=10.17.0"
},
"dependencies": {
- "marked": "1.1.1"
+ "marked": "^2.0.0"
},
"devDependencies": {},
"optionalDependencies": {},
diff --git a/src/bin/plugins/checkPlugin.js b/src/bin/plugins/checkPlugin.js
index ee96d63c3..0bf3d25b9 100755
--- a/src/bin/plugins/checkPlugin.js
+++ b/src/bin/plugins/checkPlugin.js
@@ -220,14 +220,14 @@ fs.readdir(pluginPath, (err, rootFiles) => {
}
updateDeps(parsedPackageJSON, 'devDependencies', {
- 'eslint': '^7.18.0',
- 'eslint-config-etherpad': '^1.0.24',
+ 'eslint': '^7.20.0',
+ 'eslint-config-etherpad': '^1.0.25',
'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',
+ 'eslint-plugin-promise': '^4.3.1',
+ 'eslint-plugin-you-dont-need-lodash-underscore': '^6.11.0',
});
updateDeps(parsedPackageJSON, 'peerDependencies', {
diff --git a/src/bin/plugins/lib/README.md b/src/bin/plugins/lib/README.md
index 3a1e26193..e17a23ed1 100755
--- a/src/bin/plugins/lib/README.md
+++ b/src/bin/plugins/lib/README.md
@@ -7,7 +7,10 @@ Explain what your plugin does and who it's useful for.
![screenshot](https://user-images.githubusercontent.com/220864/99979953-97841d80-2d9f-11eb-9782-5f65817c58f4.PNG)
## Installing
-npm install [plugin_name]
+
+```
+npm install --no-save --legacy-peer-deps [plugin_name]
+```
or Use the Etherpad ``/admin`` interface.
diff --git a/src/locales/az.json b/src/locales/az.json
index f1b8d76fe..6daaa4719 100644
--- a/src/locales/az.json
+++ b/src/locales/az.json
@@ -7,6 +7,7 @@
"Mastizada",
"MuratTheTurkish",
"Mushviq Abdulla",
+ "NMW03",
"Neriman2003",
"Vesely35",
"Wertuose"
diff --git a/src/locales/sq.json b/src/locales/sq.json
index 09088946b..f71a7b443 100644
--- a/src/locales/sq.json
+++ b/src/locales/sq.json
@@ -2,10 +2,12 @@
"@metadata": {
"authors": [
"Besnik b",
+ "Eraldkerciku",
"Kosovastar",
"Liridon"
]
},
+ "admin.page-title": "Paneli i Administratorit - Etherpad",
"admin_plugins": "Përgjegjës shtojcash",
"admin_plugins.available": "Shtojca të gatshme",
"admin_plugins.available_not-found": "S’u gjetën shtojca.",
diff --git a/src/node/db/API.js b/src/node/db/API.js
index ea77ac7df..c262e078b 100644
--- a/src/node/db/API.js
+++ b/src/node/db/API.js
@@ -722,7 +722,7 @@ Example returns:
*/
exports.sendClientsMessage = async (padID, msg) => {
- const pad = await getPadSafe(padID, true);
+ await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
padMessageHandler.handleCustomMessage(padID, msg);
};
diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js
index 965f2793f..a34668987 100644
--- a/src/node/db/Pad.js
+++ b/src/node/db/Pad.js
@@ -87,7 +87,7 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author)
// ex. getNumForAuthor
if (author !== '') {
- this.pool.putAttrib(['author', author || '']);
+ this.pool.putAttrib(['author', author]);
}
if (newRev % 100 === 0) {
diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js
index 48507949e..58434c8cd 100644
--- a/src/node/db/PadManager.js
+++ b/src/node/db/PadManager.js
@@ -173,7 +173,7 @@ const padIdTransforms = [
];
// returns a sanitized padId, respecting legacy pad id formats
-exports.sanitizePadId = async function sanitizePadId(padId) {
+exports.sanitizePadId = async (padId) => {
for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
const exists = await exports.doesPadExist(padId);
diff --git a/src/node/eejs/index.js b/src/node/eejs/index.js
index d7d8db5ff..1bf29634b 100644
--- a/src/node/eejs/index.js
+++ b/src/node/eejs/index.js
@@ -84,7 +84,8 @@ exports.require = (name, args, mod) => {
const cache = settings.maxAge !== 0;
const template = cache && templateCache.get(ejspath) || ejs.compile(
- `<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
+ '<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' +
+ `${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
{filename: ejspath});
if (cache) templateCache.set(ejspath, template);
diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js
index c7ff1f661..e32fcf1d0 100644
--- a/src/node/handler/PadMessageHandler.js
+++ b/src/node/handler/PadMessageHandler.js
@@ -669,7 +669,8 @@ const handleUserChanges = async (socket, message) => {
if (Changeset.oldLen(changeset) !== prevText.length) {
socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark();
- throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ${Changeset.oldLen(changeset)} to document of length ${prevText.length}`);
+ throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ` +
+ `${Changeset.oldLen(changeset)} to document of length ${prevText.length}`);
}
try {
@@ -887,7 +888,8 @@ const handleClientReady = async (socket, message, authorID) => {
}
if (message.protocolVersion !== 2) {
- messageLogger.warn(`Dropped message, CLIENT_READY Message has a unknown protocolVersion '${message.protocolVersion}'!`);
+ messageLogger.warn('Dropped message, CLIENT_READY Message has a unknown protocolVersion ' +
+ `'${message.protocolVersion}'!`);
return;
}
@@ -1085,12 +1087,16 @@ const handleClientReady = async (socket, message, authorID) => {
indentationOnNewLine: settings.indentationOnNewLine,
scrollWhenFocusLineIsOutOfViewport: {
percentage: {
- editionAboveViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
- editionBelowViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
+ editionAboveViewport:
+ settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
+ editionBelowViewport:
+ settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
},
duration: settings.scrollWhenFocusLineIsOutOfViewport.duration,
- scrollWhenCaretIsInTheLastLineOfViewport: settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
- percentageToScrollWhenUserPressesArrowUp: settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
+ scrollWhenCaretIsInTheLastLineOfViewport:
+ settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
+ percentageToScrollWhenUserPressesArrowUp:
+ settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
},
initialChangesets: [], // FIXME: REMOVE THIS SHIT
};
@@ -1380,7 +1386,8 @@ const composePadChangesets = async (padId, startNum, endNum) => {
// get all changesets
const changesets = {};
await Promise.all(changesetsNeeded.map(
- (revNum) => pad.getRevisionChangeset(revNum).then((changeset) => changesets[revNum] = changeset)
+ (revNum) => pad.getRevisionChangeset(revNum)
+ .then((changeset) => changesets[revNum] = changeset)
));
// compose Changesets
@@ -1406,7 +1413,7 @@ const _getRoomSockets = (padID) => {
const room = socketio.sockets.adapter.rooms[padID];
if (room) {
- for (const id in room.sockets) {
+ for (const id of Object.keys(room.sockets)) {
roomSockets.push(socketio.sockets.sockets[id]);
}
}
diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js
index 35325c072..cad53d173 100644
--- a/src/node/handler/SocketIORouter.js
+++ b/src/node/handler/SocketIORouter.js
@@ -60,7 +60,7 @@ exports.setSocketIO = (_socket) => {
};
// tell all components about this connect
- for (const i in components) {
+ for (const i of Object.keys(components)) {
components[i].handleConnect(client);
}
@@ -84,7 +84,7 @@ exports.setSocketIO = (_socket) => {
// this instance can be brought out of a scaling cluster.
stats.gauge('lastDisconnect', () => Date.now());
// tell all components about this disconnect
- for (const i in components) {
+ for (const i of Object.keys(components)) {
components[i].handleDisconnect(client);
}
});
diff --git a/src/node/hooks/express/isValidJSONPName.js b/src/node/hooks/express/isValidJSONPName.js
deleted file mode 100644
index c8ca5bea1..000000000
--- a/src/node/hooks/express/isValidJSONPName.js
+++ /dev/null
@@ -1,85 +0,0 @@
-'use strict';
-
-const RESERVED_WORDS = [
- 'abstract',
- 'arguments',
- 'await',
- 'boolean',
- 'break',
- 'byte',
- 'case',
- 'catch',
- 'char',
- 'class',
- 'const',
- 'continue',
- 'debugger',
- 'default',
- 'delete',
- 'do',
- 'double',
- 'else',
- 'enum',
- 'eval',
- 'export',
- 'extends',
- 'false',
- 'final',
- 'finally',
- 'float',
- 'for',
- 'function',
- 'goto',
- 'if',
- 'implements',
- 'import',
- 'in',
- 'instanceof',
- 'int',
- 'interface',
- 'let',
- 'long',
- 'native',
- 'new',
- 'null',
- 'package',
- 'private',
- 'protected',
- 'public',
- 'return',
- 'short',
- 'static',
- 'super',
- 'switch',
- 'synchronized',
- 'this',
- 'throw',
- 'throws',
- 'transient',
- 'true',
- 'try',
- 'typeof',
- 'var',
- 'void',
- 'volatile',
- 'while',
- 'with',
- 'yield',
-];
-
-const regex = /^[a-zA-Z_$][0-9a-zA-Z_$]*(?:\[(?:".+"|'.+'|\d+)\])*?$/;
-
-module.exports.check = (inputStr) => {
- let isValid = true;
- inputStr.split('.').forEach((part) => {
- if (!regex.test(part)) {
- isValid = false;
- }
-
- if (RESERVED_WORDS.indexOf(part) !== -1) {
- isValid = false;
- }
- });
-
- return isValid;
-};
diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js
index bf7e5b147..c4c1ccf5c 100644
--- a/src/node/hooks/express/openapi.js
+++ b/src/node/hooks/express/openapi.js
@@ -22,7 +22,6 @@ const createHTTPError = require('http-errors');
const apiHandler = require('../../handler/APIHandler');
const settings = require('../../utils/Settings');
-const isValidJSONPName = require('./isValidJSONPName');
const log4js = require('log4js');
const logger = log4js.getLogger('API');
@@ -491,7 +490,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
};
// build operations
- for (const funcName in apiHandler.version[version]) {
+ for (const funcName of Object.keys(apiHandler.version[version])) {
let operation = {};
if (operations[funcName]) {
operation = {...operations[funcName]};
@@ -545,7 +544,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
const {app} = args;
// create openapi-backend handlers for each api version under /api/{version}/*
- for (const version in apiHandler.version) {
+ for (const version of Object.keys(apiHandler.version)) {
// we support two different styles of api: flat + rest
// TODO: do we really want to support both?
@@ -573,7 +572,6 @@ exports.expressCreateServer = (hookName, args, cb) => {
// build openapi-backend instance for this api version
const api = new OpenAPIBackend({
- apiRoot, // each api version has its own root
definition,
validate: false,
// for a small optimisation, we can run the quick startup for older
@@ -592,7 +590,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
});
// register operation handlers
- for (const funcName in apiHandler.version[version]) {
+ for (const funcName of Object.keys(apiHandler.version[version])) {
const handler = async (c, req, res) => {
// parse fields from request
const {header, params, query} = c.request;
@@ -687,12 +685,6 @@ exports.expressCreateServer = (hookName, args, cb) => {
}
}
- // support jsonp response format
- if (req.query.jsonp && isValidJSONPName.check(req.query.jsonp)) {
- res.header('Content-Type', 'application/javascript');
- response = `${req.query.jsonp}(${JSON.stringify(response)})`;
- }
-
// send response
return res.send(response);
});
diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js
index c4c1b6751..30297479a 100644
--- a/src/node/hooks/express/padurlsanitize.js
+++ b/src/node/hooks/express/padurlsanitize.js
@@ -1,7 +1,6 @@
'use strict';
const padManager = require('../../db/PadManager');
-const url = require('url');
exports.expressCreateServer = (hookName, args, cb) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html
@@ -19,10 +18,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
next();
} else {
// the pad id was sanitized, so we redirect to the sanitized version
- let realURL = sanitizedPadId;
- realURL = encodeURIComponent(realURL);
- const query = url.parse(req.url).query;
- if (query) realURL += `?${query}`;
+ const realURL = encodeURIComponent(sanitizedPadId) + new URL(req.url).search;
res.header('Location', realURL);
res.status(302).send(`You should be redirected to ${realURL}`);
}
diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js
index 825c495ca..c533ae8ca 100644
--- a/src/node/hooks/express/tests.js
+++ b/src/node/hooks/express/tests.js
@@ -1,7 +1,6 @@
'use strict';
const path = require('path');
-const npm = require('npm');
const fs = require('fs');
const util = require('util');
const settings = require('../../utils/Settings');
@@ -29,8 +28,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
res.end(`var specs_list = ${JSON.stringify(files)};\n`);
});
- // path.join seems to normalize by default, but we'll just be explicit
- const rootTestFolder = path.normalize(path.join(npm.root, '../tests/frontend/'));
+ const rootTestFolder = path.join(settings.root, 'src/tests/frontend/');
const url2FilePath = (url) => {
let subPath = url.substr('/tests/frontend'.length);
@@ -39,7 +37,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
}
subPath = subPath.split('?')[0];
- let filePath = path.normalize(path.join(rootTestFolder, subPath));
+ let filePath = path.join(rootTestFolder, subPath);
// make sure we jail the paths to the test folder, otherwise serve index
if (filePath.indexOf(rootTestFolder) !== 0) {
diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.js
index 31848b484..1cd663c4d 100644
--- a/src/node/hooks/i18n.js
+++ b/src/node/hooks/i18n.js
@@ -4,8 +4,7 @@ const languages = require('languages4translatewiki');
const fs = require('fs');
const path = require('path');
const _ = require('underscore');
-const npm = require('npm');
-const plugins = require('../../static/js/pluginfw/plugin_defs.js').plugins;
+const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
const existsSync = require('../utils/path_exists');
const settings = require('../utils/Settings');
@@ -38,10 +37,12 @@ const getAllLocales = () => {
};
// add core supported languages first
- extractLangs(`${npm.root}/ep_etherpad-lite/locales`);
+ extractLangs(path.join(settings.root, 'src/locales'));
// add plugins languages (if any)
- for (const pluginName in plugins) extractLangs(path.join(npm.root, pluginName, 'locales'));
+ for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) {
+ extractLangs(path.join(pluginPath, 'locales'));
+ }
// Build a locale index (merge all locale data other than user-supplied overrides)
const locales = {};
@@ -83,18 +84,18 @@ const getAllLocales = () => {
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
const getAvailableLangs = (locales) => {
const result = {};
- _.each(_.keys(locales), (langcode) => {
+ for (const langcode of Object.keys(locales)) {
result[langcode] = languages.getLanguageInfo(langcode);
- });
+ }
return result;
};
// returns locale index that will be served in /locales.json
const generateLocaleIndex = (locales) => {
const result = _.clone(locales); // keep English strings
- _.each(_.keys(locales), (langcode) => {
+ for (const langcode of Object.keys(locales)) {
if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`;
- });
+ }
return JSON.stringify(result);
};
@@ -108,7 +109,7 @@ exports.expressCreateServer = (n, args, cb) => {
args.app.get('/locales/:locale', (req, res) => {
// works with /locale/en and /locale/en.json requests
const locale = req.params.locale.split('.')[0];
- if (exports.availableLangs.hasOwnProperty(locale)) {
+ if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);
diff --git a/src/node/padaccess.js b/src/node/padaccess.js
index 241302852..5ca5641b5 100644
--- a/src/node/padaccess.js
+++ b/src/node/padaccess.js
@@ -2,7 +2,7 @@
const securityManager = require('./db/SecurityManager');
// checks for padAccess
-module.exports = async function (req, res) {
+module.exports = async (req, res) => {
try {
const {session: {user} = {}} = req;
const accessObj = await securityManager.checkAccess(
diff --git a/src/node/server.js b/src/node/server.js
index 0f0b0147f..aec0d442f 100755
--- a/src/node/server.js
+++ b/src/node/server.js
@@ -43,11 +43,9 @@ const UpdateCheck = require('./utils/UpdateCheck');
const db = require('./db/DB');
const express = require('./hooks/express');
const hooks = require('../static/js/pluginfw/hooks');
-const npm = require('npm/lib/npm.js');
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins');
const settings = require('./utils/Settings');
-const util = require('util');
const logger = log4js.getLogger('server');
@@ -65,9 +63,9 @@ const State = {
let state = State.INITIAL;
class Gate extends Promise {
- constructor() {
+ constructor(executor = null) {
let res;
- super((resolve) => { res = resolve; });
+ super((resolve, reject) => { res = resolve; if (executor != null) executor(resolve, reject); });
this.resolve = res;
}
}
@@ -111,10 +109,16 @@ exports.start = async () => {
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
- process.on('uncaughtException', (err) => exports.exit(err));
+ process.on('uncaughtException', (err) => {
+ logger.debug(`uncaught exception: ${err.stack || err}`);
+ exports.exit(err);
+ });
// 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; });
+ process.on('unhandledRejection', (err) => {
+ logger.debug(`unhandled rejection: ${err.stack || err}`);
+ throw err;
+ });
for (const signal of ['SIGINT', 'SIGTERM']) {
// Forcibly remove other signal listeners to prevent them from terminating node before we are
@@ -132,7 +136,6 @@ exports.start = async () => {
});
}
- await util.promisify(npm.load)();
await db.init();
await plugins.update();
const installedPlugins = Object.values(pluginDefs.plugins)
@@ -219,6 +222,7 @@ exports.exit = async (err = null) => {
process.exit(1);
}
}
+ if (!exitCalled) logger.info('Exiting...');
exitCalled = true;
switch (state) {
case State.STARTING:
@@ -241,7 +245,6 @@ exports.exit = async (err = null) => {
default:
throw new Error(`unknown State: ${state.toString()}`);
}
- logger.info('Exiting...');
exitGate = new Gate();
state = State.EXITING;
exitGate.resolve();
diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.js
index 5b364ed80..73a96bb67 100644
--- a/src/node/utils/AbsolutePaths.js
+++ b/src/node/utils/AbsolutePaths.js
@@ -43,8 +43,9 @@ let etherpadRoot = null;
*/
const popIfEndsWith = (stringArray, lastDesiredElements) => {
if (stringArray.length <= lastDesiredElements.length) {
- absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" from "${stringArray.join(path.sep)}", it should contain at least ${lastDesiredElements.length + 1} elements`);
-
+ absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" ` +
+ `from "${stringArray.join(path.sep)}", it should contain at least ` +
+ `${lastDesiredElements.length + 1} elements`);
return false;
}
@@ -54,7 +55,8 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => {
return _.initial(stringArray, lastDesiredElements.length);
}
- absPathLogger.debug(`${stringArray.join(path.sep)} does not end with "${lastDesiredElements.join(path.sep)}"`);
+ absPathLogger.debug(
+ `${stringArray.join(path.sep)} does not end with "${lastDesiredElements.join(path.sep)}"`);
return false;
};
@@ -102,7 +104,8 @@ exports.findEtherpadRoot = () => {
}
if (maybeEtherpadRoot === false) {
- absPathLogger.error(`Could not identity Etherpad base path in this ${process.platform} installation in "${foundRoot}"`);
+ absPathLogger.error('Could not identity Etherpad base path in this ' +
+ `${process.platform} installation in "${foundRoot}"`);
process.exit(1);
}
@@ -113,7 +116,8 @@ exports.findEtherpadRoot = () => {
return etherpadRoot;
}
- absPathLogger.error(`To run, Etherpad has to identify an absolute base path. This is not: "${etherpadRoot}"`);
+ absPathLogger.error(
+ `To run, Etherpad has to identify an absolute base path. This is not: "${etherpadRoot}"`);
process.exit(1);
};
@@ -132,7 +136,7 @@ exports.makeAbsolute = (somePath) => {
return somePath;
}
- const rewrittenPath = path.normalize(path.join(exports.findEtherpadRoot(), somePath));
+ const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath);
absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
return rewrittenPath;
diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js
index 999b22639..38e5fb1a6 100644
--- a/src/node/utils/ExportHtml.js
+++ b/src/node/utils/ExportHtml.js
@@ -75,7 +75,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
if (authorColors) {
css += '