Merge branch 'develop'

This commit is contained in:
John McLear 2021-02-25 18:26:20 +00:00
commit 55f76c565c
No known key found for this signature in database
GPG key ID: 599378BB471BCE1C
119 changed files with 2280 additions and 1644 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

View file

@ -23,6 +23,12 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. 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):** **Desktop (please complete the following information):**
- OS: [e.g. iOS] - OS: [e.g. iOS]
- Browser [e.g. chrome, safari] - Browser [e.g. chrome, safari]

View file

@ -4,22 +4,27 @@ name: "Backend tests"
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
withoutplugins: withoutpluginsLinux:
# run on pushes to any branch # run on pushes to any branch
# run on PRs from external forks # run on PRs from external forks
if: | if: |
(github.event_name != 'pull_request') (github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) || (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 runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [10, 12, 14, 15]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 12 node-version: ${{ matrix.node }}
- name: Install libreoffice - name: Install libreoffice
run: | run: |
@ -33,22 +38,27 @@ jobs:
- name: Run the backend tests - name: Run the backend tests
run: cd src && npm test run: cd src && npm test
withplugins: withpluginsLinux:
# run on pushes to any branch # run on pushes to any branch
# run on PRs from external forks # run on PRs from external forks
if: | if: |
(github.event_name != 'pull_request') (github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) || (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 runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [10, 12, 14, 15]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 12 node-version: ${{ matrix.node }}
- name: Install libreoffice - name: Install libreoffice
run: | run: |
@ -57,8 +67,10 @@ jobs:
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
- name: Install Etherpad plugins - 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: > run: >
npm install npm install --no-save --legacy-peer-deps
ep_align ep_align
ep_author_hover ep_author_hover
ep_cursortrace ep_cursortrace
@ -85,3 +97,89 @@ jobs:
- name: Run the backend tests - name: Run the backend tests
run: cd src && npm test 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

View file

@ -7,29 +7,41 @@ jobs:
name: with plugins name: with plugins
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [10, 12, 14, 15]
steps: 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 - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 12 node-version: ${{ matrix.node }}
- name: Run sauce-connect-action - name: Install etherpad plugins
shell: bash # We intentionally install an old ep_align version to test upgrades to the minor version number.
env: # The --legacy-peer-deps flag is required to work around a bug in npm v7:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} # https://github.com/npm/cli/issues/2199
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} run: npm install --no-save --legacy-peer-deps ep_align@0.2.27
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: src/tests/frontend/travis/sauce_tunnel.sh
# 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: src/bin/installDeps.sh 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 - name: Install etherpad plugins
run: rm -Rf node_modules/ep_align/static/tests/* run: rm -Rf node_modules/ep_align/static/tests/*
@ -38,8 +50,8 @@ jobs:
id: environment id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN - name: Create settings.json
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json" run: cp settings.json.template settings.json
- name: Write custom settings.json that enables the Admin UI tests - 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" 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 - 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 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 - name: Run the frontend admin tests
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 }} SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }} GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: | run: |
src/tests/frontend/travis/adminrunner.sh src/tests/frontend/travis/adminrunner.sh

View file

@ -8,6 +8,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -15,14 +21,6 @@ jobs:
with: with:
node-version: 12 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 - name: Install all dependencies and symlink for ep_etherpad-lite
run: src/bin/installDeps.sh run: src/bin/installDeps.sh
@ -30,15 +28,26 @@ jobs:
id: environment id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN - name: Create settings.json
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > 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 - name: Run the frontend tests
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 }} SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }} GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: | run: |
src/tests/frontend/travis/runner.sh src/tests/frontend/travis/runner.sh
@ -48,6 +57,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -55,17 +70,11 @@ jobs:
with: with:
node-version: 12 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 - 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: > run: >
npm install npm install --no-save --legacy-peer-deps
ep_align ep_align
ep_author_hover ep_author_hover
ep_cursortrace ep_cursortrace
@ -94,22 +103,30 @@ jobs:
id: environment id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN - name: Create settings.json
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json" run: cp settings.json.template settings.json
- name: Write custom settings.json that enables the Admin UI tests - name: Disable import/export rate limiting
run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json" run: |
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i 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 src/tests/frontend/specs/* 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 - name: Run the frontend tests
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 }} SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }} GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: | run: |
src/tests/frontend/travis/runner.sh src/tests/frontend/travis/runner.sh

View file

@ -51,8 +51,10 @@ jobs:
run: sudo npm install -g etherpad-load-test run: sudo npm install -g etherpad-load-test
- name: Install etherpad plugins - 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: > run: >
npm install npm install --no-save --legacy-peer-deps
ep_align ep_align
ep_author_hover ep_author_hover
ep_cursortrace ep_cursortrace

61
.github/workflows/windows-installer.yml vendored Normal file
View file

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

75
.github/workflows/windows-zip.yml vendored Normal file
View file

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

9
.lgtm.yml Normal file
View file

@ -0,0 +1,9 @@
extraction:
javascript:
index:
exclude:
- src/static/js/vendors
python:
index:
exclude:
- /

View file

@ -28,8 +28,10 @@ _install_libreoffice: &install_libreoffice >-
sudo apt-get update && sudo apt-get update &&
sudo apt-get -y install libreoffice libreoffice-pdfimport 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 >- _install_plugins: &install_plugins >-
npm install npm install --no-save --legacy-peer-deps
ep_align ep_align
ep_author_hover ep_author_hover
ep_cursortrace ep_cursortrace

View file

@ -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 # 1.8.9
### Notable fixes ### Notable fixes

View file

@ -4,7 +4,7 @@
# #
# Author: muxator # Author: muxator
FROM node:10-buster-slim FROM node:14-buster-slim
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
# plugins to install while building the container. By default no plugins are # 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 . RUN chmod -R g=u .
EXPOSE 9001 EXPOSE 9001
CMD ["node", "--experimental-worker", "src/node/server.js"] CMD ["node", "src/node/server.js"]

View file

@ -1,14 +1,32 @@
# A real-time collaborative editor for the web # A real-time collaborative editor for the web
<a href="https://hub.docker.com/r/etherpad/etherpad"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/etherpad/etherpad"></a>
[![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") ![Demo Etherpad Animated Jif](doc/images/etherpad_demo.gif "Etherpad in action")
# About # 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. 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
<a href="https://hub.docker.com/r/etherpad/etherpad"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/etherpad/etherpad?color=%2344b492"></a> ![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 # Installation
@ -43,7 +61,7 @@ start with `src/bin/run.sh` will update the dependencies.
## Windows ## Windows
### Prebuilt Windows package ### 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) 1. [Download the latest Windows package](https://etherpad.org/#download)
2. Extract the folder 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: 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 ## Customize the style with skin variants

View file

@ -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. 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 ## API Methods
### Groups ### Groups
@ -636,4 +629,3 @@ get stats of the etherpad instance
*Example returns* *Example returns*
* `{"code":0,"message":"ok","data":{"totalPads":3,"totalSessions": 2,"totalActivePads": 1}}` * `{"code":0,"message":"ok","data":{"totalPads":3,"totalSessions": 2,"totalActivePads": 1}}`

View file

@ -1,62 +1,35 @@
html { body{
border-top: solid green 5pt; border-top: solid #44b492 5pt;
} line-height:150%;
font-family: 'Quicksand',sans-serif;
body.apidoc { color: #313b4a;
width: 60%; max-width:800px;
min-width: 10cm;
margin: 0 auto; margin: 0 auto;
padding: 20px;
} }
#header { a{
padding: 1pc 0; color: #555;
color: #111;
} }
a, h1{
a:active { color: #44b492;
color: #272; line-height:100%;
}
a:focus,
a:hover {
color: #050;
} }
#apicontent a.mark, a:hover{
#apicontent a.mark:active { color: #44b492;
float: right;
color: #BBB;
font-size: 0.7cm;
text-decoration: none;
}
#apicontent a.mark:focus,
#apicontent a.mark:hover {
color: #AAA;
} }
#apicontent code { pre{
padding: 1px; background-color: #e0e0e0;
background-color: #EEE; padding:20px;
border-radius: 4px;
border: 1px solid #DDD;
}
#apicontent pre>code {
display: block;
overflow: auto;
padding: 5px;
} }
table, th, td { code{
text-align: left; background-color: #e0e0e0;
border: 1px solid gray;
border-collapse: collapse;
} }
th { img {
padding: 0.5em; max-width: 100%;
background: #EEE;
}
td {
padding: 0.5em;
} }

View file

@ -7,8 +7,8 @@ execute its own functionality based on these events.
Publicly available plugins can be found in the npm registry (see Publicly available plugins can be found in the npm registry (see
<https://npmjs.org>). Etherpad's naming convention for plugins is to prefix your <https://npmjs.org>). 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 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 plugins from npm, using `npm install --no-save --legacy-peer-deps
directory. ep_flubberworm` in Etherpad's root directory.
You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will
list all installed plugins and those available on npm. It even provides list all installed plugins and those available on npm. It even provides

View file

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

View file

@ -137,6 +137,12 @@ const getSection = (lexed) => {
const buildToc = (lexed, filename, cb) => { const buildToc = (lexed, filename, cb) => {
let toc = []; let toc = [];
let depth = 0; let depth = 0;
marked.setOptions({
headerIds: true,
headerPrefix: `${filename}_`,
});
lexed.forEach((tok) => { lexed.forEach((tok) => {
if (tok.type !== 'heading') return; if (tok.type !== 'heading') return;
if (tok.depth - depth > 1) { if (tok.depth - depth > 1) {

View file

@ -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=="
}
}
}

View file

@ -7,7 +7,7 @@
"node": ">=10.17.0" "node": ">=10.17.0"
}, },
"dependencies": { "dependencies": {
"marked": "1.1.1" "marked": "^2.0.0"
}, },
"devDependencies": {}, "devDependencies": {},
"optionalDependencies": {}, "optionalDependencies": {},

View file

@ -220,14 +220,14 @@ fs.readdir(pluginPath, (err, rootFiles) => {
} }
updateDeps(parsedPackageJSON, 'devDependencies', { updateDeps(parsedPackageJSON, 'devDependencies', {
'eslint': '^7.18.0', 'eslint': '^7.20.0',
'eslint-config-etherpad': '^1.0.24', 'eslint-config-etherpad': '^1.0.25',
'eslint-plugin-eslint-comments': '^3.2.0', 'eslint-plugin-eslint-comments': '^3.2.0',
'eslint-plugin-mocha': '^8.0.0', 'eslint-plugin-mocha': '^8.0.0',
'eslint-plugin-node': '^11.1.0', 'eslint-plugin-node': '^11.1.0',
'eslint-plugin-prefer-arrow': '^1.2.3', 'eslint-plugin-prefer-arrow': '^1.2.3',
'eslint-plugin-promise': '^4.2.1', 'eslint-plugin-promise': '^4.3.1',
'eslint-plugin-you-dont-need-lodash-underscore': '^6.10.0', 'eslint-plugin-you-dont-need-lodash-underscore': '^6.11.0',
}); });
updateDeps(parsedPackageJSON, 'peerDependencies', { updateDeps(parsedPackageJSON, 'peerDependencies', {

View file

@ -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) ![screenshot](https://user-images.githubusercontent.com/220864/99979953-97841d80-2d9f-11eb-9782-5f65817c58f4.PNG)
## Installing ## Installing
npm install [plugin_name]
```
npm install --no-save --legacy-peer-deps [plugin_name]
```
or Use the Etherpad ``/admin`` interface. or Use the Etherpad ``/admin`` interface.

View file

@ -7,6 +7,7 @@
"Mastizada", "Mastizada",
"MuratTheTurkish", "MuratTheTurkish",
"Mushviq Abdulla", "Mushviq Abdulla",
"NMW03",
"Neriman2003", "Neriman2003",
"Vesely35", "Vesely35",
"Wertuose" "Wertuose"

View file

@ -2,10 +2,12 @@
"@metadata": { "@metadata": {
"authors": [ "authors": [
"Besnik b", "Besnik b",
"Eraldkerciku",
"Kosovastar", "Kosovastar",
"Liridon" "Liridon"
] ]
}, },
"admin.page-title": "Paneli i Administratorit - Etherpad",
"admin_plugins": "Përgjegjës shtojcash", "admin_plugins": "Përgjegjës shtojcash",
"admin_plugins.available": "Shtojca të gatshme", "admin_plugins.available": "Shtojca të gatshme",
"admin_plugins.available_not-found": "Su gjetën shtojca.", "admin_plugins.available_not-found": "Su gjetën shtojca.",

View file

@ -722,7 +722,7 @@ Example returns:
*/ */
exports.sendClientsMessage = async (padID, msg) => { 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); padMessageHandler.handleCustomMessage(padID, msg);
}; };

View file

@ -87,7 +87,7 @@ Pad.prototype.appendRevision = async function appendRevision(aChangeset, author)
// ex. getNumForAuthor // ex. getNumForAuthor
if (author !== '') { if (author !== '') {
this.pool.putAttrib(['author', author || '']); this.pool.putAttrib(['author', author]);
} }
if (newRev % 100 === 0) { if (newRev % 100 === 0) {

View file

@ -173,7 +173,7 @@ const padIdTransforms = [
]; ];
// returns a sanitized padId, respecting legacy pad id formats // 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) { for (let i = 0, n = padIdTransforms.length; i < n; ++i) {
const exists = await exports.doesPadExist(padId); const exists = await exports.doesPadExist(padId);

View file

@ -84,7 +84,8 @@ exports.require = (name, args, mod) => {
const cache = settings.maxAge !== 0; const cache = settings.maxAge !== 0;
const template = cache && templateCache.get(ejspath) || ejs.compile( 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}); {filename: ejspath});
if (cache) templateCache.set(ejspath, template); if (cache) templateCache.set(ejspath, template);

View file

@ -669,7 +669,8 @@ const handleUserChanges = async (socket, message) => {
if (Changeset.oldLen(changeset) !== prevText.length) { if (Changeset.oldLen(changeset) !== prevText.length) {
socket.json.send({disconnect: 'badChangeset'}); socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark(); 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 { try {
@ -887,7 +888,8 @@ const handleClientReady = async (socket, message, authorID) => {
} }
if (message.protocolVersion !== 2) { 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; return;
} }
@ -1085,12 +1087,16 @@ const handleClientReady = async (socket, message, authorID) => {
indentationOnNewLine: settings.indentationOnNewLine, indentationOnNewLine: settings.indentationOnNewLine,
scrollWhenFocusLineIsOutOfViewport: { scrollWhenFocusLineIsOutOfViewport: {
percentage: { percentage: {
editionAboveViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport, editionAboveViewport:
editionBelowViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport, settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
editionBelowViewport:
settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
}, },
duration: settings.scrollWhenFocusLineIsOutOfViewport.duration, duration: settings.scrollWhenFocusLineIsOutOfViewport.duration,
scrollWhenCaretIsInTheLastLineOfViewport: settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport, scrollWhenCaretIsInTheLastLineOfViewport:
percentageToScrollWhenUserPressesArrowUp: settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp, settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
percentageToScrollWhenUserPressesArrowUp:
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
}, },
initialChangesets: [], // FIXME: REMOVE THIS SHIT initialChangesets: [], // FIXME: REMOVE THIS SHIT
}; };
@ -1380,7 +1386,8 @@ const composePadChangesets = async (padId, startNum, endNum) => {
// get all changesets // get all changesets
const changesets = {}; const changesets = {};
await Promise.all(changesetsNeeded.map( 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 // compose Changesets
@ -1406,7 +1413,7 @@ const _getRoomSockets = (padID) => {
const room = socketio.sockets.adapter.rooms[padID]; const room = socketio.sockets.adapter.rooms[padID];
if (room) { if (room) {
for (const id in room.sockets) { for (const id of Object.keys(room.sockets)) {
roomSockets.push(socketio.sockets.sockets[id]); roomSockets.push(socketio.sockets.sockets[id]);
} }
} }

View file

@ -60,7 +60,7 @@ exports.setSocketIO = (_socket) => {
}; };
// tell all components about this connect // tell all components about this connect
for (const i in components) { for (const i of Object.keys(components)) {
components[i].handleConnect(client); components[i].handleConnect(client);
} }
@ -84,7 +84,7 @@ exports.setSocketIO = (_socket) => {
// this instance can be brought out of a scaling cluster. // this instance can be brought out of a scaling cluster.
stats.gauge('lastDisconnect', () => Date.now()); stats.gauge('lastDisconnect', () => Date.now());
// tell all components about this disconnect // tell all components about this disconnect
for (const i in components) { for (const i of Object.keys(components)) {
components[i].handleDisconnect(client); components[i].handleDisconnect(client);
} }
}); });

View file

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

View file

@ -22,7 +22,6 @@ const createHTTPError = require('http-errors');
const apiHandler = require('../../handler/APIHandler'); const apiHandler = require('../../handler/APIHandler');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const isValidJSONPName = require('./isValidJSONPName');
const log4js = require('log4js'); const log4js = require('log4js');
const logger = log4js.getLogger('API'); const logger = log4js.getLogger('API');
@ -491,7 +490,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
}; };
// build operations // build operations
for (const funcName in apiHandler.version[version]) { for (const funcName of Object.keys(apiHandler.version[version])) {
let operation = {}; let operation = {};
if (operations[funcName]) { if (operations[funcName]) {
operation = {...operations[funcName]}; operation = {...operations[funcName]};
@ -545,7 +544,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
const {app} = args; const {app} = args;
// create openapi-backend handlers for each api version under /api/{version}/* // 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 // we support two different styles of api: flat + rest
// TODO: do we really want to support both? // 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 // build openapi-backend instance for this api version
const api = new OpenAPIBackend({ const api = new OpenAPIBackend({
apiRoot, // each api version has its own root
definition, definition,
validate: false, validate: false,
// for a small optimisation, we can run the quick startup for older // for a small optimisation, we can run the quick startup for older
@ -592,7 +590,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
}); });
// register operation handlers // 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) => { const handler = async (c, req, res) => {
// parse fields from request // parse fields from request
const {header, params, query} = c.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 // send response
return res.send(response); return res.send(response);
}); });

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const padManager = require('../../db/PadManager'); const padManager = require('../../db/PadManager');
const url = require('url');
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName, args, cb) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html // redirects browser to the pad's sanitized url if needed. otherwise, renders the html
@ -19,10 +18,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
next(); next();
} else { } else {
// the pad id was sanitized, so we redirect to the sanitized version // the pad id was sanitized, so we redirect to the sanitized version
let realURL = sanitizedPadId; const realURL = encodeURIComponent(sanitizedPadId) + new URL(req.url).search;
realURL = encodeURIComponent(realURL);
const query = url.parse(req.url).query;
if (query) realURL += `?${query}`;
res.header('Location', realURL); res.header('Location', realURL);
res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`); res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
} }

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const npm = require('npm');
const fs = require('fs'); const fs = require('fs');
const util = require('util'); const util = require('util');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
@ -29,8 +28,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
res.end(`var specs_list = ${JSON.stringify(files)};\n`); 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.join(settings.root, 'src/tests/frontend/');
const rootTestFolder = path.normalize(path.join(npm.root, '../tests/frontend/'));
const url2FilePath = (url) => { const url2FilePath = (url) => {
let subPath = url.substr('/tests/frontend'.length); let subPath = url.substr('/tests/frontend'.length);
@ -39,7 +37,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
} }
subPath = subPath.split('?')[0]; 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 // make sure we jail the paths to the test folder, otherwise serve index
if (filePath.indexOf(rootTestFolder) !== 0) { if (filePath.indexOf(rootTestFolder) !== 0) {

View file

@ -4,8 +4,7 @@ const languages = require('languages4translatewiki');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const _ = require('underscore'); const _ = require('underscore');
const npm = require('npm'); const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
const plugins = require('../../static/js/pluginfw/plugin_defs.js').plugins;
const existsSync = require('../utils/path_exists'); const existsSync = require('../utils/path_exists');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
@ -38,10 +37,12 @@ const getAllLocales = () => {
}; };
// add core supported languages first // add core supported languages first
extractLangs(`${npm.root}/ep_etherpad-lite/locales`); extractLangs(path.join(settings.root, 'src/locales'));
// add plugins languages (if any) // 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) // Build a locale index (merge all locale data other than user-supplied overrides)
const locales = {}; const locales = {};
@ -83,18 +84,18 @@ const getAllLocales = () => {
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... } // e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
const getAvailableLangs = (locales) => { const getAvailableLangs = (locales) => {
const result = {}; const result = {};
_.each(_.keys(locales), (langcode) => { for (const langcode of Object.keys(locales)) {
result[langcode] = languages.getLanguageInfo(langcode); result[langcode] = languages.getLanguageInfo(langcode);
}); }
return result; return result;
}; };
// returns locale index that will be served in /locales.json // returns locale index that will be served in /locales.json
const generateLocaleIndex = (locales) => { const generateLocaleIndex = (locales) => {
const result = _.clone(locales); // keep English strings 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`; if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`;
}); }
return JSON.stringify(result); return JSON.stringify(result);
}; };
@ -108,7 +109,7 @@ exports.expressCreateServer = (n, args, cb) => {
args.app.get('/locales/:locale', (req, res) => { args.app.get('/locales/:locale', (req, res) => {
// works with /locale/en and /locale/en.json requests // works with /locale/en and /locale/en.json requests
const locale = req.params.locale.split('.')[0]; 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('Cache-Control', `public, max-age=${settings.maxAge}`);
res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`); res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`);

View file

@ -2,7 +2,7 @@
const securityManager = require('./db/SecurityManager'); const securityManager = require('./db/SecurityManager');
// checks for padAccess // checks for padAccess
module.exports = async function (req, res) { module.exports = async (req, res) => {
try { try {
const {session: {user} = {}} = req; const {session: {user} = {}} = req;
const accessObj = await securityManager.checkAccess( const accessObj = await securityManager.checkAccess(

View file

@ -43,11 +43,9 @@ const UpdateCheck = require('./utils/UpdateCheck');
const db = require('./db/DB'); const db = require('./db/DB');
const express = require('./hooks/express'); const express = require('./hooks/express');
const hooks = require('../static/js/pluginfw/hooks'); const hooks = require('../static/js/pluginfw/hooks');
const npm = require('npm/lib/npm.js');
const pluginDefs = require('../static/js/pluginfw/plugin_defs'); const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins'); const plugins = require('../static/js/pluginfw/plugins');
const settings = require('./utils/Settings'); const settings = require('./utils/Settings');
const util = require('util');
const logger = log4js.getLogger('server'); const logger = log4js.getLogger('server');
@ -65,9 +63,9 @@ const State = {
let state = State.INITIAL; let state = State.INITIAL;
class Gate extends Promise { class Gate extends Promise {
constructor() { constructor(executor = null) {
let res; let res;
super((resolve) => { res = resolve; }); super((resolve, reject) => { res = resolve; if (executor != null) executor(resolve, reject); });
this.resolve = res; this.resolve = res;
} }
} }
@ -111,10 +109,16 @@ exports.start = async () => {
stats.gauge('memoryUsage', () => process.memoryUsage().rss); stats.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); 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 // 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. // 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']) { for (const signal of ['SIGINT', 'SIGTERM']) {
// Forcibly remove other signal listeners to prevent them from terminating node before we are // 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 db.init();
await plugins.update(); await plugins.update();
const installedPlugins = Object.values(pluginDefs.plugins) const installedPlugins = Object.values(pluginDefs.plugins)
@ -219,6 +222,7 @@ exports.exit = async (err = null) => {
process.exit(1); process.exit(1);
} }
} }
if (!exitCalled) logger.info('Exiting...');
exitCalled = true; exitCalled = true;
switch (state) { switch (state) {
case State.STARTING: case State.STARTING:
@ -241,7 +245,6 @@ exports.exit = async (err = null) => {
default: default:
throw new Error(`unknown State: ${state.toString()}`); throw new Error(`unknown State: ${state.toString()}`);
} }
logger.info('Exiting...');
exitGate = new Gate(); exitGate = new Gate();
state = State.EXITING; state = State.EXITING;
exitGate.resolve(); exitGate.resolve();

View file

@ -43,8 +43,9 @@ let etherpadRoot = null;
*/ */
const popIfEndsWith = (stringArray, lastDesiredElements) => { const popIfEndsWith = (stringArray, lastDesiredElements) => {
if (stringArray.length <= lastDesiredElements.length) { 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; return false;
} }
@ -54,7 +55,8 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => {
return _.initial(stringArray, lastDesiredElements.length); 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; return false;
}; };
@ -102,7 +104,8 @@ exports.findEtherpadRoot = () => {
} }
if (maybeEtherpadRoot === false) { 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); process.exit(1);
} }
@ -113,7 +116,8 @@ exports.findEtherpadRoot = () => {
return etherpadRoot; 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); process.exit(1);
}; };
@ -132,7 +136,7 @@ exports.makeAbsolute = (somePath) => {
return 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}"`); absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
return rewrittenPath; return rewrittenPath;

View file

@ -75,7 +75,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
if (authorColors) { if (authorColors) {
css += '<style>\n'; css += '<style>\n';
for (const a in apool.numToAttrib) { for (const a of Object.keys(apool.numToAttrib)) {
const attr = apool.numToAttrib[a]; const attr = apool.numToAttrib[a];
// skip non author attributes // skip non author attributes
@ -106,7 +106,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// this pad, and if yes puts its attrib id->props value into anumMap // this pad, and if yes puts its attrib id->props value into anumMap
props.forEach((propName, i) => { props.forEach((propName, i) => {
let attrib = [propName, true]; let attrib = [propName, true];
if (_.isArray(propName)) { if (Array.isArray(propName)) {
// propName can be in the form of ['color', 'red'], // propName can be in the form of ['color', 'red'],
// see hook exportHtmlAdditionalTagsWithData // see hook exportHtmlAdditionalTagsWithData
attrib = propName; attrib = propName;
@ -135,7 +135,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// we are not insterested on properties in the form of ['color', 'red'], // we are not insterested on properties in the form of ['color', 'red'],
// see hook exportHtmlAdditionalTagsWithData // see hook exportHtmlAdditionalTagsWithData
if (_.isArray(property)) { if (Array.isArray(property)) {
return false; return false;
} }
@ -154,7 +154,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
// data attributes // data attributes
const isSpanWithData = (i) => { const isSpanWithData = (i) => {
const property = props[i]; const property = props[i];
return _.isArray(property); return Array.isArray(property);
}; };
const emitOpenTag = (i) => { const emitOpenTag = (i) => {
@ -314,7 +314,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
await hooks.aCallAll('getLineHTMLForExport', context); await hooks.aCallAll('getLineHTMLForExport', context);
// To create list parent elements // To create list parent elements
if ((!prevLine || prevLine.listLevel !== line.listLevel) || if ((!prevLine || prevLine.listLevel !== line.listLevel) ||
(prevLine && line.listTypeName !== prevLine.listTypeName)) { (line.listTypeName !== prevLine.listTypeName)) {
const exists = _.find(openLists, (item) => ( const exists = _.find(openLists, (item) => (
item.level === line.listLevel && item.type === line.listTypeName) item.level === line.listLevel && item.type === line.listTypeName)
); );
@ -414,7 +414,7 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => {
if ((!nextLine || if ((!nextLine ||
!nextLine.listLevel || !nextLine.listLevel ||
nextLine.listLevel < line.listLevel) || nextLine.listLevel < line.listLevel) ||
(nextLine && line.listTypeName !== nextLine.listTypeName)) { (line.listTypeName !== nextLine.listTypeName)) {
let nextLevel = 0; let nextLevel = 0;
if (nextLine && nextLine.listLevel) { if (nextLine && nextLine.listLevel) {
nextLevel = nextLine.listLevel; nextLevel = nextLine.listLevel;

View file

@ -202,7 +202,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
if (line.listTypeName !== 'number') { if (line.listTypeName !== 'number') {
// We're no longer in an OL so we can reset counting // We're no longer in an OL so we can reset counting
for (const key in listNumbers) { for (const key of Object.keys(listNumbers)) {
delete listNumbers[key]; delete listNumbers[key];
} }
} }

View file

@ -22,6 +22,13 @@ const hooks = require('../../static/js/pluginfw/hooks');
exports.setPadRaw = (padId, r) => { exports.setPadRaw = (padId, r) => {
const records = JSON.parse(r); const records = JSON.parse(r);
const blockElems = ['div', 'br', 'p', 'pre', 'li', 'author', 'lmkr', 'insertorder'];
// get supported block Elements from plugins, we will use this later.
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
blockElems.push(element);
});
Object.keys(records).forEach(async (key) => { Object.keys(records).forEach(async (key) => {
let value = records[key]; let value = records[key];
@ -53,6 +60,17 @@ exports.setPadRaw = (padId, r) => {
} else { } else {
// Not author data, probably pad data // Not author data, probably pad data
// we can split it to look to see if it's pad data // we can split it to look to see if it's pad data
// is this an attribute we support or not? If not, tell the admin
if (value.pool) {
for (const attrib of Object.keys(value.pool.numToAttrib)) {
const attribName = value.pool.numToAttrib[attrib][0];
if (blockElems.indexOf(attribName) === -1) {
console.warn('Plugin missing: ' +
`You might want to install a plugin to support this node name: ${attribName}`);
}
}
}
const oldPadId = key.split(':'); const oldPadId = key.split(':');
// we know it's pad data // we know it's pad data

View file

@ -21,6 +21,7 @@
* limitations under the License. * limitations under the License.
*/ */
const assert = require('assert').strict;
const settings = require('./Settings'); const settings = require('./Settings');
const fs = require('fs').promises; const fs = require('fs').promises;
const path = require('path'); const path = require('path');
@ -32,7 +33,7 @@ const log4js = require('log4js');
const logger = log4js.getLogger('Minify'); const logger = log4js.getLogger('Minify');
const ROOT_DIR = path.normalize(`${__dirname}/../../static/`); const ROOT_DIR = path.join(settings.root, 'src/static/');
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
@ -92,6 +93,36 @@ const requestURIs = (locations, method, headers, callback) => {
}); });
}; };
const sanitizePathname = (p) => {
// Replace all backslashes with forward slashes to support Windows. This MUST be done BEFORE path
// normalization, otherwise an attacker will be able to read arbitrary files anywhere on the
// filesystem. See https://nvd.nist.gov/vuln/detail/CVE-2015-3297. Node.js treats both the
// backlash and the forward slash characters as pathname component separators on Windows so this
// does not change the meaning of the pathname.
p = p.replace(/\\/g, '/');
// The Node.js documentation says that path.join() normalizes, and the documentation for
// path.normalize() says that it resolves '..' and '.' components. The word "resolve" implies that
// it examines the filesystem to resolve symbolic links, so 'a/../b' might not be the same thing
// as 'b'. Most path normalization functions from other libraries (e.g. Python's
// os.path.normpath()) clearly state that they do not examine the filesystem -- they are simple
// string manipulations. Node.js's path.normalize() probably also does a simple string
// manipulation, but if not it must be given a real pathname. Join with ROOT_DIR here just in
// case. ROOT_DIR will be removed later.
p = path.join(ROOT_DIR, p);
// Prevent attempts to read outside of ROOT_DIR via extra '..' components. ROOT_DIR is assumed to
// be normalized.
assert(ROOT_DIR.endsWith(path.sep));
if (!p.startsWith(ROOT_DIR)) throw new Error(`attempt to read outside ROOT_DIR (${ROOT_DIR})`);
// Convert back to a relative pathname.
p = p.slice(ROOT_DIR.length);
// On Windows, path.normalize replaces forward slashes with backslashes. Convert back to forward
// slashes. THIS IS DANGEROUS UNLESS BACKSLASHES ARE REPLACED WITH FORWARD SLASHES BEFORE PATH
// NORMALIZATION, otherwise on POSIXish systems '..\\' in the input pathname would not be
// normalized away before being converted to '../'.
p = p.replace(/\\/g, '/');
return p;
};
/** /**
* creates the minifed javascript for the given minified name * creates the minifed javascript for the given minified name
* @param req the Express request * @param req the Express request
@ -99,20 +130,22 @@ const requestURIs = (locations, method, headers, callback) => {
*/ */
const minify = async (req, res) => { const minify = async (req, res) => {
let filename = req.params.filename; let filename = req.params.filename;
try {
// No relative paths, especially if they may go up the file hierarchy. filename = sanitizePathname(filename);
filename = path.normalize(path.join(ROOT_DIR, filename)); } catch (err) {
filename = filename.replace(/\.\./g, ''); logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
if (filename.indexOf(ROOT_DIR) === 0) {
filename = filename.slice(ROOT_DIR.length);
filename = filename.replace(/\\/g, '/');
} else {
res.writeHead(404, {}); res.writeHead(404, {});
res.end(); res.end();
return; return;
} }
// Backward compatibility for plugins that were written when jQuery lived at
// src/static/js/jquery.js.
if (['js/jquery.js', 'plugins/ep_etherpad-lite/static/js/jquery.js'].indexOf(filename) !== -1) {
logger.warn(`request for deprecated jQuery path: ${filename}`);
filename = 'js/vendors/jquery.js';
}
/* Handle static files for plugins/libraries: /* Handle static files for plugins/libraries:
paths like "plugins/ep_myplugin/static/js/test.js" paths like "plugins/ep_myplugin/static/js/test.js"
are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js, are rewritten into ROOT_PATH_OF_MYPLUGIN/static/js/test.js,
@ -126,13 +159,19 @@ const minify = async (req, res) => {
if (plugins.plugins[library] && match[3]) { if (plugins.plugins[library] && match[3]) {
const plugin = plugins.plugins[library]; const plugin = plugins.plugins[library];
const pluginPath = plugin.package.realPath; const pluginPath = plugin.package.realPath;
filename = path.relative(ROOT_DIR, pluginPath + libraryPath); filename = path.relative(ROOT_DIR, path.join(pluginPath, libraryPath));
filename = filename.replace(/\\/g, '/'); // windows path fix // On Windows, path.relative converts forward slashes to backslashes. Convert them back
// because some of the code below assumes forward slashes. Node.js treats both the backlash
// and the forward slash characters as pathname component separators on Windows so this does
// not change the meaning of the pathname. This conversion does not introduce a directory
// traversal vulnerability because all '..\\' substrings have already been removed by
// sanitizePathname.
filename = filename.replace(/\\/g, '/');
} else if (LIBRARY_WHITELIST.indexOf(library) !== -1) { } else if (LIBRARY_WHITELIST.indexOf(library) !== -1) {
// Go straight into node_modules // Go straight into node_modules
// Avoid `require.resolve()`, since 'mustache' and 'mustache/index.js' // Avoid `require.resolve()`, since 'mustache' and 'mustache/index.js'
// would end up resolving to logically distinct resources. // would end up resolving to logically distinct resources.
filename = `../node_modules/${library}${libraryPath}`; filename = path.join('../node_modules/', library, libraryPath);
} }
} }
@ -174,7 +213,7 @@ const minify = async (req, res) => {
// find all includes in ace.js and embed them. // find all includes in ace.js and embed them.
const getAceFile = async () => { const getAceFile = async () => {
let data = await fs.readFile(`${ROOT_DIR}js/ace.js`, 'utf8'); let data = await fs.readFile(path.join(ROOT_DIR, 'js/ace.js'), 'utf8');
// Find all includes in ace.js and embed them // Find all includes in ace.js and embed them
const filenames = []; const filenames = [];
@ -187,10 +226,7 @@ const getAceFile = async () => {
filenames.push(matches[2]); filenames.push(matches[2]);
} }
} }
// Always include the require kernel.
filenames.push('../static/js/require-kernel.js');
data += ';\n';
data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n'; data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n';
// Request the contents of the included file on the server-side and write // Request the contents of the included file on the server-side and write
@ -198,7 +234,7 @@ const getAceFile = async () => {
await Promise.all(filenames.map(async (filename) => { await Promise.all(filenames.map(async (filename) => {
// Hostname "invalid.invalid" is a dummy value to allow parsing as a URI. // Hostname "invalid.invalid" is a dummy value to allow parsing as a URI.
const baseURI = 'http://invalid.invalid'; const baseURI = 'http://invalid.invalid';
let resourceURI = baseURI + path.normalize(path.join('/static/', filename)); let resourceURI = baseURI + path.join('/static/', filename);
resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?) resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?)
const [status, , body] = await requestURI(resourceURI, 'GET', {}); const [status, , body] = await requestURI(resourceURI, 'GET', {});
@ -234,7 +270,7 @@ const statFile = async (filename, dirStatLimit) => {
} else { } else {
let stats; let stats;
try { try {
stats = await fs.stat(ROOT_DIR + filename); stats = await fs.stat(path.join(ROOT_DIR, filename));
} catch (err) { } catch (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
// Stat the directory instead. // Stat the directory instead.
@ -248,12 +284,12 @@ const statFile = async (filename, dirStatLimit) => {
}; };
const lastModifiedDateOfEverything = async () => { const lastModifiedDateOfEverything = async () => {
const folders2check = [`${ROOT_DIR}js/`, `${ROOT_DIR}css/`]; const folders2check = [path.join(ROOT_DIR, 'js/'), path.join(ROOT_DIR, 'css/')];
let latestModification = null; let latestModification = null;
// go through this two folders // go through this two folders
await Promise.all(folders2check.map(async (path) => { await Promise.all(folders2check.map(async (dir) => {
// read the files in the folder // read the files in the folder
const files = await fs.readdir(path); const files = await fs.readdir(dir);
// we wanna check the directory itself for changes too // we wanna check the directory itself for changes too
files.push('.'); files.push('.');
@ -261,7 +297,7 @@ const lastModifiedDateOfEverything = async () => {
// go through all files in this folder // go through all files in this folder
await Promise.all(files.map(async (filename) => { await Promise.all(files.map(async (filename) => {
// get the stat data of this file // get the stat data of this file
const stats = await fs.stat(`${path}/${filename}`); const stats = await fs.stat(path.join(dir, filename));
// compare the modification time to the highest found // compare the modification time to the highest found
if (latestModification == null || stats.mtime > latestModification) { if (latestModification == null || stats.mtime > latestModification) {
@ -323,7 +359,7 @@ const getFileCompressed = async (filename, contentType) => {
const getFile = async (filename) => { const getFile = async (filename) => {
if (filename === 'js/ace.js') return await getAceFile(); if (filename === 'js/ace.js') return await getAceFile();
if (filename === 'js/require-kernel.js') return requireDefinition(); if (filename === 'js/require-kernel.js') return requireDefinition();
return await fs.readFile(ROOT_DIR + filename); return await fs.readFile(path.join(ROOT_DIR, filename));
}; };
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err))); exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));

View file

@ -32,11 +32,13 @@ exports.enforceMinNodeVersion = (minNodeVersion) => {
// we cannot use template literals, since we still do not know if we are // we cannot use template literals, since we still do not know if we are
// running under Node >= 4.0 // running under Node >= 4.0
if (semver.lt(currentNodeVersion, minNodeVersion)) { if (semver.lt(currentNodeVersion, minNodeVersion)) {
console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. Please upgrade at least to Node ${minNodeVersion}`); console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. ` +
`Please upgrade at least to Node ${minNodeVersion}`);
process.exit(1); process.exit(1);
} }
console.debug(`Running on Node ${currentNodeVersion} (minimum required Node version: ${minNodeVersion})`); console.debug(`Running on Node ${currentNodeVersion} ` +
`(minimum required Node version: ${minNodeVersion})`);
}; };
/** /**
@ -51,6 +53,8 @@ exports.checkDeprecationStatus = (lowestNonDeprecatedNodeVersion, epRemovalVersi
const currentNodeVersion = process.version; const currentNodeVersion = process.version;
if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) {
console.warn(`Support for Node ${currentNodeVersion} will be removed in Etherpad ${epRemovalVersion}. Please consider updating at least to Node ${lowestNonDeprecatedNodeVersion}`); console.warn(
`Support for Node ${currentNodeVersion} will be removed in Etherpad ${epRemovalVersion}. ` +
`Please consider updating at least to Node ${lowestNonDeprecatedNodeVersion}`);
} }
}; };

View file

@ -35,26 +35,14 @@ const argv = require('./Cli').argv;
const jsonminify = require('jsonminify'); const jsonminify = require('jsonminify');
const log4js = require('log4js'); const log4js = require('log4js');
const randomString = require('./randomstring'); const randomString = require('./randomstring');
const suppressDisableMsg = ' -- To suppress these warning messages change suppressErrorsInPadText to true in your settings.json\n'; const suppressDisableMsg = ' -- To suppress these warning messages change ' +
'suppressErrorsInPadText to true in your settings.json\n';
const _ = require('underscore'); const _ = require('underscore');
/* Root path of the installation */ /* Root path of the installation */
exports.root = absolutePaths.findEtherpadRoot(); exports.root = absolutePaths.findEtherpadRoot();
console.log(`All relative paths will be interpreted relative to the identified Etherpad base dir: ${exports.root}`); console.log('All relative paths will be interpreted relative to the identified ' +
`Etherpad base dir: ${exports.root}`);
/*
* At each start, Etherpad generates a random string and appends it as query
* parameter to the URLs of the static assets, in order to force their reload.
* Subsequent requests will be cached, as long as the server is not reloaded.
*
* For the rationale behind this choice, see
* https://github.com/ether/etherpad-lite/pull/3958
*
* ACHTUNG: this may prevent caching HTTP proxies to work
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead
*/
exports.randomVersionString = randomString(4);
console.log(`Random string used for versioning assets: ${exports.randomVersionString}`);
/** /**
* The app title, visible e.g. in the browser window * The app title, visible e.g. in the browser window
@ -128,7 +116,14 @@ exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
/** /**
* The default Text of a new pad * The default Text of a new pad
*/ */
exports.defaultPadText = 'Welcome to Etherpad!\n\nThis pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!\n\nEtherpad on Github: https:\/\/github.com\/ether\/etherpad-lite\n'; exports.defaultPadText = [
'Welcome to Etherpad!',
'',
'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' +
'text. This allows you to collaborate seamlessly on documents!',
'',
'Etherpad on Github: https://github.com/ether/etherpad-lite',
].join('\n');
/** /**
* The default Pad Settings for a user (Can be overridden by changing the setting * The default Pad Settings for a user (Can be overridden by changing the setting
@ -474,7 +469,7 @@ const storeSettings = (settingsObj) => {
// we know this setting, so we overwrite it // we know this setting, so we overwrite it
// or it's a settings hash, specific to a plugin // or it's a settings hash, specific to a plugin
if (exports[i] !== undefined || i.indexOf('ep_') === 0) { if (exports[i] !== undefined || i.indexOf('ep_') === 0) {
if (_.isObject(settingsObj[i]) && !_.isArray(settingsObj[i])) { if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) {
exports[i] = _.defaults(settingsObj[i], exports[i]); exports[i] = _.defaults(settingsObj[i], exports[i]);
} else { } else {
exports[i] = settingsObj[i]; exports[i] = settingsObj[i];
@ -601,7 +596,8 @@ const lookupEnvironmentVariables = (obj) => {
const defaultValue = match[3]; const defaultValue = match[3];
if ((envVarValue === undefined) && (defaultValue === undefined)) { if ((envVarValue === undefined) && (defaultValue === undefined)) {
console.warn(`Environment variable "${envVarName}" does not contain any value for configuration key "${key}", and no default was given. Returning null.`); console.warn(`Environment variable "${envVarName}" does not contain any value for `+
`configuration key "${key}", and no default was given. Returning null.`);
/* /*
* We have to return null, because if we just returned undefined, the * We have to return null, because if we just returned undefined, the
@ -611,7 +607,8 @@ const lookupEnvironmentVariables = (obj) => {
} }
if ((envVarValue === undefined) && (defaultValue !== undefined)) { if ((envVarValue === undefined) && (defaultValue !== undefined)) {
console.debug(`Environment variable "${envVarName}" not found for configuration key "${key}". Falling back to default value.`); console.debug(`Environment variable "${envVarName}" not found for ` +
`configuration key "${key}". Falling back to default value.`);
return coerceValue(defaultValue); return coerceValue(defaultValue);
} }
@ -622,7 +619,8 @@ const lookupEnvironmentVariables = (obj) => {
* For numeric and boolean strings let's convert it to proper types before * For numeric and boolean strings let's convert it to proper types before
* returning it, in order to maintain backward compatibility. * returning it, in order to maintain backward compatibility.
*/ */
console.debug(`Configuration key "${key}" will be read from environment variable "${envVarName}"`); console.debug(
`Configuration key "${key}" will be read from environment variable "${envVarName}"`);
return coerceValue(envVarValue); return coerceValue(envVarValue);
}); });
@ -676,7 +674,8 @@ const parseSettings = (settingsFilename, isSettings) => {
return replacedSettings; return replacedSettings;
} catch (e) { } catch (e) {
console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`); console.error(`There was an error processing your ${settingsType} ` +
`file from ${settingsFilename}: ${e.message}`);
process.exit(1); process.exit(1);
} }
@ -703,7 +702,8 @@ exports.reloadSettings = () => {
log4js.replaceConsole(); log4js.replaceConsole();
if (!exports.skinName) { if (!exports.skinName) {
console.warn('No "skinName" parameter found. Please check out settings.json.template and update your settings.json. Falling back to the default "colibris".'); console.warn('No "skinName" parameter found. Please check out settings.json.template and ' +
'update your settings.json. Falling back to the default "colibris".');
exports.skinName = 'colibris'; exports.skinName = 'colibris';
} }
@ -713,24 +713,27 @@ exports.reloadSettings = () => {
const countPieces = exports.skinName.split(path.sep).length; const countPieces = exports.skinName.split(path.sep).length;
if (countPieces !== 1) { if (countPieces !== 1) {
console.error(`skinName must be the name of a directory under "${skinBasePath}". This is not valid: "${exports.skinName}". Falling back to the default "colibris".`); console.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` +
`not valid: "${exports.skinName}". Falling back to the default "colibris".`);
exports.skinName = 'colibris'; exports.skinName = 'colibris';
} }
// informative variable, just for the log messages // informative variable, just for the log messages
let skinPath = path.normalize(path.join(skinBasePath, exports.skinName)); let skinPath = path.join(skinBasePath, exports.skinName);
// what if someone sets skinName == ".." or "."? We catch him! // what if someone sets skinName == ".." or "."? We catch him!
if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) {
console.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. Falling back to the default "colibris".`); console.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` +
'Falling back to the default "colibris".');
exports.skinName = 'colibris'; exports.skinName = 'colibris';
skinPath = path.join(skinBasePath, exports.skinName); skinPath = path.join(skinBasePath, exports.skinName);
} }
if (fs.existsSync(skinPath) === false) { if (fs.existsSync(skinPath) === false) {
console.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); console.error(
`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`);
exports.skinName = 'colibris'; exports.skinName = 'colibris';
skinPath = path.join(skinBasePath, exports.skinName); skinPath = path.join(skinBasePath, exports.skinName);
} }
@ -745,7 +748,7 @@ exports.reloadSettings = () => {
if (!exists) { if (!exists) {
const abiwordError = 'Abiword does not exist at this path, check your settings file.'; const abiwordError = 'Abiword does not exist at this path, check your settings file.';
if (!exports.suppressErrorsInPadText) { if (!exports.suppressErrorsInPadText) {
exports.defaultPadText = `${exports.defaultPadText}\nError: ${abiwordError}${suppressDisableMsg}`; exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`;
} }
console.error(`${abiwordError} File location: ${exports.abiword}`); console.error(`${abiwordError} File location: ${exports.abiword}`);
exports.abiword = null; exports.abiword = null;
@ -757,10 +760,11 @@ exports.reloadSettings = () => {
if (exports.soffice) { if (exports.soffice) {
fs.exists(exports.soffice, (exists) => { fs.exists(exports.soffice, (exists) => {
if (!exists) { if (!exists) {
const sofficeError = 'soffice (libreoffice) does not exist at this path, check your settings file.'; const sofficeError =
'soffice (libreoffice) does not exist at this path, check your settings file.';
if (!exports.suppressErrorsInPadText) { if (!exports.suppressErrorsInPadText) {
exports.defaultPadText = `${exports.defaultPadText}\nError: ${sofficeError}${suppressDisableMsg}`; exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`;
} }
console.error(`${sofficeError} File location: ${exports.soffice}`); console.error(`${sofficeError} File location: ${exports.soffice}`);
exports.soffice = null; exports.soffice = null;
@ -774,18 +778,22 @@ exports.reloadSettings = () => {
exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
console.info(`Session key loaded from: ${sessionkeyFilename}`); console.info(`Session key loaded from: ${sessionkeyFilename}`);
} catch (e) { } catch (e) {
console.info(`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); console.info(
`Session key file "${sessionkeyFilename}" not found. Creating with random contents.`);
exports.sessionKey = randomString(32); exports.sessionKey = randomString(32);
fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8');
} }
} else { } else {
console.warn('Declaring the sessionKey in the settings.json is deprecated. This value is auto-generated now. Please remove the setting from the file. -- If you are seeing this error after restarting using the Admin User Interface then you can ignore this message.'); console.warn('Declaring the sessionKey in the settings.json is deprecated. ' +
'This value is auto-generated now. Please remove the setting from the file. -- ' +
'If you are seeing this error after restarting using the Admin User ' +
'Interface then you can ignore this message.');
} }
if (exports.dbType === 'dirty') { if (exports.dbType === 'dirty') {
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
if (!exports.suppressErrorsInPadText) { if (!exports.suppressErrorsInPadText) {
exports.defaultPadText = `${exports.defaultPadText}\nWarning: ${dirtyWarning}${suppressDisableMsg}`; exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
} }
exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename);
@ -794,8 +802,23 @@ exports.reloadSettings = () => {
if (exports.ip === '') { if (exports.ip === '') {
// using Unix socket for connectivity // using Unix socket for connectivity
console.warn('The settings file contains an empty string ("") for the "ip" parameter. The "port" parameter will be interpreted as the path to a Unix socket to bind at.'); console.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
'"port" parameter will be interpreted as the path to a Unix socket to bind at.');
} }
/*
* At each start, Etherpad generates a random string and appends it as query
* parameter to the URLs of the static assets, in order to force their reload.
* Subsequent requests will be cached, as long as the server is not reloaded.
*
* For the rationale behind this choice, see
* https://github.com/ether/etherpad-lite/pull/3958
*
* ACHTUNG: this may prevent caching HTTP proxies to work
* TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead
*/
exports.randomVersionString = randomString(4);
console.log(`Random string used for versioning assets: ${exports.randomVersionString}`);
}; };
// initially load settings // initially load settings

View file

@ -44,7 +44,7 @@ try {
_crypto = undefined; _crypto = undefined;
} }
let CACHE_DIR = path.normalize(path.join(settings.root, 'var/')); let CACHE_DIR = path.join(settings.root, 'var/');
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
const responseCache = {}; const responseCache = {};

View file

@ -28,65 +28,130 @@ const logLines = (readable, logLineFn) => {
}; };
/** /**
* Similar to `util.promisify(child_rocess.exec)`, except: * Runs a command, logging its output to Etherpad's logs by default.
* - `cwd` defaults to the Etherpad root directory. *
* - PATH is prefixed with src/node_modules/.bin so that utilities from installed dependencies * Examples:
* (e.g., npm) are preferred over system utilities. *
* - Output is passed to logger callback functions by default. See below for details. * Just run a command, logging stdout and stder to Etherpad's logs:
* await runCmd(['ls', '-l']);
*
* Capture just stdout as a string:
* const stdout = await runCmd(['ls', '-l'], {stdio: [null, 'string']});
*
* Capture both stdout and stderr as strings:
* const p = runCmd(['ls', '-l'], {stdio: 'string'});
* const stdout = await p; // Or: await p.stdout;
* const stderr = await p.stderr;
*
* Call a callback with each line of stdout:
* await runCmd(['ls', '-l'], {stdio: [null, (line) => console.log(line)]});
* *
* @param args Array of command-line arguments, where `args[0]` is the command to run. * @param args Array of command-line arguments, where `args[0]` is the command to run.
* @param opts Optional options that will be passed to `child_process.spawn()` with two extensions: * @param opts As with `child_process.spawn()`, except:
* - `stdoutLogger`: Callback that is called each time a line of text is written to stdout (utf8 * - `cwd` defaults to the Etherpad root directory.
* is assumed). The line (without trailing newline) is passed as the only argument. If null, * - `env.PATH` is prefixed with `src/node_modules/.bin:node_modules/.bin` so that utilities from
* stdout is not logged. If unset, defaults to no-op. Ignored if stdout is not a pipe. * installed dependencies (e.g., npm) are preferred over system utilities.
* - `stderrLogger`: Like `stdoutLogger` but for stderr. * - By default stdout and stderr are logged to the Etherpad log at log levels INFO and ERROR.
* To pipe without logging you must explicitly use 'pipe' for opts.stdio.
* - opts.stdio[1] and opts.stdio[2] can be functions that will be called each time a line (utf8)
* is written to stdout or stderr. The line (without its trailing newline, if present) will be
* passed as the only argument, and the return value is ignored. opts.stdio = fn is equivalent
* to opts.stdio = [null, fn, fn].
* - opts.stdio[1] and opts.stdio[2] can be 'string', which will cause output to be collected,
* decoded as utf8, and returned (see below). opts.stdio = 'string' is equivalent to
* opts.stdio = [null, 'string', 'string'].
* *
* @returns A Promise with `stdout`, `stderr`, and `child` properties containing the stdout stream, * @returns A Promise that resolves when the command exits. The Promise resolves to the complete
* stderr stream, and ChildProcess objects, respectively. * stdout if opts.stdio[1] is 'string', otherwise it resolves to undefined. The returned Promise is
* augmented with these additional properties:
* - `stdout`: If opts.stdio[1] is 'pipe', the stdout stream object. If opts.stdio[1] is 'string',
* a Promise that will resolve to the complete stdout (utf8 decoded) when the command exits.
* - `stderr`: Similar to `stdout` but for stderr.
* - `child`: The ChildProcess object.
*/ */
module.exports = exports = (args, opts = {}) => { module.exports = exports = (args, opts = {}) => {
logger.debug(`Executing command: ${args.join(' ')}`); logger.debug(`Executing command: ${args.join(' ')}`);
const {stdoutLogger = () => {}, stderrLogger = () => {}} = opts; opts = {cwd: settings.root, ...opts};
// Avoid confusing child_process.spawn() with our extensions. logger.debug(`cwd: ${opts.cwd}`);
opts = {...opts}; // Make a copy to avoid mutating the caller's copy.
delete opts.stdoutLogger;
delete opts.stderrLogger;
// Log stdout and stderr by default.
const stdio =
Array.isArray(opts.stdio) ? opts.stdio.slice() // Copy to avoid mutating the caller's array.
: typeof opts.stdio === 'function' ? [null, opts.stdio, opts.stdio]
: opts.stdio === 'string' ? [null, 'string', 'string']
: Array(3).fill(opts.stdio);
const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`);
if (stdio[1] == null) stdio[1] = (line) => cmdLogger.info(line);
if (stdio[2] == null) stdio[2] = (line) => cmdLogger.error(line);
const stdioLoggers = [];
const stdioSaveString = [];
for (const fd of [1, 2]) {
if (typeof stdio[fd] === 'function') {
stdioLoggers[fd] = stdio[fd];
stdio[fd] = 'pipe';
} else if (stdio[fd] === 'string') {
stdioSaveString[fd] = true;
stdio[fd] = 'pipe';
}
}
opts.stdio = stdio;
// On Windows the PATH environment var might be spelled "Path".
const pathVarName =
Object.keys(process.env).filter((k) => k.toUpperCase() === 'PATH')[0] || 'PATH';
// Set PATH so that utilities from installed dependencies (e.g., npm) are preferred over system // Set PATH so that utilities from installed dependencies (e.g., npm) are preferred over system
// (global) utilities. // (global) utilities.
let {env = process.env} = opts; const {env = process.env} = opts;
env = {...env}; // Copy to avoid modifying process.env. const {[pathVarName]: PATH} = env;
// On Windows the PATH environment var might be spelled "Path". opts.env = {
const pathVarName = Object.keys(env).filter((k) => k.toUpperCase() === 'PATH')[0] || 'PATH'; ...env, // Copy env to avoid modifying process.env or the caller's supplied env.
env[pathVarName] = [ [pathVarName]: [
path.join(settings.root, 'src', 'node_modules', '.bin'), path.join(settings.root, 'src', 'node_modules', '.bin'),
path.join(settings.root, 'node_modules', '.bin'), path.join(settings.root, 'node_modules', '.bin'),
...(env[pathVarName] ? env[pathVarName].split(path.delimiter) : []), ...(PATH ? PATH.split(path.delimiter) : []),
].join(path.delimiter); ].join(path.delimiter),
logger.debug(`${pathVarName}=${env[pathVarName]}`); };
logger.debug(`${pathVarName}=${opts.env[pathVarName]}`);
// Create an error object to use in case the process fails. This is done here rather than in the // Create an error object to use in case the process fails. This is done here rather than in the
// process's `exit` handler so that we get a useful stack trace. // process's `exit` handler so that we get a useful stack trace.
const procFailedErr = new Error(`Command exited non-zero: ${args.join(' ')}`); const procFailedErr = new Error();
const proc = spawn(args[0], args.slice(1), {cwd: settings.root, ...opts, env}); const proc = spawn(args[0], args.slice(1), opts);
if (proc.stdout != null && stdoutLogger != null) logLines(proc.stdout, stdoutLogger); const streams = [undefined, proc.stdout, proc.stderr];
if (proc.stderr != null && stderrLogger != null) logLines(proc.stderr, stderrLogger);
const p = new Promise((resolve, reject) => { let px;
proc.on('exit', (code, signal) => { const p = new Promise((resolve, reject) => { px = {resolve, reject}; });
[, p.stdout, p.stderr] = streams;
p.child = proc;
const stdioStringPromises = [undefined, Promise.resolve(), Promise.resolve()];
for (const fd of [1, 2]) {
if (streams[fd] == null) continue;
if (stdioLoggers[fd] != null) {
logLines(streams[fd], stdioLoggers[fd]);
} else if (stdioSaveString[fd]) {
p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => {
const chunks = [];
for await (const chunk of streams[fd]) chunks.push(chunk);
return Buffer.concat(chunks).toString().replace(/\n+$/g, '');
})();
}
}
proc.on('exit', async (code, signal) => {
const [, stdout] = await Promise.all(stdioStringPromises);
if (code !== 0) { if (code !== 0) {
logger.debug(procFailedErr.stack); procFailedErr.message =
`Command exited ${code ? `with code ${code}` : `on signal ${signal}`}: ${args.join(' ')}`;
procFailedErr.code = code; procFailedErr.code = code;
procFailedErr.signal = signal; procFailedErr.signal = signal;
return reject(procFailedErr); logger.debug(procFailedErr.stack);
return px.reject(procFailedErr);
} }
logger.debug(`Command returned successfully: ${args.join(' ')}`); logger.debug(`Command returned successfully: ${args.join(' ')}`);
resolve(); px.resolve(stdout);
}); });
});
p.stdout = proc.stdout;
p.stderr = proc.stderr;
p.child = proc;
return p; return p;
}; };

View file

@ -1,49 +0,0 @@
'use strict';
const log4js = require('log4js');
const runCmd = require('./run_cmd');
const logger = log4js.getLogger('runNpm');
const npmLogger = log4js.getLogger('npm');
const stdoutLogger = (line) => npmLogger.info(line);
const stderrLogger = (line) => npmLogger.error(line);
/**
* Wrapper around `runCmd()` that logs output to an npm logger by default.
*
* @param args Command-line arguments to pass to npm.
* @param opts See the documentation for `runCmd()`. The `stdoutLogger` and `stderrLogger` options
* default to a log4js logger.
*
* @returns A Promise with additional `stdout`, `stderr`, and `child` properties. See the
* documentation for `runCmd()`.
*/
module.exports = exports = (args, opts = {}) => {
const cmd = ['npm', ...args];
logger.info(`Executing command: ${cmd.join(' ')}`);
const p = runCmd(cmd, {stdoutLogger, stderrLogger, ...opts});
p.then(
() => logger.info(`Successfully ran command: ${cmd.join(' ')}`),
() => logger.error(`npm command failed: ${cmd.join(' ')}`));
// MUST return the original Promise returned from runCmd so that the caller can access stdout.
return p;
};
// Log the version of npm at startup.
let loggedVersion = false;
(async () => {
if (loggedVersion) return;
loggedVersion = true;
const p = runCmd(['npm', '--version'], {stdoutLogger: null, stderrLogger});
const chunks = [];
await Promise.all([
(async () => { for await (const chunk of p.stdout) chunks.push(chunk); })(),
p, // Await in parallel to avoid unhandled rejection if np rejects during chunk read.
]);
const version = Buffer.concat(chunks).toString().replace(/\n+$/g, '');
logger.info(`npm --version: ${version}`);
})().catch((err) => {
logger.error(`Failed to get npm version: ${err.stack}`);
// This isn't a fatal error so don't re-throw.
});

View file

@ -2,7 +2,7 @@
"pad.js": [ "pad.js": [
"pad.js" "pad.js"
, "pad_utils.js" , "pad_utils.js"
, "browser.js" , "vendors/browser.js"
, "pad_cookie.js" , "pad_cookie.js"
, "pad_editor.js" , "pad_editor.js"
, "pad_editbar.js" , "pad_editbar.js"
@ -16,10 +16,10 @@
, "pad_savedrevs.js" , "pad_savedrevs.js"
, "pad_connectionstatus.js" , "pad_connectionstatus.js"
, "chat.js" , "chat.js"
, "gritter.js" , "vendors/gritter.js"
, "$js-cookie/src/js.cookie.js" , "$js-cookie/src/js.cookie.js"
, "$tinycon/tinycon.js" , "$tinycon/tinycon.js"
, "farbtastic.js" , "vendors/farbtastic.js"
, "skin_variants.js" , "skin_variants.js"
, "socketio.js" , "socketio.js"
] ]
@ -28,7 +28,7 @@
, "colorutils.js" , "colorutils.js"
, "draggable.js" , "draggable.js"
, "pad_utils.js" , "pad_utils.js"
, "browser.js" , "vendors/browser.js"
, "pad_cookie.js" , "pad_cookie.js"
, "pad_editor.js" , "pad_editor.js"
, "pad_editbar.js" , "pad_editbar.js"
@ -49,7 +49,7 @@
] ]
, "ace2_inner.js": [ , "ace2_inner.js": [
"ace2_inner.js" "ace2_inner.js"
, "browser.js" , "vendors/browser.js"
, "AttributePool.js" , "AttributePool.js"
, "Changeset.js" , "Changeset.js"
, "ChangesetUtils.js" , "ChangesetUtils.js"
@ -68,11 +68,11 @@
] ]
, "ace2_common.js": [ , "ace2_common.js": [
"ace2_common.js" "ace2_common.js"
, "browser.js" , "vendors/browser.js"
, "jquery.js" , "vendors/jquery.js"
, "rjquery.js" , "rjquery.js"
, "$async.js" , "$async.js"
, "underscore.js" , "vendors/underscore.js"
, "$underscore.js" , "$underscore.js"
, "$underscore/underscore.js" , "$underscore/underscore.js"
, "security.js" , "security.js"
@ -82,4 +82,5 @@
, "pluginfw/shared.js" , "pluginfw/shared.js"
, "pluginfw/hooks.js" , "pluginfw/hooks.js"
] ]
, "jquery.js": ["jquery.js"]
} }

286
src/package-lock.json generated
View file

@ -1,41 +1,48 @@
{ {
"name": "ep_etherpad-lite", "name": "ep_etherpad-lite",
"version": "1.8.9", "version": "1.8.10",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": { "@apidevtools/json-schema-ref-parser": {
"version": "8.0.0", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz",
"integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==", "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==",
"requires": { "requires": {
"@jsdevtools/ono": "^7.1.0", "@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1", "call-me-maybe": "^1.0.1",
"js-yaml": "^3.13.1" "js-yaml": "^3.13.1"
} }
}, },
"@apidevtools/openapi-schemas": { "@azure/abort-controller": {
"version": "2.0.4", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.3.tgz",
"integrity": "sha512-ob5c4UiaMYkb24pNhvfSABShAwpREvUGCkqjiz/BX9gKZ32y/S22M+ALIHftTAuv9KsFVSpVdIDzi9ZzFh5TCA==" "integrity": "sha512-kCibMwqffnwlw3c+e879rCE1Am1I2BfhjOeO54XNA8i/cEuzktnBQbTrzh67XwibHO05YuNgZzSWy9ocVfFAGw==",
},
"@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
},
"@apidevtools/swagger-parser": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-9.0.1.tgz",
"integrity": "sha512-Irqybg4dQrcHhZcxJc/UM4vO7Ksoj1Id5e+K94XUOzllqX1n47HEA50EKiXTCQbykxuJ4cYGIivjx/MRSTC5OA==",
"requires": { "requires": {
"@apidevtools/json-schema-ref-parser": "^8.0.0", "tslib": "^2.0.0"
"@apidevtools/openapi-schemas": "^2.0.2", },
"@apidevtools/swagger-methods": "^3.0.0", "dependencies": {
"@jsdevtools/ono": "^7.1.0", "tslib": {
"call-me-maybe": "^1.0.1", "version": "2.1.0",
"openapi-types": "^1.3.5", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"z-schema": "^4.2.2" "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
}
}
},
"@azure/core-auth": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.2.0.tgz",
"integrity": "sha512-KUl+Nwn/Sm6Lw5d3U90m1jZfNSL087SPcqHLxwn2T6PupNKmcgsEbDjHB25gDvHO4h7pBsTlrdJAY7dz+Qk8GA==",
"requires": {
"@azure/abort-controller": "^1.0.0",
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
"integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="
}
} }
}, },
"@azure/ms-rest-azure-env": { "@azure/ms-rest-azure-env": {
@ -44,10 +51,11 @@
"integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw=="
}, },
"@azure/ms-rest-js": { "@azure/ms-rest-js": {
"version": "2.1.0", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.2.3.tgz",
"integrity": "sha512-4BXLVImYRt+jcUmEJ5LUWglI8RBNVQndY6IcyvQ4U8O4kIXdmlRz3cJdA/RpXf5rKT38KOoTO2T6Z1f6Z1HDBg==", "integrity": "sha512-sXOhOu/37Tr8428f32Jwuwga975Xw64pYg1UeUwOBMhkNgtn5vUuNRa3fhmem+I6f8EKoi6hOsYDFlaHeZ52jA==",
"requires": { "requires": {
"@azure/core-auth": "^1.1.4",
"@types/node-fetch": "^2.3.7", "@types/node-fetch": "^2.3.7",
"@types/tunnel": "0.0.1", "@types/tunnel": "0.0.1",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
@ -83,9 +91,9 @@
} }
}, },
"@azure/ms-rest-nodeauth": { "@azure/ms-rest-nodeauth": {
"version": "3.0.6", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.6.tgz", "resolved": "https://registry.npmjs.org/@azure/ms-rest-nodeauth/-/ms-rest-nodeauth-3.0.7.tgz",
"integrity": "sha512-2twuzsXHdKMzEFI2+Sr82o6yS4ppNGZceYwil8PFo+rJxOZIoBm9e0//YC+dKilV/3F+6K/HuW8LdskDrJEQWA==", "integrity": "sha512-7Q1MyMB+eqUQy8JO+virSIzAjqR2UbKXE/YQZe+53gC8yakm8WOQ5OzGfPP+eyHqeRs6bQESyw2IC5feLWlT2A==",
"requires": { "requires": {
"@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-azure-env": "^2.0.0",
"@azure/ms-rest-js": "^2.0.4", "@azure/ms-rest-js": "^2.0.4",
@ -108,12 +116,12 @@
"dev": true "dev": true
}, },
"@babel/highlight": { "@babel/highlight": {
"version": "7.10.4", "version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz",
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.10.4", "@babel/helper-validator-identifier": "^7.12.11",
"chalk": "^2.0.0", "chalk": "^2.0.0",
"js-tokens": "^4.0.0" "js-tokens": "^4.0.0"
}, },
@ -263,6 +271,11 @@
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true "dev": true
}, },
"@tediousjs/connection-string": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.3.0.tgz",
"integrity": "sha512-d/keJiNKfpHo+GmSB8QcsAwBx8h+V1UbdozA5TD+eSLXprNY53JAYub47J9evsSKWDdNG5uVj0FiMozLKuzowQ=="
},
"@types/caseless": { "@types/caseless": {
"version": "0.12.2", "version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
@ -274,9 +287,9 @@
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
}, },
"@types/node": { "@types/node": {
"version": "14.14.22", "version": "14.14.31",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
"integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==" "integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g=="
}, },
"@types/node-fetch": { "@types/node-fetch": {
"version": "2.5.8", "version": "2.5.8",
@ -288,9 +301,9 @@
}, },
"dependencies": { "dependencies": {
"form-data": { "form-data": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"requires": { "requires": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@ -1654,12 +1667,12 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
}, },
"eslint": { "eslint": {
"version": "7.18.0", "version": "7.20.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.20.0.tgz",
"integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==", "integrity": "sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.0.0", "@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.3.0", "@eslint/eslintrc": "^0.3.0",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"chalk": "^4.0.0", "chalk": "^4.0.0",
@ -1671,7 +1684,7 @@
"eslint-utils": "^2.1.0", "eslint-utils": "^2.1.0",
"eslint-visitor-keys": "^2.0.0", "eslint-visitor-keys": "^2.0.0",
"espree": "^7.3.1", "espree": "^7.3.1",
"esquery": "^1.2.0", "esquery": "^1.4.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"file-entry-cache": "^6.0.0", "file-entry-cache": "^6.0.0",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
@ -1795,9 +1808,9 @@
} }
}, },
"eslint-config-etherpad": { "eslint-config-etherpad": {
"version": "1.0.24", "version": "1.0.25",
"resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.24.tgz", "resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.25.tgz",
"integrity": "sha512-zM92/lricP0ALURQWhSFKk8gwDUVkgNiup/Gv5lly6bHj9OIMpK86SlLv/NPkQZYM609pyQjKIeiObsiCSdQsw==", "integrity": "sha512-KYTGf08dlwvsg05Y2hm0zurCwVMyZrsxGRnPEhL2wclk26xhnPYfNfruQQqk7nghfWFLrAL+VscnkZLCQEPBXQ==",
"dev": true "dev": true
}, },
"eslint-plugin-es": { "eslint-plugin-es": {
@ -1883,15 +1896,15 @@
"dev": true "dev": true
}, },
"eslint-plugin-promise": { "eslint-plugin-promise": {
"version": "4.2.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz",
"integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", "integrity": "sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==",
"dev": true "dev": true
}, },
"eslint-plugin-you-dont-need-lodash-underscore": { "eslint-plugin-you-dont-need-lodash-underscore": {
"version": "6.10.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.10.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.11.0.tgz",
"integrity": "sha512-Zu1KbHiWKf+alVvT+kFX2M5HW1gmtnkfF1l2cjmFozMnG0gbGgXo8oqK7lwk+ygeOXDmVfOyijqBd7SUub9AEQ==", "integrity": "sha512-cIprUmcACzxBg5rUrrCMbyAI3O0jYsB80+9PGq8XsvRTrxDSIzLitNhBetu9erb3TDxyW6OPseyOZzfQdR8oow==",
"dev": true, "dev": true,
"requires": { "requires": {
"kebab-case": "^1.0.0" "kebab-case": "^1.0.0"
@ -1960,9 +1973,9 @@
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
}, },
"esquery": { "esquery": {
"version": "1.3.1", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
"dev": true, "dev": true,
"requires": { "requires": {
"estraverse": "^5.1.0" "estraverse": "^5.1.0"
@ -2138,9 +2151,9 @@
"dev": true "dev": true
}, },
"file-entry-cache": { "file-entry-cache": {
"version": "6.0.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
"dev": true, "dev": true,
"requires": { "requires": {
"flat-cache": "^3.0.4" "flat-cache": "^3.0.4"
@ -2921,12 +2934,14 @@
"jsonschema": { "jsonschema": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz",
"integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==",
"dev": true
}, },
"jsonschema-draft4": { "jsonschema-draft4": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz",
"integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=" "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=",
"dev": true
}, },
"jsprim": { "jsprim": {
"version": "1.4.1", "version": "1.4.1",
@ -3091,12 +3106,8 @@
"lodash.get": { "lodash.get": {
"version": "4.4.2", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
}, "dev": true
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
}, },
"lodash.isplainobject": { "lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
@ -3181,9 +3192,9 @@
} }
}, },
"log4js": { "log4js": {
"version": "0.6.35", "version": "0.6.38",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.35.tgz", "resolved": "https://registry.npmjs.org/log4js/-/log4js-0.6.38.tgz",
"integrity": "sha1-OrHafLFII7dO04ZcSFk6zfEfG1k=", "integrity": "sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0=",
"requires": { "requires": {
"readable-stream": "~1.0.2", "readable-stream": "~1.0.2",
"semver": "~4.3.3" "semver": "~4.3.3"
@ -3423,12 +3434,19 @@
"requires": { "requires": {
"lodash": "^4.17.11", "lodash": "^4.17.11",
"openapi-types": "^1.3.2" "openapi-types": "^1.3.2"
},
"dependencies": {
"openapi-types": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz",
"integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg=="
}
} }
}, },
"mongodb": { "mongodb": {
"version": "3.6.3", "version": "3.6.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.4.tgz",
"integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==", "integrity": "sha512-Y+Ki9iXE9jI+n9bVtbTOOdK0B95d6wVGSucwtBkvQ+HIvVdTCfpVRp01FDC24uhC/Q2WXQ8Lpq3/zwtB5Op9Qw==",
"requires": { "requires": {
"bl": "^2.2.1", "bl": "^2.2.1",
"bson": "^1.1.4", "bson": "^1.1.4",
@ -3444,10 +3462,11 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"mssql": { "mssql": {
"version": "7.0.0-beta.2", "version": "7.0.0-beta.3",
"resolved": "https://registry.npmjs.org/mssql/-/mssql-7.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/mssql/-/mssql-7.0.0-beta.3.tgz",
"integrity": "sha512-7fOp+QzFf24ir/gGeSvyyGlQKfxZj6tx88vsk4UiQw/t/zpJ9PLjOBOoi6Ff+Tw/CZ1aJTa83MPm+CRYJ/UCQA==", "integrity": "sha512-Jn/q64Dg2UjbNTqsBwCHFdMjxs4xIVqgWQ1hmDKvBR0T8ebHfPnGTzfNl4oE/VwqP1m0As+v2CMjyqOi9WneuQ==",
"requires": { "requires": {
"@tediousjs/connection-string": "^0.3.0",
"debug": "^4", "debug": "^4",
"tarn": "^3.0.1", "tarn": "^3.0.1",
"tedious": "^9.2.3" "tedious": "^9.2.3"
@ -6952,35 +6971,54 @@
} }
}, },
"openapi-backend": { "openapi-backend": {
"version": "2.4.1", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-2.4.1.tgz", "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-3.9.0.tgz",
"integrity": "sha512-48j8QhDD9sfV6t7Zgn9JrfJtCpJ53bmoT2bzXYYig1HhG/Xn0Aa5fJhM0cQSZq9nq78/XbU7RDEa3e+IADNkmA==", "integrity": "sha512-RaEBFBFBGFcnOSqoKiX+Dg+CxS0zWBop1qw+JjPLzLs7ob637QEZcEGvKYKcwGv99mWwvcCHIuGH9l+LV5pHew==",
"requires": { "requires": {
"@apidevtools/json-schema-ref-parser": "^9.0.7",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"bath-es5": "^3.0.3", "bath-es5": "^3.0.3",
"cookie": "^0.4.0", "cookie": "^0.4.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mock-json-schema": "^1.0.5", "mock-json-schema": "^1.0.7",
"openapi-schema-validation": "^0.4.2", "openapi-schema-validator": "^7.0.1",
"openapi-types": "^1.3.4", "openapi-types": "^7.0.1",
"qs": "^6.6.0", "qs": "^6.9.3"
"swagger-parser": "^9.0.1" },
"dependencies": {
"qs": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
"integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ=="
}
} }
}, },
"openapi-schema-validation": { "openapi-schema-validation": {
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz", "resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz",
"integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==", "integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==",
"dev": true,
"requires": { "requires": {
"jsonschema": "1.2.4", "jsonschema": "1.2.4",
"jsonschema-draft4": "^1.0.0", "jsonschema-draft4": "^1.0.0",
"swagger-schema-official": "2.0.0-bab6bed" "swagger-schema-official": "2.0.0-bab6bed"
} }
}, },
"openapi-schema-validator": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-7.2.3.tgz",
"integrity": "sha512-XT8NM5e/zBBa/cydTS1IeYkCPzJp9oixvt9Y1lEx+2gsCTOooNxw9x/KEivtWMSokne7X1aR+VtsYHQtNNOSyA==",
"requires": {
"ajv": "^6.5.2",
"lodash.merge": "^4.6.1",
"openapi-types": "^7.2.3",
"swagger-schema-official": "2.0.0-bab6bed"
}
},
"openapi-types": { "openapi-types": {
"version": "1.3.5", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-7.2.3.tgz",
"integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==" "integrity": "sha512-olbaNxz12R27+mTyJ/ZAFEfUruauHH27AkeQHDHRq5AF0LdNkK1SSV7EourXQDK+4aX7dv2HtyirAGK06WMAsA=="
}, },
"optional-js": { "optional-js": {
"version": "2.3.0", "version": "2.3.0",
@ -7352,9 +7390,9 @@
} }
}, },
"redis-commands": { "redis-commands": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
}, },
"redis-errors": { "redis-errors": {
"version": "1.2.0", "version": "1.2.0",
@ -7634,19 +7672,19 @@
"optional": true "optional": true
}, },
"simple-git": { "simple-git": {
"version": "2.31.0", "version": "2.35.2",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.31.0.tgz", "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.35.2.tgz",
"integrity": "sha512-/+rmE7dYZMbRAfEmn8EUIOwlM2G7UdzpkC60KF86YAfXGnmGtsPrKsym0hKvLBdFLLW019C+aZld1+6iIVy5xA==", "integrity": "sha512-UjOKsrz92Bx7z00Wla5V6qLSf5X2XSp0sL2gzKw1Bh7iJfDPDaU7gK5avIup0yo1/sMOSUMQer2b9GcnF6nmTQ==",
"requires": { "requires": {
"@kwsites/file-exists": "^1.1.1", "@kwsites/file-exists": "^1.1.1",
"@kwsites/promise-deferred": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1",
"debug": "^4.3.1" "debug": "^4.3.2"
}, },
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "4.3.1", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": { "requires": {
"ms": "2.1.2" "ms": "2.1.2"
} }
@ -7891,9 +7929,9 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
}, },
"sqlite3": { "sqlite3": {
"version": "5.0.1", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.1.tgz", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz",
"integrity": "sha512-kh2lTIcYNfmVcvhVJihsYuPj9U0xzBbh6bmqILO2hkryWSC9RRhzYmkIDtJkJ+d8Kg4wZRJ0T1reyHUEspICfg==", "integrity": "sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==",
"optional": true, "optional": true,
"requires": { "requires": {
"node-addon-api": "^3.0.0", "node-addon-api": "^3.0.0",
@ -8076,14 +8114,6 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
}, },
"swagger-parser": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-9.0.1.tgz",
"integrity": "sha512-oxOHUaeNetO9ChhTJm2fD+48DbGbLD09ZEOwPOWEqcW8J6zmjWxutXtSuOiXsoRgDWvORYlImbwM21Pn+EiuvQ==",
"requires": {
"@apidevtools/swagger-parser": "9.0.1"
}
},
"swagger-schema-official": { "swagger-schema-official": {
"version": "2.0.0-bab6bed", "version": "2.0.0-bab6bed",
"resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz",
@ -8107,9 +8137,9 @@
}, },
"dependencies": { "dependencies": {
"ajv": { "ajv": {
"version": "7.0.3", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.1.tgz",
"integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==", "integrity": "sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
@ -8389,9 +8419,9 @@
} }
}, },
"ueberdb2": { "ueberdb2": {
"version": "1.2.5", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.2.5.tgz", "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.3.1.tgz",
"integrity": "sha512-Bts6kmVvhVDWiZjD1JAT1qYknHHK6t9L7kGIFIedGAZRNQ3lRw2XJdf9hKbFpN2HM0J3S/aJoLrZO5BLk3UiaA==", "integrity": "sha512-uhUSJfI5sNWdiXxae0kOg88scaMIKcV0CVeojwPQzgm93vQVuGyCqS1g1i3gTZel6SwmXRFtYtfmtAmiEe+HBQ==",
"requires": { "requires": {
"async": "^3.2.0", "async": "^3.2.0",
"cassandra-driver": "^4.5.1", "cassandra-driver": "^4.5.1",
@ -8449,9 +8479,9 @@
} }
}, },
"unorm": { "unorm": {
"version": "1.4.1", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz", "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
"integrity": "sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA=" "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
}, },
"unpipe": { "unpipe": {
"version": "1.0.0", "version": "1.0.0",
@ -8487,11 +8517,6 @@
"integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==",
"dev": true "dev": true
}, },
"validator": {
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz",
"integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ=="
},
"vargs": { "vargs": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/vargs/-/vargs-0.1.0.tgz", "resolved": "https://registry.npmjs.org/vargs/-/vargs-0.1.0.tgz",
@ -8847,17 +8872,6 @@
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
}, },
"z-schema": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.3.tgz",
"integrity": "sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A==",
"requires": {
"commander": "^2.7.1",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"validator": "^12.0.0"
}
},
"zip-stream": { "zip-stream": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz",

View file

@ -50,12 +50,12 @@
"jsonminify": "0.4.1", "jsonminify": "0.4.1",
"languages4translatewiki": "0.1.3", "languages4translatewiki": "0.1.3",
"lodash.clonedeep": "4.5.0", "lodash.clonedeep": "4.5.0",
"log4js": "0.6.35", "log4js": "0.6.38",
"measured-core": "1.51.1", "measured-core": "1.51.1",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"nodeify": "1.0.1", "nodeify": "1.0.1",
"npm": "6.14.11", "npm": "6.14.11",
"openapi-backend": "2.4.1", "openapi-backend": "^3.9.0",
"proxy-addr": "^2.0.6", "proxy-addr": "^2.0.6",
"rate-limiter-flexible": "^2.1.4", "rate-limiter-flexible": "^2.1.4",
"rehype": "^10.0.0", "rehype": "^10.0.0",
@ -69,23 +69,23 @@
"threads": "^1.4.0", "threads": "^1.4.0",
"tiny-worker": "^2.3.0", "tiny-worker": "^2.3.0",
"tinycon": "0.6.8", "tinycon": "0.6.8",
"ueberdb2": "^1.2.5", "ueberdb2": "^1.3.1",
"underscore": "1.12.0", "underscore": "1.12.0",
"unorm": "1.4.1", "unorm": "1.6.0",
"wtfnode": "^0.8.4" "wtfnode": "^0.8.4"
}, },
"bin": { "bin": {
"etherpad-lite": "node/server.js" "etherpad-lite": "node/server.js"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.18.0", "eslint": "^7.20.0",
"eslint-config-etherpad": "^1.0.24", "eslint-config-etherpad": "^1.0.25",
"eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-mocha": "^8.0.0", "eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.3.1",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.11.0",
"etherpad-cli-client": "0.0.9", "etherpad-cli-client": "0.0.9",
"mocha": "7.1.2", "mocha": "7.1.2",
"mocha-froth": "^0.2.10", "mocha-froth": "^0.2.10",
@ -100,11 +100,11 @@
"ignorePatterns": [ "ignorePatterns": [
"/static/js/admin/jquery.autosize.js", "/static/js/admin/jquery.autosize.js",
"/static/js/admin/minify.json.js", "/static/js/admin/minify.json.js",
"/static/js/browser.js", "/static/js/vendors/browser.js",
"/static/js/farbtastic.js", "/static/js/vendors/farbtastic.js",
"/static/js/gritter.js", "/static/js/vendors/gritter.js",
"/static/js/html10n.js", "/static/js/vendors/html10n.js",
"/static/js/jquery.js", "/static/js/vendors/jquery.js",
"/static/js/vendors/nice-select.js", "/static/js/vendors/nice-select.js",
"/tests/frontend/lib/" "/tests/frontend/lib/"
], ],
@ -237,6 +237,6 @@
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
"test-container": "mocha --timeout 5000 tests/container/specs/api" "test-container": "mocha --timeout 5000 tests/container/specs/api"
}, },
"version": "1.8.9", "version": "1.8.10",
"license": "Apache-2.0" "license": "Apache-2.0"
} }

View file

@ -70,3 +70,10 @@ input {
@media (max-width: 800px) { @media (max-width: 800px) {
.hide-for-mobile { display: none; } .hide-for-mobile { display: none; }
} }
.etherpadBrand{
width:20%;
max-width:100px;
margin-left:auto;
margin-right:auto;
}

View file

@ -130,7 +130,6 @@
.buttonicon-microphone-alt-slash:before { content: '\e83e'; } /* '' */ .buttonicon-microphone-alt-slash:before { content: '\e83e'; } /* '' */
.buttonicon-compress:before { content: '\e83f'; } /* '' */ .buttonicon-compress:before { content: '\e83f'; } /* '' */
.buttonicon-expand:before { content: '\e840'; } /* '' */ .buttonicon-expand:before { content: '\e840'; } /* '' */
.buttonicon-spin5:before { content: '\e841'; } /* '' */
.buttonicon-eye-slash:before { content: '\e843'; } /* '' */ .buttonicon-eye-slash:before { content: '\e843'; } /* '' */
.buttonicon-list-ol:before { content: '\e844'; } /* '' */ .buttonicon-list-ol:before { content: '\e844'; } /* '' */
.buttonicon-bold:before { content: '\e845'; } /* '' */ .buttonicon-bold:before { content: '\e845'; } /* '' */

65
src/static/img/brand.svg Executable file
View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="355px" height="355px" viewBox="0 0 355 355" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Group 10</title>
<defs>
<!-- top line -->
<rect id="path-4" x="41" y="110" width="142" height="25" rx="12.5">
<animate attributeName="width" from="0" to="142" dur="3s" fill="freeze"/>
</rect>
<filter x="-11.3%" y="-32.0%" width="122.5%" height="228.0%" filterUnits="objectBoundingBox" id="filter-5">
<feOffset dx="0" dy="8" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="4" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0568181818 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
<!-- middle line -->
<rect id="path-2" x="42" y="167" width="168" height="27" rx="13.5">
<animate attributeName="width" from="0" to="168" dur="5s" fill="freeze"/>
</rect>
<filter x="-9.5%" y="-29.6%" width="119.0%" height="218.5%" filterUnits="objectBoundingBox" id="filter-3">
<feOffset dx="0" dy="8" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="4" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0568181818 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
<!-- bottom line -->
<rect id="path-6" x="41" y="226" width="105" height="25" rx="12.5">
<animate attributeName="width" from="0" to="105" dur="2s" fill="freeze"/>
</rect>
<filter x="-15.2%" y="-32.0%" width="130.5%" height="228.0%" filterUnits="objectBoundingBox" id="filter-7">
<feOffset dx="0" dy="8" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="4" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0568181818 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
</filter>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-5-Copy-2" transform="translate(-415.000000, -351.000000)">
<g id="Group-10" transform="translate(415.000000, 351.000000)">
<g id="Group-9" transform="translate(0.000000, 15.000000)">
<!-- small radio wave -->
<path d="M237.612214,138.157654 C234.725783,135.28192 230.051254,135.279644 227.164823,138.157654 C224.278392,141.035663 224.278392,145.698831 227.164823,148.57684 C234.93988,156.329214 239.222735,166.601382 239.222735,177.499403 C239.222735,188.397423 234.93988,198.669591 227.164823,206.424696 C224.278392,209.30043 224.278392,213.965873 227.164823,216.841607 C228.608267,218.280384 230.497251,219 232.388518,219 C234.277503,219 236.16877,218.280384 237.612214,216.841607 C248.18012,206.304532 254,192.334147 254,177.499403 C254,162.665114 248.18012,148.694728 237.612214,138.157654 Z" id="Path-Copy-26" fill-opacity="0.200482" fill="#000000" fill-rule="nonzero" opacity="0.754065225">
<animate attributeName="opacity" from="0" to="1" dur="3s" repeatCount="indefinite"/>
</path>
<!-- large radio wave -->
<path d="M267.333026,113.158661 C264.51049,110.280446 259.939438,110.280446 257.116902,113.158661 C254.294366,116.039154 254.294366,120.709078 257.116902,123.586837 C285.703837,152.763042 285.703837,200.237641 257.116902,229.413847 C254.294366,232.292061 254.294366,236.96153 257.116902,239.839744 C258.528393,241.280219 260.375562,242 262.224964,242 C264.074365,242 265.921535,241.279763 267.333026,239.837011 C301.555658,204.912576 301.555658,148.084007 267.333026,113.158661 Z" id="Path-Copy-27" fill-opacity="0.250565" fill="#131514" fill-rule="nonzero" opacity="0.754065225">
<animate attributeName="opacity" from="0" to="1" dur="3s" repeatCount="indefinite"/>
</path>
<!-- top line -->
<g id="Rectangle-Copy-56">
<use fill="#000000" fill-opacity="0.200482" fill-rule="evenodd" xlink:href="#path-4"></use>
</g>
<!-- middle line -->
<g id="Rectangle-Copy-55">
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"></use>
<use fill="#000000" fill-opacity="0.200482" fill-rule="evenodd" xlink:href="#path-2"></use>
</g>
<!-- bottom line -->
<g id="Rectangle-Copy-57">
<use fill="black" fill-opacity="1" filter="url(#filter-7)" xlink:href="#path-6"></use>
<use fill="#000000" fill-opacity="0.200482" xlink:href="#path-6"></use>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -2,7 +2,7 @@
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils'); const ChangesetUtils = require('./ChangesetUtils');
const _ = require('./underscore'); const _ = require('./vendors/underscore');
const lineMarkerAttribute = 'lmkr'; const lineMarkerAttribute = 'lmkr';
@ -209,10 +209,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false; if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false;
if (selStart[0] !== selEnd[0]) { // -> More than one line selected if (selStart[0] !== selEnd[0]) { // -> More than one line selected
let hasAttrib = true;
// from selStart to the end of the first line // from selStart to the end of the first line
hasAttrib = hasAttrib && rangeHasAttrib( let hasAttrib = rangeHasAttrib(
selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]); selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
// for all lines in between // for all lines in between
@ -406,7 +404,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
hasAttrib = this.getAttributeOnSelection(attributeName); hasAttrib = this.getAttributeOnSelection(attributeName);
} else { } else {
const attributesOnCaretPosition = this.getAttributesOnCaret(); const attributesOnCaretPosition = this.getAttributesOnCaret();
hasAttrib = _.contains(_.flatten(attributesOnCaretPosition), attributeName); const allAttribs = [].concat(...attributesOnCaretPosition); // flatten
hasAttrib = allAttribs.includes(attributeName);
} }
return hasAttrib; return hasAttrib;
}, },

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,5 @@
'use strict';
/** /**
* This module contains several helper Functions to build Changesets * This module contains several helper Functions to build Changesets
* based on a SkipList * based on a SkipList
@ -18,7 +20,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
exports.buildRemoveRange = function (rep, builder, start, end) { exports.buildRemoveRange = (rep, builder, start, end) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]); const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]); const endLineOffset = rep.lines.offsetOfIndex(end[0]);
@ -30,7 +32,7 @@ exports.buildRemoveRange = function (rep, builder, start, end) {
} }
}; };
exports.buildKeepRange = function (rep, builder, start, end, attribs, pool) { exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]); const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]); const endLineOffset = rep.lines.offsetOfIndex(end[0]);
@ -42,7 +44,7 @@ exports.buildKeepRange = function (rep, builder, start, end, attribs, pool) {
} }
}; };
exports.buildKeepToStartOfRange = function (rep, builder, start) { exports.buildKeepToStartOfRange = (rep, builder, start) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]); const startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]); builder.keep(startLineOffset, start[0]);

View file

@ -24,8 +24,6 @@
// requires: top // requires: top
// requires: undefined // requires: undefined
const KERNEL_SOURCE = '../static/js/require-kernel.js';
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const pluginUtils = require('./pluginfw/shared'); const pluginUtils = require('./pluginfw/shared');
@ -196,15 +194,10 @@ const Ace2Editor = function () {
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`);
pushStyleTagsFor(iframeHTML, includedCSS); pushStyleTagsFor(iframeHTML, includedCSS);
iframeHTML.push(`<script type="text/javascript" src="../static/js/require-kernel.js?v=${clientVars.randomVersionString}"></script>`);
if (!Ace2Editor.EMBEDED || !Ace2Editor.EMBEDED[KERNEL_SOURCE]) {
// Remotely src'd script tag will not work in IE; it must be embedded, so
// throw an error if it is not.
throw new Error('Require kernel could not be found.');
}
iframeHTML.push(scriptTag( iframeHTML.push(scriptTag(
`${Ace2Editor.EMBEDED[KERNEL_SOURCE]}\n\ `\n\
require.setRootURI("../javascripts/src");\n\ require.setRootURI("../javascripts/src");\n\
require.setLibraryURI("../javascripts/lib");\n\ require.setLibraryURI("../javascripts/lib");\n\
require.setGlobalKeyPath("require");\n\ require.setGlobalKeyPath("require");\n\

View file

@ -18,7 +18,7 @@
*/ */
let documentAttributeManager; let documentAttributeManager;
const browser = require('./browser'); const browser = require('./vendors/browser');
const padutils = require('./pad_utils').padutils; const padutils = require('./pad_utils').padutils;
const Ace2Common = require('./ace2_common'); const Ace2Common = require('./ace2_common');
const $ = require('./rjquery').$; const $ = require('./rjquery').$;
@ -3782,11 +3782,28 @@ function Ace2Inner() {
// We apply the height of a line in the doc body, to the corresponding sidediv line number // We apply the height of a line in the doc body, to the corresponding sidediv line number
const updateLineNumbers = () => { const updateLineNumbers = () => {
if (!currentCallStack || currentCallStack && !currentCallStack.domClean) return; if (!currentCallStack || !currentCallStack.domClean) return;
// Refs #4228, to avoid layout trashing, we need to first calculate all the heights, // Refs #4228, to avoid layout trashing, we need to first calculate all the heights,
// and then apply at once all new height to div elements // and then apply at once all new height to div elements
const lineOffsets = [];
// To place the line number on the same Z point as the first character of the first line
// we need to know the line height including the margins of the firstChild within the line
// This is somewhat computationally expensive as it looks at the first element within
// the line. Alternative, cheaper approaches are welcome.
// Original Issue: https://github.com/ether/etherpad-lite/issues/4527
const lineHeights = []; const lineHeights = [];
// 24 is the default line height within Etherpad - There may be quirks here such as
// none text elements (images/embeds etc) where the line height will be greater
// but as it's non-text type the line-height/margins might not be present and it
// could be that this breaks a theme that has a different default line height..
// So instead of using an integer here we get the value from the Editor CSS.
const innerdocbody = document.querySelector('#innerdocbody');
const innerdocbodyStyles = getComputedStyle(innerdocbody);
const defaultLineHeight = parseInt(innerdocbodyStyles['line-height']);
let docLine = doc.body.firstChild; let docLine = doc.body.firstChild;
let currentLine = 0; let currentLine = 0;
let h = null; let h = null;
@ -3811,7 +3828,21 @@ function Ace2Inner() {
// last line // last line
h = (docLine.clientHeight || docLine.offsetHeight); h = (docLine.clientHeight || docLine.offsetHeight);
} }
lineHeights.push(h); lineOffsets.push(h);
if (docLine.clientHeight !== defaultLineHeight) {
// line is wrapped OR has a larger line height within so we will do additional
// computation to figure out the line-height of the first element and
// use that for displaying the side div line number inline with the first line
// of content -- This is used in ep_headings, ep_font_size etc. where the line
// height is increased.
const elementStyle = window.getComputedStyle(docLine.firstChild);
const lineHeight = parseInt(elementStyle.getPropertyValue('line-height'));
const marginBottom = parseInt(elementStyle.getPropertyValue('margin-bottom'));
lineHeights.push(lineHeight + marginBottom);
} else {
lineHeights.push(defaultLineHeight);
}
docLine = docLine.nextSibling; docLine = docLine.nextSibling;
currentLine++; currentLine++;
} }
@ -3823,8 +3854,9 @@ function Ace2Inner() {
// Apply height to existing sidediv lines // Apply height to existing sidediv lines
currentLine = 0; currentLine = 0;
while (sidebarLine && currentLine <= lineNumbersShown) { while (sidebarLine && currentLine <= lineNumbersShown) {
if (lineHeights[currentLine] != null) { if (lineOffsets[currentLine] != null) {
sidebarLine.style.height = `${lineHeights[currentLine]}px`; sidebarLine.style.height = `${lineOffsets[currentLine]}px`;
sidebarLine.style.lineHeight = `${lineHeights[currentLine]}px`;
} }
sidebarLine = sidebarLine.nextSibling; sidebarLine = sidebarLine.nextSibling;
currentLine++; currentLine++;
@ -3839,8 +3871,9 @@ function Ace2Inner() {
while (lineNumbersShown < newNumLines) { while (lineNumbersShown < newNumLines) {
lineNumbersShown++; lineNumbersShown++;
const div = odoc.createElement('DIV'); const div = odoc.createElement('DIV');
if (lineHeights[currentLine]) { if (lineOffsets[currentLine]) {
div.style.height = `${lineHeights[currentLine]}px`; div.style.height = `${lineOffsets[currentLine]}px`;
div.style.lineHeight = `${lineHeights[currentLine]}px`;
} }
$(div).append($(`<span class='line-number'>${String(lineNumbersShown)}</span>`)); $(div).append($(`<span class='line-number'>${String(lineNumbersShown)}</span>`));
fragment.appendChild(div); fragment.appendChild(div);

View file

@ -84,11 +84,12 @@ $(document).ready(() => {
for (const attr in plugin) { for (const attr in plugin) {
if (attr === 'name') { // Hack to rewrite URLS into name if (attr === 'name') { // Hack to rewrite URLS into name
const link = $('<a>'); const link = $('<a>')
link.attr('href', `https://npmjs.org/package/${plugin.name}`); .attr('href', `https://npmjs.org/package/${plugin.name}`)
link.attr('plugin', 'Plugin details'); .attr('plugin', 'Plugin details')
link.attr('target', '_blank'); .attr('rel', 'noopener noreferrer')
link.text(plugin.name.substr(3)); .attr('target', '_blank')
.text(plugin.name.substr(3));
row.find('.name').append(link); row.find('.name').append(link);
} else { } else {
row.find(`.${attr}`).text(plugin[attr]); row.find(`.${attr}`).text(plugin[attr]);

View file

@ -28,12 +28,12 @@ const AttribPool = require('./AttributePool');
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const linestylefilter = require('./linestylefilter').linestylefilter; const linestylefilter = require('./linestylefilter').linestylefilter;
const colorutils = require('./colorutils').colorutils; const colorutils = require('./colorutils').colorutils;
const _ = require('./underscore'); const _ = require('./vendors/underscore');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
// These parameters were global, now they are injected. A reference to the // These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate. // Timeslider controller would probably be more appropriate.
function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) { const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {
let goToRevisionIfEnabledCount = 0; let goToRevisionIfEnabledCount = 0;
let changesetLoader = undefined; let changesetLoader = undefined;
@ -488,6 +488,6 @@ function loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData); receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData);
return changesetLoader; return changesetLoader;
} };
exports.loadBroadcastJS = loadBroadcastJS; exports.loadBroadcastJS = loadBroadcastJS;

View file

@ -23,7 +23,7 @@
// These parameters were global, now they are injected. A reference to the // These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate. // Timeslider controller would probably be more appropriate.
const _ = require('./underscore'); const _ = require('./vendors/underscore');
const padmodals = require('./pad_modals').padmodals; const padmodals = require('./pad_modals').padmodals;
const colorutils = require('./colorutils').colorutils; const colorutils = require('./colorutils').colorutils;

View file

@ -4,37 +4,17 @@
// This function is useful to get the caret position of the line as // This function is useful to get the caret position of the line as
// is represented by the browser // is represented by the browser
exports.getPosition = () => { exports.getPosition = () => {
let rect, line;
const range = getSelectionRange(); const range = getSelectionRange();
const isSelectionInsideTheEditor = range && if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
$(range.endContainer).closest('body')[0].id === 'innerdocbody'; // When there's a <br> or any element that has no height, we can't get the dimension of the
// element where the caret is. As we can't get the element height, we create a text node to get
if (isSelectionInsideTheEditor) { // the dimensions on the position.
// when we have the caret in an empty line, e.g. a line with only a <br>,
// getBoundingClientRect() returns all dimensions value as 0
const selectionIsInTheBeginningOfLine = range.endOffset > 0;
if (selectionIsInTheBeginningOfLine) {
const clonedRange = createSelectionRange(range); const clonedRange = createSelectionRange(range);
line = getPositionOfElementOrSelection(clonedRange);
clonedRange.detach();
}
// when there's a <br> or any element that has no height, we can't get
// the dimension of the element where the caret is
if (!rect || rect.height === 0) {
const clonedRange = createSelectionRange(range);
// as we can't get the element height, we create a text node to get the dimensions
// on the position
const shadowCaret = $(document.createTextNode('|')); const shadowCaret = $(document.createTextNode('|'));
clonedRange.insertNode(shadowCaret[0]); clonedRange.insertNode(shadowCaret[0]);
clonedRange.selectNode(shadowCaret[0]); clonedRange.selectNode(shadowCaret[0]);
const line = getPositionOfElementOrSelection(clonedRange);
line = getPositionOfElementOrSelection(clonedRange);
clonedRange.detach();
shadowCaret.remove(); shadowCaret.remove();
}
}
return line; return line;
}; };

View file

@ -1,3 +1,5 @@
'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it. * This helps other people to understand this code better and helps them to improve it.
@ -23,7 +25,7 @@
const AttributePool = require('./AttributePool'); const AttributePool = require('./AttributePool');
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) { const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// latest official text from server // latest official text from server
let baseAText = Changeset.makeAText('\n'); let baseAText = Changeset.makeAText('\n');
// changes applied to baseText that have been submitted // changes applied to baseText that have been submitted
@ -42,30 +44,30 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
let changeCallbackTimeout = null; let changeCallbackTimeout = null;
function setChangeCallbackTimeout() { const setChangeCallbackTimeout = () => {
// can call this multiple times per call-stack, because // can call this multiple times per call-stack, because
// we only schedule a call to changeCallback if it exists // we only schedule a call to changeCallback if it exists
// and if there isn't a timeout already scheduled. // and if there isn't a timeout already scheduled.
if (changeCallback && changeCallbackTimeout === null) { if (changeCallback && changeCallbackTimeout == null) {
changeCallbackTimeout = scheduler.setTimeout(() => { changeCallbackTimeout = scheduler.setTimeout(() => {
try { try {
changeCallback(); changeCallback();
} catch (pseudoError) {} finally { } catch (pseudoError) {
// as empty as my soul
} finally {
changeCallbackTimeout = null; changeCallbackTimeout = null;
} }
}, 0); }, 0);
} }
} };
let self; let self;
return self = { return self = {
isTracking() { isTracking: () => tracking,
return tracking; setBaseText: (text) => {
},
setBaseText(text) {
self.setBaseAttributedText(Changeset.makeAText(text), null); self.setBaseAttributedText(Changeset.makeAText(text), null);
}, },
setBaseAttributedText(atext, apoolJsonObj) { setBaseAttributedText: (atext, apoolJsonObj) => {
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => { aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
tracking = true; tracking = true;
baseAText = Changeset.cloneAText(atext); baseAText = Changeset.cloneAText(atext);
@ -83,7 +85,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
} }
}); });
}, },
composeUserChangeset(c) { composeUserChangeset: (c) => {
if (!tracking) return; if (!tracking) return;
if (applyingNonUserChanges) return; if (applyingNonUserChanges) return;
if (Changeset.isIdentity(c)) return; if (Changeset.isIdentity(c)) return;
@ -91,7 +93,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
setChangeCallbackTimeout(); setChangeCallbackTimeout();
}, },
applyChangesToBase(c, optAuthor, apoolJsonObj) { applyChangesToBase: (c, optAuthor, apoolJsonObj) => {
if (!tracking) return; if (!tracking) return;
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => { aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
@ -111,8 +113,10 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
const preferInsertingAfterUserChanges = true; const preferInsertingAfterUserChanges = true;
const oldUserChangeset = userChangeset; const oldUserChangeset = userChangeset;
userChangeset = Changeset.follow(c2, oldUserChangeset, preferInsertingAfterUserChanges, apool); userChangeset = Changeset.follow(
const postChange = Changeset.follow(oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool); c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
const postChange = Changeset.follow(
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor); const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
applyingNonUserChanges = true; applyingNonUserChanges = true;
@ -123,7 +127,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
} }
}); });
}, },
prepareUserChangeset() { prepareUserChangeset: () => {
// If there are user changes to submit, 'changeset' will be the // If there are user changes to submit, 'changeset' will be the
// changeset, else it will be null. // changeset, else it will be null.
let toSubmit; let toSubmit;
@ -135,7 +139,9 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
// add forEach function to Array.prototype for IE8 // add forEach function to Array.prototype for IE8
if (!('forEach' in Array.prototype)) { if (!('forEach' in Array.prototype)) {
Array.prototype.forEach = function (action, that /* opt*/) { Array.prototype.forEach = function (action, that /* opt*/) {
for (let i = 0, n = this.length; i < n; i++) if (i in this) action.call(that, this[i], i, this); for (let i = 0, n = this.length; i < n; i++) {
if (i in this) action.call(that, this[i], i, this);
}
}; };
} }
@ -143,28 +149,33 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
const authorId = parent.parent.pad.myUserInfo.userId; const authorId = parent.parent.pad.myUserInfo.userId;
// Sanitize authorship // Sanitize authorship
// We need to replace all author attribs with thisSession.author, in case they copy/pasted or otherwise inserted other peoples changes // We need to replace all author attribs with thisSession.author,
// in case they copy/pasted or otherwise inserted other peoples changes
if (apool.numToAttrib) { if (apool.numToAttrib) {
let authorAttr;
for (const attr in apool.numToAttrib) { for (const attr in apool.numToAttrib) {
if (apool.numToAttrib[attr][0] == 'author' && apool.numToAttrib[attr][1] == authorId) var authorAttr = Number(attr).toString(36); if (apool.numToAttrib[attr][0] === 'author' &&
apool.numToAttrib[attr][1] === authorId) {
authorAttr = Number(attr).toString(36);
}
} }
// Replace all added 'author' attribs with the value of the current user // Replace all added 'author' attribs with the value of the current user
var cs = Changeset.unpack(userChangeset); const cs = Changeset.unpack(userChangeset);
const iterator = Changeset.opIterator(cs.ops); const iterator = Changeset.opIterator(cs.ops);
let op; let op;
const assem = Changeset.mergingOpAssembler(); const assem = Changeset.mergingOpAssembler();
while (iterator.hasNext()) { while (iterator.hasNext()) {
op = iterator.next(); op = iterator.next();
if (op.opcode == '+') { if (op.opcode === '+') {
var newAttrs = ''; let newAttrs = '';
op.attribs.split('*').forEach((attrNum) => { op.attribs.split('*').forEach((attrNum) => {
if (!attrNum) return; if (!attrNum) return;
const attr = apool.getAttrib(parseInt(attrNum, 36)); const attr = apool.getAttrib(parseInt(attrNum, 36));
if (!attr) return; if (!attr) return;
if ('author' == attr[0]) { if ('author' === attr[0]) {
// replace that author with the current one // replace that author with the current one
newAttrs += `*${authorAttr}`; newAttrs += `*${authorAttr}`;
} else { newAttrs += `*${attrNum}`; } // overtake all other attribs as is } else { newAttrs += `*${attrNum}`; } // overtake all other attribs as is
@ -181,7 +192,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
else toSubmit = userChangeset; else toSubmit = userChangeset;
} }
var cs = null; let cs = null;
if (toSubmit) { if (toSubmit) {
submittedChangeset = toSubmit; submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit)); userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
@ -201,7 +212,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
}; };
return data; return data;
}, },
applyPreparedChangesetToBase() { applyPreparedChangesetToBase: () => {
if (!submittedChangeset) { if (!submittedChangeset) {
// violation of protocol; use prepareUserChangeset first // violation of protocol; use prepareUserChangeset first
throw new Error('applySubmittedChangesToBase: no submitted changes to apply'); throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
@ -210,13 +221,11 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool); baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
submittedChangeset = null; submittedChangeset = null;
}, },
setUserChangeNotificationCallback(callback) { setUserChangeNotificationCallback: (callback) => {
changeCallback = callback; changeCallback = callback;
}, },
hasUncommittedChanges() { hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
return !!(submittedChangeset || (!Changeset.isIdentity(userChangeset)));
},
}; };
} };
exports.makeChangesetTracker = makeChangesetTracker; exports.makeChangesetTracker = makeChangesetTracker;

View file

@ -24,7 +24,7 @@
const chat = require('./chat').chat; const chat = require('./chat').chat;
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const browser = require('./browser'); const browser = require('./vendors/browser');
// Dependency fill on init. This exists for `pad.socket` only. // Dependency fill on init. This exists for `pad.socket` only.
// TODO: bind directly to the socket. // TODO: bind directly to the socket.
@ -34,7 +34,7 @@ const getSocket = () => pad && pad.socket;
/** Call this when the document is ready, and a new Ace2Editor() has been created and inited. /** Call this when the document is ready, and a new Ace2Editor() has been created and inited.
ACE's ready callback does not need to have fired yet. ACE's ready callback does not need to have fired yet.
"serverVars" are from calling doc.getCollabClientVars() on the server. */ "serverVars" are from calling doc.getCollabClientVars() on the server. */
function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad) { const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) => {
const editor = ace2editor; const editor = ace2editor;
pad = _pad; // Inject pad to avoid a circular dependency. pad = _pad; // Inject pad to avoid a circular dependency.
@ -583,6 +583,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
setUpSocket(); setUpSocket();
return self; return self;
} };
exports.getCollabClient = getCollabClient; exports.getCollabClient = getCollabClient;

View file

@ -195,7 +195,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
} }
} }
if (listType === 'none' || !listType) { if (listType === 'none') {
delete state.lineAttributes.list; delete state.lineAttributes.list;
} else { } else {
state.lineAttributes.list = listType; state.lineAttributes.list = listType;
@ -315,6 +315,10 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
const localAttribs = state.localAttribs; const localAttribs = state.localAttribs;
state.localAttribs = null; state.localAttribs = null;
const isBlock = isBlockElement(node); const isBlock = isBlockElement(node);
if (!isBlock && node.name && (node.name !== 'body') && (node.name !== 'br')) {
console.warn('Plugin missing: ' +
`You might want to install a plugin to support this node name: ${node.name}`);
}
const isEmpty = _isEmpty(node, state); const isEmpty = _isEmpty(node, state);
if (isBlock) _ensureColumnZero(state); if (isBlock) _ensureColumnZero(state);
const startLine = lines.length() - 1; const startLine = lines.length() - 1;

View file

@ -24,7 +24,7 @@
const Security = require('./security'); const Security = require('./security');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const _ = require('./underscore'); const _ = require('./vendors/underscore');
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
const noop = () => {}; const noop = () => {};

View file

@ -1,3 +1,6 @@
'use strict';
/* eslint-disable-next-line max-len */
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0 // @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
/** /**
* Copyright 2011 Peter Martischka, Primary Technology. * Copyright 2011 Peter Martischka, Primary Technology.
@ -16,28 +19,26 @@
* limitations under the License. * limitations under the License.
*/ */
/* global $, customStart */ const randomPadName = () => {
function randomPadName() {
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when // the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
// using the PRNG below // using the PRNG below
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'; const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_';
// the length of the pad name is chosen to get 120-bit security: log2(64^20) = 120 // the length of the pad name is chosen to get 120-bit security: log2(64^20) = 120
const string_length = 20; const stringLength = 20;
// make room for 8-bit integer values that span from 0 to 255. // make room for 8-bit integer values that span from 0 to 255.
const randomarray = new Uint8Array(string_length); const randomarray = new Uint8Array(stringLength);
// use browser's PRNG to generate a "unique" sequence // use browser's PRNG to generate a "unique" sequence
const cryptoObj = window.crypto || window.msCrypto; // for IE 11 const cryptoObj = window.crypto || window.msCrypto; // for IE 11
cryptoObj.getRandomValues(randomarray); cryptoObj.getRandomValues(randomarray);
let randomstring = ''; let randomstring = '';
for (let i = 0; i < string_length; i++) { for (let i = 0; i < stringLength; i++) {
// instead of writing "Math.floor(randomarray[i]/256*64)" // instead of writing "Math.floor(randomarray[i]/256*64)"
// we can save some cycles. // we can save some cycles.
const rnum = Math.floor(randomarray[i] / 4); const rnum = Math.floor(randomarray[i] / 4);
randomstring += chars.substring(rnum, rnum + 1); randomstring += chars.substring(rnum, rnum + 1);
} }
return randomstring; return randomstring;
} };
$(() => { $(() => {
$('#go2Name').submit(() => { $('#go2Name').submit(() => {
@ -55,7 +56,7 @@ $(() => {
}); });
// start the custom js // start the custom js
if (typeof customStart === 'function') customStart(); if (typeof window.customStart === 'function') window.customStart();
}); });
// @license-end // @license-end

View file

@ -1,4 +1,6 @@
(function (document) { 'use strict';
((document) => {
// Set language for l10n // Set language for l10n
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/); let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
if (language) language = language[1]; if (language) language = language[1];

View file

@ -26,9 +26,9 @@ let socket;
// These jQuery things should create local references, but for now `require()` // These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins. // assigns to the global `$` and augments it with plugins.
require('./jquery'); require('./vendors/jquery');
require('./farbtastic'); require('./vendors/farbtastic');
require('./gritter'); require('./vendors/gritter');
const Cookies = require('./pad_utils').Cookies; const Cookies = require('./pad_utils').Cookies;
const chat = require('./chat').chat; const chat = require('./chat').chat;

View file

@ -22,7 +22,7 @@
* limitations under the License. * limitations under the License.
*/ */
const browser = require('./browser'); const browser = require('./vendors/browser');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils; const padutils = require('./pad_utils').padutils;
const padeditor = require('./pad_editor').padeditor; const padeditor = require('./pad_editor').padeditor;

View file

@ -18,15 +18,14 @@
const padutils = require('./pad_utils').padutils; const padutils = require('./pad_utils').padutils;
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const browser = require('./browser');
let myUserInfo = {}; let myUserInfo = {};
let colorPickerOpen = false; let colorPickerOpen = false;
let colorPickerSetup = false; let colorPickerSetup = false;
const paduserlist = (function () { const paduserlist = (() => {
const rowManager = (function () { const rowManager = (() => {
// The row manager handles rendering rows of the user list and animating // The row manager handles rendering rows of the user list and animating
// their insertion, removal, and reordering. It manipulates TD height // their insertion, removal, and reordering. It manipulates TD height
// and TD opacity. // and TD opacity.
@ -291,7 +290,7 @@ const paduserlist = (function () {
updateRow, updateRow,
}; };
return self; return self;
}()); // //////// rowManager })(); // //////// rowManager
const otherUsersInfo = []; const otherUsersInfo = [];
const otherUsersData = []; const otherUsersData = [];
@ -347,7 +346,7 @@ const paduserlist = (function () {
let pad = undefined; let pad = undefined;
const self = { const self = {
init(myInitialUserInfo, _pad) { init: (myInitialUserInfo, _pad) => {
pad = _pad; pad = _pad;
self.setMyUserInfo(myInitialUserInfo); self.setMyUserInfo(myInitialUserInfo);
@ -544,7 +543,7 @@ const paduserlist = (function () {
}, },
}; };
return self; return self;
}()); })();
const getColorPickerSwatchIndex = (jnode) => $('#colorpickerswatches li').index(jnode); const getColorPickerSwatchIndex = (jnode) => $('#colorpickerswatches li').index(jnode);

View file

@ -12,9 +12,10 @@ exports.update = (cb) => {
// of execution on Firefox. This schedules the response in the run-loop, // of execution on Firefox. This schedules the response in the run-loop,
// which appears to fix the issue. // which appears to fix the issue.
const callback = () => setTimeout(cb, 0); const callback = () => setTimeout(cb, 0);
$.ajaxSetup({cache: false});
jQuery.getJSON(`${exports.baseURL}pluginfw/plugin-definitions.json`).done((data) => { jQuery.getJSON(
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`
).done((data) => {
defs.plugins = data.plugins; defs.plugins = data.plugins;
defs.parts = data.parts; defs.parts = data.parts;
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks'); defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');

View file

@ -3,59 +3,65 @@
const log4js = require('log4js'); const log4js = require('log4js');
const plugins = require('./plugins'); const plugins = require('./plugins');
const hooks = require('./hooks'); const hooks = require('./hooks');
const npm = require('npm');
const request = require('request'); const request = require('request');
const util = require('util'); const runCmd = require('../../../node/utils/run_cmd');
const settings = require('../../../node/utils/Settings');
let npmIsLoaded = false; const logger = log4js.getLogger('plugins');
const loadNpm = async () => {
if (npmIsLoaded) return;
await util.promisify(npm.load)({});
npmIsLoaded = true;
npm.on('log', log4js.getLogger('npm').log);
};
const onAllTasksFinished = () => { const onAllTasksFinished = async () => {
hooks.aCallAll('restartServer', {}, () => {}); settings.reloadSettings();
await hooks.aCallAll('loadSettings', {settings});
await hooks.aCallAll('restartServer');
}; };
let tasks = 0; let tasks = 0;
function wrapTaskCb(cb) { const wrapTaskCb = (cb) => {
tasks++; tasks++;
return function (...args) { return (...args) => {
cb && cb.apply(this, args); cb && cb(...args);
tasks--; tasks--;
if (tasks === 0) onAllTasksFinished(); if (tasks === 0) onAllTasksFinished();
}; };
} };
exports.uninstall = async (pluginName, cb = null) => { exports.uninstall = async (pluginName, cb = null) => {
cb = wrapTaskCb(cb); cb = wrapTaskCb(cb);
logger.info(`Uninstalling plugin ${pluginName}...`);
try { try {
await loadNpm(); // The --no-save flag prevents npm from creating package.json or package-lock.json.
await util.promisify(npm.commands.uninstall)([pluginName]); // The --legacy-peer-deps flag is required to work around a bug in npm v7:
await hooks.aCallAll('pluginUninstall', {pluginName}); // https://github.com/npm/cli/issues/2199
await plugins.update(); await runCmd(['npm', 'uninstall', '--no-save', '--legacy-peer-deps', pluginName]);
} catch (err) { } catch (err) {
logger.error(`Failed to uninstall plugin ${pluginName}`);
cb(err || new Error(err)); cb(err || new Error(err));
throw err; throw err;
} }
logger.info(`Successfully uninstalled plugin ${pluginName}`);
await hooks.aCallAll('pluginUninstall', {pluginName});
await plugins.update();
cb(null); cb(null);
}; };
exports.install = async (pluginName, cb = null) => { exports.install = async (pluginName, cb = null) => {
cb = wrapTaskCb(cb); cb = wrapTaskCb(cb);
logger.info(`Installing plugin ${pluginName}...`);
try { try {
await loadNpm(); // The --no-save flag prevents npm from creating package.json or package-lock.json.
await util.promisify(npm.commands.install)([`${pluginName}@latest`]); // The --legacy-peer-deps flag is required to work around a bug in npm v7:
await hooks.aCallAll('pluginInstall', {pluginName}); // https://github.com/npm/cli/issues/2199
await plugins.update(); await runCmd(['npm', 'install', '--no-save', '--legacy-peer-deps', pluginName]);
} catch (err) { } catch (err) {
logger.error(`Failed to install plugin ${pluginName}`);
cb(err || new Error(err)); cb(err || new Error(err));
throw err; throw err;
} }
logger.info(`Successfully installed plugin ${pluginName}`);
await hooks.aCallAll('pluginInstall', {pluginName});
await plugins.update();
cb(null); cb(null);
}; };
@ -77,7 +83,7 @@ exports.getAvailablePlugins = (maxCacheAge) => {
try { try {
plugins = JSON.parse(plugins); plugins = JSON.parse(plugins);
} catch (err) { } catch (err) {
console.error('error parsing plugins.json:', err); logger.error(`error parsing plugins.json: ${err.stack || err}`);
plugins = []; plugins = [];
} }
@ -107,7 +113,7 @@ exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCac
!~results[pluginName].description.toLowerCase().indexOf(searchTerm)) !~results[pluginName].description.toLowerCase().indexOf(searchTerm))
) { ) {
if (typeof results[pluginName].description === 'undefined') { if (typeof results[pluginName].description === 'undefined') {
console.debug('plugin without Description: %s', results[pluginName].name); logger.debug(`plugin without Description: ${results[pluginName].name}`);
} }
continue; continue;

View file

@ -2,15 +2,26 @@
const fs = require('fs').promises; const fs = require('fs').promises;
const hooks = require('./hooks'); const hooks = require('./hooks');
const log4js = require('log4js');
const path = require('path'); const path = require('path');
const runNpm = require('../../../node/utils/run_npm'); const runCmd = require('../../../node/utils/run_cmd');
const tsort = require('./tsort'); const tsort = require('./tsort');
const util = require('util');
const settings = require('../../../node/utils/Settings');
const pluginUtils = require('./shared'); const pluginUtils = require('./shared');
const defs = require('./plugin_defs'); const defs = require('./plugin_defs');
const logger = log4js.getLogger('plugins');
// Log the version of npm at startup.
(async () => {
try {
const version = await runCmd(['npm', '--version'], {stdio: [null, 'string']});
logger.info(`npm --version: ${version}`);
} catch (err) {
logger.error(`Failed to get npm version: ${err.stack || err}`);
// This isn't a fatal error so don't re-throw.
}
})();
exports.prefix = 'ep_'; exports.prefix = 'ep_';
exports.formatPlugins = () => Object.keys(defs.plugins).join(', '); exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
@ -32,7 +43,7 @@ exports.formatHooks = (hookSetName) => {
const callInit = async () => { const callInit = async () => {
await Promise.all(Object.keys(defs.plugins).map(async (pluginName) => { await Promise.all(Object.keys(defs.plugins).map(async (pluginName) => {
const plugin = defs.plugins[pluginName]; const plugin = defs.plugins[pluginName];
const epInit = path.normalize(path.join(plugin.package.path, '.ep_initialized')); const epInit = path.join(plugin.package.path, '.ep_initialized');
try { try {
await fs.stat(epInit); await fs.stat(epInit);
} catch (err) { } catch (err) {
@ -48,7 +59,7 @@ exports.pathNormalization = (part, hookFnName, hookName) => {
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName; const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName;
const moduleName = tmp.join(':') || part.plugin; const moduleName = tmp.join(':') || part.plugin;
const packageDir = path.dirname(defs.plugins[part.plugin].package.path); const packageDir = path.dirname(defs.plugins[part.plugin].package.path);
const fileName = path.normalize(path.join(packageDir, moduleName)); const fileName = path.join(packageDir, moduleName);
return `${fileName}:${functionName}`; return `${fileName}:${functionName}`;
}; };
@ -58,8 +69,11 @@ exports.update = async () => {
const plugins = {}; const plugins = {};
// Load plugin metadata ep.json // Load plugin metadata ep.json
await Promise.all(Object.keys(packages).map( await Promise.all(Object.keys(packages).map(async (pluginName) => {
async (pluginName) => await loadPlugin(packages, pluginName, plugins, parts))); logger.info(`Loading plugin ${pluginName}...`);
await loadPlugin(packages, pluginName, plugins, parts);
}));
logger.info(`Loaded ${Object.keys(packages).length} plugins`);
defs.plugins = plugins; defs.plugins = plugins;
defs.parts = sortParts(parts); defs.parts = sortParts(parts);
@ -69,21 +83,14 @@ exports.update = async () => {
}; };
exports.getPackages = async () => { exports.getPackages = async () => {
// Note: Do not pass `--prod` because it does not work if there is no package.json. logger.info('Running npm to get a list of installed plugins...');
const np = runNpm(['ls', '--long', '--json', '--depth=0'], { // Notes:
stdoutLogger: null, // We want to capture stdout, so don't attempt to log it. // * Do not pass `--prod` otherwise `npm ls` will fail if there is no `package.json`.
env: { // * The `--no-production` flag is required (or the `NODE_ENV` environment variable must be
...process.env, // unset or set to `development`) because otherwise `npm ls` will not mention any packages
// NODE_ENV must be set to development for `npm ls` to show files without a package.json. // that are not included in `package.json` (which is expected to not exist).
NODE_ENV: 'development', const cmd = ['npm', 'ls', '--long', '--json', '--depth=0', '--no-production'];
}, const {dependencies = {}} = JSON.parse(await runCmd(cmd, {stdio: [null, 'string']}));
});
const chunks = [];
await Promise.all([
(async () => { for await (const chunk of np.stdout) chunks.push(chunk); })(),
np, // Await in parallel to avoid unhandled rejection if np rejects during chunk read.
]);
const {dependencies = {}} = JSON.parse(Buffer.concat(chunks).toString());
await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => { await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => {
if (!pkg.startsWith(exports.prefix)) { if (!pkg.startsWith(exports.prefix)) {
delete dependencies[pkg]; delete dependencies[pkg];
@ -107,11 +114,11 @@ const loadPlugin = async (packages, pluginName, plugins, parts) => {
part.full_name = `${pluginName}/${part.name}`; part.full_name = `${pluginName}/${part.name}`;
parts[part.full_name] = part; parts[part.full_name] = part;
} }
} catch (ex) { } catch (err) {
console.error(`Unable to parse plugin definition file ${pluginPath}: ${ex.toString()}`); logger.error(`Unable to parse plugin definition file ${pluginPath}: ${err.stack || err}`);
} }
} catch (er) { } catch (err) {
console.error(`Unable to load plugin definition file ${pluginPath}`); logger.error(`Unable to load plugin definition file ${pluginPath}: ${err.stack || err}`);
} }
}; };

View file

@ -81,6 +81,7 @@ const tsortTest = () => {
try { try {
sorted = tsort(edges); sorted = tsort(edges);
console.log('succeeded', sorted);
} catch (e) { } catch (e) {
console.log(e.message); console.log(e.message);
} }

View file

@ -1,5 +1,5 @@
'use strict'; 'use strict';
// Provides a require'able version of jQuery without leaking $ and jQuery; // Provides a require'able version of jQuery without leaking $ and jQuery;
window.$ = require('./jquery'); window.$ = require('./vendors/jquery');
const jq = window.$.noConflict(true); const jq = window.$.noConflict(true);
exports.jQuery = exports.$ = jq; exports.jQuery = exports.$ = jq;

View file

@ -1,3 +1,5 @@
'use strict';
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *

View file

@ -23,7 +23,7 @@
*/ */
const Ace2Common = require('./ace2_common'); const Ace2Common = require('./ace2_common');
const _ = require('./underscore'); const _ = require('./vendors/underscore');
const noop = Ace2Common.noop; const noop = Ace2Common.noop;

View file

@ -24,7 +24,7 @@
// These jQuery things should create local references, but for now `require()` // These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins. // assigns to the global `$` and augments it with plugins.
require('./jquery'); require('./vendors/jquery');
const Cookies = require('./pad_utils').Cookies; const Cookies = require('./pad_utils').Cookies;
const randomString = require('./pad_utils').randomString; const randomString = require('./pad_utils').randomString;

View file

@ -23,7 +23,7 @@
*/ */
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const _ = require('./underscore'); const _ = require('./vendors/underscore');
const undoModule = (() => { const undoModule = (() => {
const stack = (() => { const stack = (() => {

View file

@ -1,3 +1,7 @@
// WARNING: This file may have been modified from original.
// TODO: Check requirement of this file, this afaik was to cover weird edge cases
// that have probably been fixed in browsers.
/*! /*!
* Bowser - a browser detector * Bowser - a browser detector
* https://github.com/ded/bowser * https://github.com/ded/bowser

View file

@ -1,3 +1,6 @@
// WARNING: This file has been modified from original.
// TODO: Replace with https://github.com/Simonwep/pickr
// Farbtastic 2.0 alpha // Farbtastic 2.0 alpha
// Original can be found at: // Original can be found at:
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/src/farbtastic.js // https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/src/farbtastic.js

View file

@ -1,3 +1,5 @@
// WARNING: This file has been modified from the Original
/* /*
* Gritter for jQuery * Gritter for jQuery
* http://www.boedesign.com/ * http://www.boedesign.com/

View file

@ -1,3 +1,5 @@
// WARNING: This file has been modified from the Original
/** /**
* Copyright (c) 2012 Marcel Klehr * Copyright (c) 2012 Marcel Klehr
* Copyright (c) 2011-2012 Fabien Cazenave, Mozilla * Copyright (c) 2011-2012 Fabien Cazenave, Mozilla

View file

@ -1,3 +1,6 @@
// WARNING: This file has been modified from the Original
// TODO: Nice Select seems relatively abandoned, we should consider other options.
/* jQuery Nice Select - v1.1.0 /* jQuery Nice Select - v1.1.0
https://github.com/hernansartorio/jquery-nice-select https://github.com/hernansartorio/jquery-nice-select
Made by Hernán Sartorio */ Made by Hernán Sartorio */
@ -60,14 +63,14 @@
.addClass($select.attr('class') || '') .addClass($select.attr('class') || '')
.addClass($select.attr('disabled') ? 'disabled' : '') .addClass($select.attr('disabled') ? 'disabled' : '')
.attr('tabindex', $select.attr('disabled') ? null : '0') .attr('tabindex', $select.attr('disabled') ? null : '0')
.html('<span class="current"></span><ul class="list thin-scrollbar"></ul>') .html('<span class="current"></span><ul class="list"></ul>')
); );
var $dropdown = $select.next(); var $dropdown = $select.next();
var $options = $select.find('option'); var $options = $select.find('option');
var $selected = $select.find('option:selected'); var $selected = $select.find('option:selected');
$dropdown.find('.current').html($selected.data('display') || $selected.text()); $dropdown.find('.current').html($selected.data('display') || $selected.text());
$options.each(function(i) { $options.each(function(i) {
var $option = $(this); var $option = $(this);
@ -94,31 +97,12 @@
var $dropdown = $(this); var $dropdown = $(this);
$('.nice-select').not($dropdown).removeClass('open'); $('.nice-select').not($dropdown).removeClass('open');
$dropdown.toggleClass('open'); $dropdown.toggleClass('open');
if ($dropdown.hasClass('open')) { if ($dropdown.hasClass('open')) {
$dropdown.find('.option'); $dropdown.find('.option');
$dropdown.find('.focus').removeClass('focus'); $dropdown.find('.focus').removeClass('focus');
$dropdown.find('.selected').addClass('focus'); $dropdown.find('.selected').addClass('focus');
if ($dropdown.closest('.toolbar').length > 0) {
$dropdown.find('.list').css('left', $dropdown.offset().left);
$dropdown.find('.list').css('top', $dropdown.offset().top + $dropdown.outerHeight());
$dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px');
}
$listHeight = $dropdown.find('.list').outerHeight();
$top = $dropdown.parent().offset().top;
$bottom = $('body').height() - $top;
$maxListHeight = $bottom - $dropdown.outerHeight() - 20;
if ($maxListHeight < 200) {
$dropdown.addClass('reverse');
$maxListHeight = 250;
} else {
$dropdown.removeClass('reverse')
}
$dropdown.find('.list').css('max-height', $maxListHeight + 'px');
} else { } else {
$dropdown.focus(); $dropdown.focus();
} }

View file

@ -1 +1,3 @@
'use strict';
module.exports = require('underscore'); module.exports = require('underscore');

View file

@ -1,5 +1,7 @@
function customStart() { 'use strict';
window.customStart = () => {
// define your javascript here // define your javascript here
// jquery is available - except index.js // jquery is available - except index.js
// you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/ // you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
} };

View file

@ -1,5 +1,7 @@
function customStart() { 'use strict';
window.customStart = () => {
$('#pad_title').show(); $('#pad_title').show();
$('.buttonicon').mousedown(function () { $(this).parent().addClass('pressed'); }); $('.buttonicon').mousedown(function () { $(this).parent().addClass('pressed'); });
$('.buttonicon').mouseup(function () { $(this).parent().removeClass('pressed'); }); $('.buttonicon').mouseup(function () { $(this).parent().removeClass('pressed'); });
} };

View file

@ -12,8 +12,20 @@
} }
#sidedivinner>div .line-number { #sidedivinner>div .line-number {
line-height: 24px; line-height: inherit;
font-family: RobotoMono; font-family: RobotoMono;
display: inline-block;
color: #576273; color: #576273;
color: var(--text-soft-color); color: var(--text-soft-color);
height:100%;
}
#sidedivinner>div .line-number:hover {
background-color: var(--bg-soft-color);
border-radius: 5px 0 0 5px;
font-weight: bold;
color: var(--text-color);
}
.plugin-ep_author_neat #sidedivinner>div .line-number:hover {
background-color: transparent;
} }

View file

@ -1,2 +1,4 @@
function customStart() { 'use strict';
}
window.customStart = () => {
};

View file

@ -1,5 +1,7 @@
function customStart() { 'use strict';
window.customStart = () => {
// define your javascript here // define your javascript here
// jquery is available - except index.js // jquery is available - except index.js
// you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/ // you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
} };

View file

@ -1,5 +1,7 @@
function customStart() { 'use strict';
window.customStart = () => {
// define your javascript here // define your javascript here
// jquery is available - except index.js // jquery is available - except index.js
// you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/ // you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
} };

View file

@ -1,5 +1,7 @@
function customStart() { 'use strict';
window.customStart = () => {
// define your javascript here // define your javascript here
// jquery is available - except index.js // jquery is available - except index.js
// you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/ // you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
} };

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>API Test and Examples Page</title> <title>API Test and Examples Page</title>
<script type="text/javascript" src="js/jquery.js"></script> <script type="text/javascript" src="js/vendors/jquery.js"></script>
<style type="text/css"> <style type="text/css">
body { body {
font-size:9pt; font-size:9pt;

View file

@ -4,10 +4,10 @@
<title data-l10n-id="admin.page-title">Admin Dashboard - Etherpad</title> <title data-l10n-id="admin.page-title">Admin Dashboard - Etherpad</title>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="../static/css/admin.css"> <link rel="stylesheet" href="../static/css/admin.css">
<script src="../static/js/jquery.js"></script> <script src="../static/js/vendors/jquery.js"></script>
<script src="../socket.io/socket.io.js"></script> <script src="../socket.io/socket.io.js"></script>
<link rel="localizations" type="application/l10n+json" href="../locales.json" /> <link rel="localizations" type="application/l10n+json" href="../locales.json" />
<script src="../static/js/html10n.js"></script> <script src="../static/js/vendors/html10n.js"></script>
<script src="../static/js/l10n.js"></script> <script src="../static/js/l10n.js"></script>
</head> </head>
<body> <body>

View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="../../static/css/admin.css"> <link rel="stylesheet" href="../../static/css/admin.css">
<link rel="localizations" type="application/l10n+json" href="../../locales.json" /> <link rel="localizations" type="application/l10n+json" href="../../locales.json" />
<script src="../../static/js/html10n.js"></script> <script src="../../static/js/vendors/html10n.js"></script>
<script src="../../static/js/l10n.js"></script> <script src="../../static/js/l10n.js"></script>
</head> </head>
<body> <body>

View file

@ -4,12 +4,12 @@
<title data-l10n-id="admin_plugins.page-title">Plugin manager - Etherpad</title> <title data-l10n-id="admin_plugins.page-title">Plugin manager - Etherpad</title>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="../static/css/admin.css"> <link rel="stylesheet" href="../static/css/admin.css">
<script src="../static/js/jquery.js"></script> <script src="../static/js/vendors/jquery.js"></script>
<script src="../socket.io/socket.io.js"></script> <script src="../socket.io/socket.io.js"></script>
<script src="../static/js/socketio.js"></script> <script src="../static/js/socketio.js"></script>
<script src="../static/js/admin/plugins.js"></script> <script src="../static/js/admin/plugins.js"></script>
<link rel="localizations" type="application/l10n+json" href="../locales.json" /> <link rel="localizations" type="application/l10n+json" href="../locales.json" />
<script src="../static/js/html10n.js"></script> <script src="../static/js/vendors/html10n.js"></script>
<script src="../static/js/l10n.js"></script> <script src="../static/js/l10n.js"></script>
</head> </head>
<body> <body>

View file

@ -4,14 +4,14 @@
<title data-l10n-id="admin_settings.page-title">Settings - Etherpad</title> <title data-l10n-id="admin_settings.page-title">Settings - Etherpad</title>
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="../static/css/admin.css"> <link rel="stylesheet" href="../static/css/admin.css">
<script src="../static/js/jquery.js"></script> <script src="../static/js/vendors/jquery.js"></script>
<script src="../socket.io/socket.io.js"></script> <script src="../socket.io/socket.io.js"></script>
<script src="../static/js/socketio.js"></script> <script src="../static/js/socketio.js"></script>
<script src="../static/js/admin/minify.json.js"></script> <script src="../static/js/admin/minify.json.js"></script>
<script src="../static/js/admin/settings.js"></script> <script src="../static/js/admin/settings.js"></script>
<script src="../static/js/admin/jquery.autosize.js"></script> <script src="../static/js/admin/jquery.autosize.js"></script>
<link rel="localizations" type="application/l10n+json" href="../locales.json" /> <link rel="localizations" type="application/l10n+json" href="../locales.json" />
<script src="../static/js/html10n.js"></script> <script src="../static/js/vendors/html10n.js"></script>
<script src="../static/js/l10n.js"></script> <script src="../static/js/l10n.js"></script>
</head> </head>
<body> <body>

View file

@ -11,9 +11,9 @@
<link rel="shortcut icon" href="<%=settings.favicon%>"> <link rel="shortcut icon" href="<%=settings.favicon%>">
<link rel="localizations" type="application/l10n+json" href="locales.json"> <link rel="localizations" type="application/l10n+json" href="locales.json">
<script type="text/javascript" src="static/js/html10n.js?v=<%=settings.randomVersionString%>"></script> <script type="text/javascript" src="static/js/vendors/html10n.js?v=<%=settings.randomVersionString%>"></script>
<script type="text/javascript" src="static/js/l10n.js?v=<%=settings.randomVersionString%>"></script> <script type="text/javascript" src="static/js/l10n.js?v=<%=settings.randomVersionString%>"></script>
<script src="static/js/jquery.js"></script> <script src="static/js/vendors/jquery.js"></script>
<script src="static/js/index.js"></script> <script src="static/js/index.js"></script>
<style> <style>

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