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**
If applicable, add screenshots to help explain your problem.
**Server (please complete the following information):**
- Etherpad version:
- OS: [e.g., Ubuntu 20.04]
- Node.js version (`node --version`):
- npm version (`npm --version`):
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]

View file

@ -4,22 +4,27 @@ name: "Backend tests"
on: [push, pull_request]
jobs:
withoutplugins:
withoutpluginsLinux:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: without plugins
name: Linux without plugins
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [10, 12, 14, 15]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
node-version: ${{ matrix.node }}
- name: Install libreoffice
run: |
@ -33,22 +38,27 @@ jobs:
- name: Run the backend tests
run: cd src && npm test
withplugins:
withpluginsLinux:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: with Plugins
name: Linux with Plugins
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [10, 12, 14, 15]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
node-version: ${{ matrix.node }}
- name: Install libreoffice
run: |
@ -57,8 +67,10 @@ jobs:
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
- name: Install Etherpad plugins
# The --legacy-peer-deps flag is required to work around a bug in npm v7:
# https://github.com/npm/cli/issues/2199
run: >
npm install
npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace
@ -85,3 +97,89 @@ jobs:
- name: Run the backend tests
run: cd src && npm test
withoutpluginsWindows:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: Windows without plugins
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Install all dependencies and symlink for ep_etherpad-lite
run: |
cd src
npm ci --no-optional
- name: Fix up the settings.json
run: |
powershell -Command "(gc settings.json.template) -replace '\"max\": 10', '\"max\": 10000' | Out-File -encoding ASCII settings.json.holder"
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
- name: Run the backend tests
run: cd src && npm test
withpluginsWindows:
# run on pushes to any branch
# run on PRs from external forks
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: Windows with Plugins
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
- name: Install Etherpad plugins
# The --legacy-peer-deps flag is required to work around a bug in npm v7:
# https://github.com/npm/cli/issues/2199
run: >
npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace
ep_font_size
ep_hash_auth
ep_headings2
ep_image_upload
ep_markdown
ep_readonly_guest
ep_set_title_on_pad
ep_spellcheck
ep_subscript_and_superscript
ep_table_of_contents
# This must be run after installing the plugins, otherwise npm will try to
# hoist common dependencies by removing them from src/node_modules and
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
# logic appears to be buggy, because it sometimes removes dependencies from
# src/node_modules but fails to add them to the top-level node_modules. Even
# if npm correctly hoists the dependencies, the hoisting seems to confuse
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
- name: Install all dependencies and symlink for ep_etherpad-lite
run: |
cd src
npm ci --no-optional
- name: Fix up the settings.json
run: |
powershell -Command "(gc settings.json.template) -replace '\"max\": 10', '\"max\": 10000' | Out-File -encoding ASCII settings.json.holder"
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
- name: Run the backend tests
run: cd src && npm test

View file

@ -7,29 +7,41 @@ jobs:
name: with plugins
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [10, 12, 14, 15]
steps:
- name: Generate Sauce Labs strings
id: sauce_strings
run: |
printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }} - Node ${{ matrix.node }}'
printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}-node${{ matrix.node }}'
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 12
node-version: ${{ matrix.node }}
- name: Run sauce-connect-action
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: src/tests/frontend/travis/sauce_tunnel.sh
- name: Install etherpad plugins
# We intentionally install an old ep_align version to test upgrades to the minor version number.
# The --legacy-peer-deps flag is required to work around a bug in npm v7:
# https://github.com/npm/cli/issues/2199
run: npm install --no-save --legacy-peer-deps ep_align@0.2.27
# This must be run after installing the plugins, otherwise npm will try to
# hoist common dependencies by removing them from src/node_modules and
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
# logic appears to be buggy, because it sometimes removes dependencies from
# src/node_modules but fails to add them to the top-level node_modules. Even
# if npm correctly hoists the dependencies, the hoisting seems to confuse
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
- name: Install all dependencies and symlink for ep_etherpad-lite
run: src/bin/installDeps.sh
# We intentionally install a much old ep_align version to test update minor versions
- name: Install etherpad plugins
run: npm install ep_align@0.2.27
# Nuke plugin tests
- name: Install etherpad plugins
run: rm -Rf node_modules/ep_align/static/tests/*
@ -38,8 +50,8 @@ jobs:
id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json"
- name: Create settings.json
run: cp settings.json.template settings.json
- name: Write custom settings.json that enables the Admin UI tests
run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json"
@ -47,12 +59,19 @@ jobs:
- name: Remove standard frontend test files, so only admin tests are run
run: mv src/tests/frontend/specs/* /tmp && mv /tmp/admin*.js src/tests/frontend/specs
- uses: saucelabs/sauce-connect-action@v1.1.2
with:
username: ${{ secrets.SAUCE_USERNAME }}
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }}
- name: Run the frontend admin tests
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
src/tests/frontend/travis/adminrunner.sh

View file

@ -8,6 +8,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Generate Sauce Labs strings
id: sauce_strings
run: |
printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}'
printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}'
- name: Checkout repository
uses: actions/checkout@v2
@ -15,14 +21,6 @@ jobs:
with:
node-version: 12
- name: Run sauce-connect-action
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: src/tests/frontend/travis/sauce_tunnel.sh
- name: Install all dependencies and symlink for ep_etherpad-lite
run: src/bin/installDeps.sh
@ -30,15 +28,26 @@ jobs:
id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json"
- name: Create settings.json
run: cp settings.json.template settings.json
- name: Disable import/export rate limiting
run: |
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json
- uses: saucelabs/sauce-connect-action@v1.1.2
with:
username: ${{ secrets.SAUCE_USERNAME }}
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }}
- name: Run the frontend tests
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
src/tests/frontend/travis/runner.sh
@ -48,6 +57,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Generate Sauce Labs strings
id: sauce_strings
run: |
printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}'
printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}'
- name: Checkout repository
uses: actions/checkout@v2
@ -55,17 +70,11 @@ jobs:
with:
node-version: 12
- name: Run sauce-connect-action
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
run: src/tests/frontend/travis/sauce_tunnel.sh
- name: Install Etherpad plugins
# The --legacy-peer-deps flag is required to work around a bug in npm v7:
# https://github.com/npm/cli/issues/2199
run: >
npm install
npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace
@ -94,22 +103,30 @@ jobs:
id: environment
run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})"
- name: Write custom settings.json with loglevel WARN
run: "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/' < settings.json.template > settings.json"
- name: Create settings.json
run: cp settings.json.template settings.json
- name: Write custom settings.json that enables the Admin UI tests
run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme\",\"is_admin\":true}}/' settings.json"
- name: Disable import/export rate limiting
run: |
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json
# XXX we should probably run all tests, because plugins could effect their results
- name: Remove standard frontend test files, so only plugin tests are run
run: rm src/tests/frontend/specs/*
- uses: saucelabs/sauce-connect-action@v1.1.2
with:
username: ${{ secrets.SAUCE_USERNAME }}
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }}
- name: Run the frontend tests
shell: bash
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
TRAVIS_JOB_NUMBER: ${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}
SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }}
TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }}
GIT_HASH: ${{ steps.environment.outputs.sha_short }}
run: |
src/tests/frontend/travis/runner.sh

View file

@ -51,8 +51,10 @@ jobs:
run: sudo npm install -g etherpad-load-test
- name: Install etherpad plugins
# The --legacy-peer-deps flag is required to work around a bug in npm v7:
# https://github.com/npm/cli/issues/2199
run: >
npm install
npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace

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 -y install libreoffice libreoffice-pdfimport
# The --legacy-peer-deps flag is required to work around a bug in npm v7:
# https://github.com/npm/cli/issues/2199
_install_plugins: &install_plugins >-
npm install
npm install --no-save --legacy-peer-deps
ep_align
ep_author_hover
ep_cursortrace

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

View file

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

View file

@ -1,14 +1,32 @@
# 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")
# About
Etherpad is a real-time collaborative editor [scalable to thousands of simultaneous real time users](http://scale.etherpad.org/). It provides [full data export](https://github.com/ether/etherpad-lite/wiki/Understanding-Etherpad's-Full-Data-Export-capabilities) capabilities, and runs on _your_ server, under _your_ control.
**[Try it out](https://video.etherpad.com)**
# Try it out
Etherpad is extremely flexible providing you the means to modify it to solve whatever problem your community has. We provide some demo instances for you try different experiences available within Etherpad. Pad content is automatically removed after 24 hours.
* [Rich Editing](https://rich.etherpad.com) - A full rich text WYSIWYG editor.
* [Minimalist editor](https://minimalist.etherpad.com) - A minimalist editor that can be embedded within your tool.
* [Dark Mode](https://dark.etherpad.com) - Theme settings to have Etherpad start in dark mode, ideal for using Etherpad at night or for long durations.
* [Images](https://image.etherpad.com) - Plugins to improve provide Image support within a pad.
* [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio chat in a pad.
* [Collaboration++](https://collab.etherpad.com) - Plugins to improve the really-real time collaboration experience, suitable for busy pads.
* [Document Analysis](https://analysis.etherpad.com) - Plugins to improve author and document analysis during and post creation.
# Project Status
### Code Quality
[![Code Quality](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml) [![Total alerts](https://img.shields.io/lgtm/alerts/g/ether/etherpad-lite.svg?logo=lgtm&logoWidth=18&color=%2344b492)](https://lgtm.com/projects/g/ether/etherpad-lite/alerts/) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ether/etherpad-lite.svg?logo=lgtm&logoWidth=18&color=%2344b492)](https://lgtm.com/projects/g/ether/etherpad-lite/context:javascript) [![package.lock](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/lint-package-lock.yml)
### Testing
[![Backend tests](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml) [![Simulated Load](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml) [![Rate Limit](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml) [![Windows Zip](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml) [![Docker file](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml)
[![Frontend admin tests](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) [![Frontend tests](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) [![Windows Installer](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml)
### Engagement
<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
@ -43,7 +61,7 @@ start with `src/bin/run.sh` will update the dependencies.
## Windows
### Prebuilt Windows package
This package runs on any Windows machine, but for development purposes, please do a manual install.
This package runs on any Windows machine. You can perform a manual installation via git for development purposes, but as this uses symlinks which performs unreliably on Windows, please stick to the prebuilt package if possible.
1. [Download the latest Windows package](https://etherpad.org/#download)
2. Extract the folder
@ -105,7 +123,7 @@ Etherpad is very customizable through plugins. Instructions for installing theme
Run the following command in your Etherpad folder to get all of the features visible in the demo gif:
```
npm install ep_headings2 ep_markdown ep_comments_page ep_align ep_font_color ep_webrtc ep_embedded_hyperlinks2
npm install --no-save --legacy-peer-deps ep_headings2 ep_markdown ep_comments_page ep_align ep_font_color ep_webrtc ep_embedded_hyperlinks2
```
## Customize the style with skin variants

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.
### JSONP
The API provides _JSONP_ support to allow requests from a server in a different domain.
Simply add `&jsonp=?` to the API call.
Example usage: https://api.jquery.com/jQuery.getJSON/
## API Methods
### Groups
@ -636,4 +629,3 @@ get stats of the etherpad instance
*Example returns*
* `{"code":0,"message":"ok","data":{"totalPads":3,"totalSessions": 2,"totalActivePads": 1}}`

View file

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

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
<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 from npm, using `npm install ep_flubberworm` in Etherpad's root
directory.
plugins from npm, using `npm install --no-save --legacy-peer-deps
ep_flubberworm` in Etherpad's root directory.
You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will
list all installed plugins and those available on npm. It even provides

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) => {
let toc = [];
let depth = 0;
marked.setOptions({
headerIds: true,
headerPrefix: `${filename}_`,
});
lexed.forEach((tok) => {
if (tok.type !== 'heading') return;
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"
},
"dependencies": {
"marked": "1.1.1"
"marked": "^2.0.0"
},
"devDependencies": {},
"optionalDependencies": {},

View file

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

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)
## Installing
npm install [plugin_name]
```
npm install --no-save --legacy-peer-deps [plugin_name]
```
or Use the Etherpad ``/admin`` interface.

View file

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

View file

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

View file

@ -722,7 +722,7 @@ Example returns:
*/
exports.sendClientsMessage = async (padID, msg) => {
const pad = await getPadSafe(padID, true);
await getPadSafe(padID, true); // Throw if the padID is invalid or if the pad does not exist.
padMessageHandler.handleCustomMessage(padID, msg);
};

View file

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

View file

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

View file

@ -84,7 +84,8 @@ exports.require = (name, args, mod) => {
const cache = settings.maxAge !== 0;
const template = cache && templateCache.get(ejspath) || ejs.compile(
`<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
'<% e._init({get: () => __output, set: (s) => { __output = s; }}); %>' +
`${fs.readFileSync(ejspath).toString()}<% e._exit(); %>`,
{filename: ejspath});
if (cache) templateCache.set(ejspath, template);

View file

@ -669,7 +669,8 @@ const handleUserChanges = async (socket, message) => {
if (Changeset.oldLen(changeset) !== prevText.length) {
socket.json.send({disconnect: 'badChangeset'});
stats.meter('failedChangesets').mark();
throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ${Changeset.oldLen(changeset)} to document of length ${prevText.length}`);
throw new Error(`Can't apply USER_CHANGES ${changeset} with oldLen ` +
`${Changeset.oldLen(changeset)} to document of length ${prevText.length}`);
}
try {
@ -887,7 +888,8 @@ const handleClientReady = async (socket, message, authorID) => {
}
if (message.protocolVersion !== 2) {
messageLogger.warn(`Dropped message, CLIENT_READY Message has a unknown protocolVersion '${message.protocolVersion}'!`);
messageLogger.warn('Dropped message, CLIENT_READY Message has a unknown protocolVersion ' +
`'${message.protocolVersion}'!`);
return;
}
@ -1085,12 +1087,16 @@ const handleClientReady = async (socket, message, authorID) => {
indentationOnNewLine: settings.indentationOnNewLine,
scrollWhenFocusLineIsOutOfViewport: {
percentage: {
editionAboveViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
editionBelowViewport: settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
editionAboveViewport:
settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionAboveViewport,
editionBelowViewport:
settings.scrollWhenFocusLineIsOutOfViewport.percentage.editionBelowViewport,
},
duration: settings.scrollWhenFocusLineIsOutOfViewport.duration,
scrollWhenCaretIsInTheLastLineOfViewport: settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
percentageToScrollWhenUserPressesArrowUp: settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
scrollWhenCaretIsInTheLastLineOfViewport:
settings.scrollWhenFocusLineIsOutOfViewport.scrollWhenCaretIsInTheLastLineOfViewport,
percentageToScrollWhenUserPressesArrowUp:
settings.scrollWhenFocusLineIsOutOfViewport.percentageToScrollWhenUserPressesArrowUp,
},
initialChangesets: [], // FIXME: REMOVE THIS SHIT
};
@ -1380,7 +1386,8 @@ const composePadChangesets = async (padId, startNum, endNum) => {
// get all changesets
const changesets = {};
await Promise.all(changesetsNeeded.map(
(revNum) => pad.getRevisionChangeset(revNum).then((changeset) => changesets[revNum] = changeset)
(revNum) => pad.getRevisionChangeset(revNum)
.then((changeset) => changesets[revNum] = changeset)
));
// compose Changesets
@ -1406,7 +1413,7 @@ const _getRoomSockets = (padID) => {
const room = socketio.sockets.adapter.rooms[padID];
if (room) {
for (const id in room.sockets) {
for (const id of Object.keys(room.sockets)) {
roomSockets.push(socketio.sockets.sockets[id]);
}
}

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,8 +43,9 @@ let etherpadRoot = null;
*/
const popIfEndsWith = (stringArray, lastDesiredElements) => {
if (stringArray.length <= lastDesiredElements.length) {
absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" from "${stringArray.join(path.sep)}", it should contain at least ${lastDesiredElements.length + 1} elements`);
absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" ` +
`from "${stringArray.join(path.sep)}", it should contain at least ` +
`${lastDesiredElements.length + 1} elements`);
return false;
}
@ -54,7 +55,8 @@ const popIfEndsWith = (stringArray, lastDesiredElements) => {
return _.initial(stringArray, lastDesiredElements.length);
}
absPathLogger.debug(`${stringArray.join(path.sep)} does not end with "${lastDesiredElements.join(path.sep)}"`);
absPathLogger.debug(
`${stringArray.join(path.sep)} does not end with "${lastDesiredElements.join(path.sep)}"`);
return false;
};
@ -102,7 +104,8 @@ exports.findEtherpadRoot = () => {
}
if (maybeEtherpadRoot === false) {
absPathLogger.error(`Could not identity Etherpad base path in this ${process.platform} installation in "${foundRoot}"`);
absPathLogger.error('Could not identity Etherpad base path in this ' +
`${process.platform} installation in "${foundRoot}"`);
process.exit(1);
}
@ -113,7 +116,8 @@ exports.findEtherpadRoot = () => {
return etherpadRoot;
}
absPathLogger.error(`To run, Etherpad has to identify an absolute base path. This is not: "${etherpadRoot}"`);
absPathLogger.error(
`To run, Etherpad has to identify an absolute base path. This is not: "${etherpadRoot}"`);
process.exit(1);
};
@ -132,7 +136,7 @@ exports.makeAbsolute = (somePath) => {
return somePath;
}
const rewrittenPath = path.normalize(path.join(exports.findEtherpadRoot(), somePath));
const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath);
absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`);
return rewrittenPath;

View file

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

View file

@ -202,7 +202,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
if (line.listTypeName !== 'number') {
// 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];
}
}

View file

@ -22,6 +22,13 @@ const hooks = require('../../static/js/pluginfw/hooks');
exports.setPadRaw = (padId, 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) => {
let value = records[key];
@ -53,6 +60,17 @@ exports.setPadRaw = (padId, r) => {
} else {
// Not author data, probably 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(':');
// we know it's pad data

View file

@ -21,6 +21,7 @@
* limitations under the License.
*/
const assert = require('assert').strict;
const settings = require('./Settings');
const fs = require('fs').promises;
const path = require('path');
@ -32,7 +33,7 @@ const log4js = require('log4js');
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);
@ -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
* @param req the Express request
@ -99,20 +130,22 @@ const requestURIs = (locations, method, headers, callback) => {
*/
const minify = async (req, res) => {
let filename = req.params.filename;
// No relative paths, especially if they may go up the file hierarchy.
filename = path.normalize(path.join(ROOT_DIR, filename));
filename = filename.replace(/\.\./g, '');
if (filename.indexOf(ROOT_DIR) === 0) {
filename = filename.slice(ROOT_DIR.length);
filename = filename.replace(/\\/g, '/');
} else {
try {
filename = sanitizePathname(filename);
} catch (err) {
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
res.writeHead(404, {});
res.end();
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:
paths like "plugins/ep_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]) {
const plugin = plugins.plugins[library];
const pluginPath = plugin.package.realPath;
filename = path.relative(ROOT_DIR, pluginPath + libraryPath);
filename = filename.replace(/\\/g, '/'); // windows path fix
filename = path.relative(ROOT_DIR, path.join(pluginPath, libraryPath));
// 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) {
// Go straight into node_modules
// Avoid `require.resolve()`, since 'mustache' and 'mustache/index.js'
// 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.
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
const filenames = [];
@ -187,10 +226,7 @@ const getAceFile = async () => {
filenames.push(matches[2]);
}
}
// Always include the require kernel.
filenames.push('../static/js/require-kernel.js');
data += ';\n';
data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n';
// 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) => {
// Hostname "invalid.invalid" is a dummy value to allow parsing as a URI.
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?)
const [status, , body] = await requestURI(resourceURI, 'GET', {});
@ -234,7 +270,7 @@ const statFile = async (filename, dirStatLimit) => {
} else {
let stats;
try {
stats = await fs.stat(ROOT_DIR + filename);
stats = await fs.stat(path.join(ROOT_DIR, filename));
} catch (err) {
if (err.code === 'ENOENT') {
// Stat the directory instead.
@ -248,12 +284,12 @@ const statFile = async (filename, dirStatLimit) => {
};
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;
// 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
const files = await fs.readdir(path);
const files = await fs.readdir(dir);
// we wanna check the directory itself for changes too
files.push('.');
@ -261,7 +297,7 @@ const lastModifiedDateOfEverything = async () => {
// go through all files in this folder
await Promise.all(files.map(async (filename) => {
// 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
if (latestModification == null || stats.mtime > latestModification) {
@ -323,7 +359,7 @@ const getFileCompressed = async (filename, contentType) => {
const getFile = async (filename) => {
if (filename === 'js/ace.js') return await getAceFile();
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)));

View file

@ -32,11 +32,13 @@ exports.enforceMinNodeVersion = (minNodeVersion) => {
// we cannot use template literals, since we still do not know if we are
// running under Node >= 4.0
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);
}
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;
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 log4js = require('log4js');
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');
/* Root path of the installation */
exports.root = absolutePaths.findEtherpadRoot();
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}`);
console.log('All relative paths will be interpreted relative to the identified ' +
`Etherpad base dir: ${exports.root}`);
/**
* 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
*/
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
@ -474,7 +469,7 @@ const storeSettings = (settingsObj) => {
// we know this setting, so we overwrite it
// or it's a settings hash, specific to a plugin
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]);
} else {
exports[i] = settingsObj[i];
@ -601,7 +596,8 @@ const lookupEnvironmentVariables = (obj) => {
const defaultValue = match[3];
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
@ -611,7 +607,8 @@ const lookupEnvironmentVariables = (obj) => {
}
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);
}
@ -622,7 +619,8 @@ const lookupEnvironmentVariables = (obj) => {
* For numeric and boolean strings let's convert it to proper types before
* 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);
});
@ -676,7 +674,8 @@ const parseSettings = (settingsFilename, isSettings) => {
return replacedSettings;
} 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);
}
@ -703,7 +702,8 @@ exports.reloadSettings = () => {
log4js.replaceConsole();
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';
}
@ -713,24 +713,27 @@ exports.reloadSettings = () => {
const countPieces = exports.skinName.split(path.sep).length;
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';
}
// 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!
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';
skinPath = path.join(skinBasePath, exports.skinName);
}
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';
skinPath = path.join(skinBasePath, exports.skinName);
}
@ -745,7 +748,7 @@ exports.reloadSettings = () => {
if (!exists) {
const abiwordError = 'Abiword does not exist at this path, check your settings file.';
if (!exports.suppressErrorsInPadText) {
exports.defaultPadText = `${exports.defaultPadText}\nError: ${abiwordError}${suppressDisableMsg}`;
exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`;
}
console.error(`${abiwordError} File location: ${exports.abiword}`);
exports.abiword = null;
@ -757,10 +760,11 @@ exports.reloadSettings = () => {
if (exports.soffice) {
fs.exists(exports.soffice, (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) {
exports.defaultPadText = `${exports.defaultPadText}\nError: ${sofficeError}${suppressDisableMsg}`;
exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`;
}
console.error(`${sofficeError} File location: ${exports.soffice}`);
exports.soffice = null;
@ -774,18 +778,22 @@ exports.reloadSettings = () => {
exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8');
console.info(`Session key loaded from: ${sessionkeyFilename}`);
} 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);
fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8');
}
} 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') {
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.';
if (!exports.suppressErrorsInPadText) {
exports.defaultPadText = `${exports.defaultPadText}\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`;
}
exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename);
@ -794,8 +802,23 @@ exports.reloadSettings = () => {
if (exports.ip === '') {
// 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

View file

@ -44,7 +44,7 @@ try {
_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;
const responseCache = {};

View file

@ -28,65 +28,130 @@ const logLines = (readable, logLineFn) => {
};
/**
* Similar to `util.promisify(child_rocess.exec)`, except:
* - `cwd` defaults to the Etherpad root directory.
* - PATH is prefixed with src/node_modules/.bin so that utilities from installed dependencies
* (e.g., npm) are preferred over system utilities.
* - Output is passed to logger callback functions by default. See below for details.
* Runs a command, logging its output to Etherpad's logs by default.
*
* Examples:
*
* 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 opts Optional options that will be passed to `child_process.spawn()` with two extensions:
* - `stdoutLogger`: Callback that is called each time a line of text is written to stdout (utf8
* is assumed). The line (without trailing newline) is passed as the only argument. If null,
* stdout is not logged. If unset, defaults to no-op. Ignored if stdout is not a pipe.
* - `stderrLogger`: Like `stdoutLogger` but for stderr.
* @param opts As with `child_process.spawn()`, except:
* - `cwd` defaults to the Etherpad root directory.
* - `env.PATH` is prefixed with `src/node_modules/.bin:node_modules/.bin` so that utilities from
* installed dependencies (e.g., npm) are preferred over system utilities.
* - 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,
* stderr stream, and ChildProcess objects, respectively.
* @returns A Promise that resolves when the command exits. The Promise resolves to the complete
* 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 = {}) => {
logger.debug(`Executing command: ${args.join(' ')}`);
const {stdoutLogger = () => {}, stderrLogger = () => {}} = opts;
// Avoid confusing child_process.spawn() with our extensions.
opts = {...opts}; // Make a copy to avoid mutating the caller's copy.
delete opts.stdoutLogger;
delete opts.stderrLogger;
opts = {cwd: settings.root, ...opts};
logger.debug(`cwd: ${opts.cwd}`);
// 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
// (global) utilities.
let {env = process.env} = opts;
env = {...env}; // Copy to avoid modifying process.env.
// On Windows the PATH environment var might be spelled "Path".
const pathVarName = Object.keys(env).filter((k) => k.toUpperCase() === 'PATH')[0] || 'PATH';
env[pathVarName] = [
const {env = process.env} = opts;
const {[pathVarName]: PATH} = env;
opts.env = {
...env, // Copy env to avoid modifying process.env or the caller's supplied env.
[pathVarName]: [
path.join(settings.root, 'src', 'node_modules', '.bin'),
path.join(settings.root, 'node_modules', '.bin'),
...(env[pathVarName] ? env[pathVarName].split(path.delimiter) : []),
].join(path.delimiter);
logger.debug(`${pathVarName}=${env[pathVarName]}`);
...(PATH ? PATH.split(path.delimiter) : []),
].join(path.delimiter),
};
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
// 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});
if (proc.stdout != null && stdoutLogger != null) logLines(proc.stdout, stdoutLogger);
if (proc.stderr != null && stderrLogger != null) logLines(proc.stderr, stderrLogger);
const p = new Promise((resolve, reject) => {
proc.on('exit', (code, signal) => {
const proc = spawn(args[0], args.slice(1), opts);
const streams = [undefined, proc.stdout, proc.stderr];
let px;
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) {
logger.debug(procFailedErr.stack);
procFailedErr.message =
`Command exited ${code ? `with code ${code}` : `on signal ${signal}`}: ${args.join(' ')}`;
procFailedErr.code = code;
procFailedErr.signal = signal;
return reject(procFailedErr);
logger.debug(procFailedErr.stack);
return px.reject(procFailedErr);
}
logger.debug(`Command returned successfully: ${args.join(' ')}`);
resolve();
px.resolve(stdout);
});
});
p.stdout = proc.stdout;
p.stderr = proc.stderr;
p.child = proc;
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_utils.js"
, "browser.js"
, "vendors/browser.js"
, "pad_cookie.js"
, "pad_editor.js"
, "pad_editbar.js"
@ -16,10 +16,10 @@
, "pad_savedrevs.js"
, "pad_connectionstatus.js"
, "chat.js"
, "gritter.js"
, "vendors/gritter.js"
, "$js-cookie/src/js.cookie.js"
, "$tinycon/tinycon.js"
, "farbtastic.js"
, "vendors/farbtastic.js"
, "skin_variants.js"
, "socketio.js"
]
@ -28,7 +28,7 @@
, "colorutils.js"
, "draggable.js"
, "pad_utils.js"
, "browser.js"
, "vendors/browser.js"
, "pad_cookie.js"
, "pad_editor.js"
, "pad_editbar.js"
@ -49,7 +49,7 @@
]
, "ace2_inner.js": [
"ace2_inner.js"
, "browser.js"
, "vendors/browser.js"
, "AttributePool.js"
, "Changeset.js"
, "ChangesetUtils.js"
@ -68,11 +68,11 @@
]
, "ace2_common.js": [
"ace2_common.js"
, "browser.js"
, "jquery.js"
, "vendors/browser.js"
, "vendors/jquery.js"
, "rjquery.js"
, "$async.js"
, "underscore.js"
, "vendors/underscore.js"
, "$underscore.js"
, "$underscore/underscore.js"
, "security.js"
@ -82,4 +82,5 @@
, "pluginfw/shared.js"
, "pluginfw/hooks.js"
]
, "jquery.js": ["jquery.js"]
}

286
src/package-lock.json generated
View file

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

View file

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

View file

@ -70,3 +70,10 @@ input {
@media (max-width: 800px) {
.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-compress:before { content: '\e83f'; } /* '' */
.buttonicon-expand:before { content: '\e840'; } /* '' */
.buttonicon-spin5:before { content: '\e841'; } /* '' */
.buttonicon-eye-slash:before { content: '\e843'; } /* '' */
.buttonicon-list-ol:before { content: '\e844'; } /* '' */
.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 ChangesetUtils = require('./ChangesetUtils');
const _ = require('./underscore');
const _ = require('./vendors/underscore');
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[0] !== selEnd[0]) { // -> More than one line selected
let hasAttrib = true;
// 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]);
// for all lines in between
@ -406,7 +404,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
hasAttrib = this.getAttributeOnSelection(attributeName);
} else {
const attributesOnCaretPosition = this.getAttributesOnCaret();
hasAttrib = _.contains(_.flatten(attributesOnCaretPosition), attributeName);
const allAttribs = [].concat(...attributesOnCaretPosition); // flatten
hasAttrib = allAttribs.includes(attributeName);
}
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
* based on a SkipList
@ -18,7 +20,7 @@
* See the License for the specific language governing permissions and
* 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 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 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]);
builder.keep(startLineOffset, start[0]);

View file

@ -24,8 +24,6 @@
// requires: top
// requires: undefined
const KERNEL_SOURCE = '../static/js/require-kernel.js';
const hooks = require('./pluginfw/hooks');
const pluginUtils = require('./pluginfw/shared');
@ -196,15 +194,10 @@ const Ace2Editor = function () {
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`);
pushStyleTagsFor(iframeHTML, includedCSS);
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(`<script type="text/javascript" src="../static/js/require-kernel.js?v=${clientVars.randomVersionString}"></script>`);
iframeHTML.push(scriptTag(
`${Ace2Editor.EMBEDED[KERNEL_SOURCE]}\n\
`\n\
require.setRootURI("../javascripts/src");\n\
require.setLibraryURI("../javascripts/lib");\n\
require.setGlobalKeyPath("require");\n\

View file

@ -18,7 +18,7 @@
*/
let documentAttributeManager;
const browser = require('./browser');
const browser = require('./vendors/browser');
const padutils = require('./pad_utils').padutils;
const Ace2Common = require('./ace2_common');
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
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,
// 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 = [];
// 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 currentLine = 0;
let h = null;
@ -3811,7 +3828,21 @@ function Ace2Inner() {
// last line
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;
currentLine++;
}
@ -3823,8 +3854,9 @@ function Ace2Inner() {
// Apply height to existing sidediv lines
currentLine = 0;
while (sidebarLine && currentLine <= lineNumbersShown) {
if (lineHeights[currentLine] != null) {
sidebarLine.style.height = `${lineHeights[currentLine]}px`;
if (lineOffsets[currentLine] != null) {
sidebarLine.style.height = `${lineOffsets[currentLine]}px`;
sidebarLine.style.lineHeight = `${lineHeights[currentLine]}px`;
}
sidebarLine = sidebarLine.nextSibling;
currentLine++;
@ -3839,8 +3871,9 @@ function Ace2Inner() {
while (lineNumbersShown < newNumLines) {
lineNumbersShown++;
const div = odoc.createElement('DIV');
if (lineHeights[currentLine]) {
div.style.height = `${lineHeights[currentLine]}px`;
if (lineOffsets[currentLine]) {
div.style.height = `${lineOffsets[currentLine]}px`;
div.style.lineHeight = `${lineHeights[currentLine]}px`;
}
$(div).append($(`<span class='line-number'>${String(lineNumbersShown)}</span>`));
fragment.appendChild(div);

View file

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

View file

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

View file

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

View file

@ -4,37 +4,17 @@
// This function is useful to get the caret position of the line as
// is represented by the browser
exports.getPosition = () => {
let rect, line;
const range = getSelectionRange();
const isSelectionInsideTheEditor = range &&
$(range.endContainer).closest('body')[0].id === 'innerdocbody';
if (isSelectionInsideTheEditor) {
// 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) {
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
// 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
// the dimensions on the position.
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('|'));
clonedRange.insertNode(shadowCaret[0]);
clonedRange.selectNode(shadowCaret[0]);
line = getPositionOfElementOrSelection(clonedRange);
clonedRange.detach();
const line = getPositionOfElementOrSelection(clonedRange);
shadowCaret.remove();
}
}
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 helps other people to understand this code better and helps them to improve it.
@ -23,7 +25,7 @@
const AttributePool = require('./AttributePool');
const Changeset = require('./Changeset');
function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// latest official text from server
let baseAText = Changeset.makeAText('\n');
// changes applied to baseText that have been submitted
@ -42,30 +44,30 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
let changeCallbackTimeout = null;
function setChangeCallbackTimeout() {
const setChangeCallbackTimeout = () => {
// can call this multiple times per call-stack, because
// we only schedule a call to changeCallback if it exists
// and if there isn't a timeout already scheduled.
if (changeCallback && changeCallbackTimeout === null) {
if (changeCallback && changeCallbackTimeout == null) {
changeCallbackTimeout = scheduler.setTimeout(() => {
try {
changeCallback();
} catch (pseudoError) {} finally {
} catch (pseudoError) {
// as empty as my soul
} finally {
changeCallbackTimeout = null;
}
}, 0);
}
}
};
let self;
return self = {
isTracking() {
return tracking;
},
setBaseText(text) {
isTracking: () => tracking,
setBaseText: (text) => {
self.setBaseAttributedText(Changeset.makeAText(text), null);
},
setBaseAttributedText(atext, apoolJsonObj) {
setBaseAttributedText: (atext, apoolJsonObj) => {
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
tracking = true;
baseAText = Changeset.cloneAText(atext);
@ -83,7 +85,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
}
});
},
composeUserChangeset(c) {
composeUserChangeset: (c) => {
if (!tracking) return;
if (applyingNonUserChanges) return;
if (Changeset.isIdentity(c)) return;
@ -91,7 +93,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
setChangeCallbackTimeout();
},
applyChangesToBase(c, optAuthor, apoolJsonObj) {
applyChangesToBase: (c, optAuthor, apoolJsonObj) => {
if (!tracking) return;
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
@ -111,8 +113,10 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
const preferInsertingAfterUserChanges = true;
const oldUserChangeset = userChangeset;
userChangeset = Changeset.follow(c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
const postChange = Changeset.follow(oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
userChangeset = Changeset.follow(
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
const postChange = Changeset.follow(
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
applyingNonUserChanges = true;
@ -123,7 +127,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
}
});
},
prepareUserChangeset() {
prepareUserChangeset: () => {
// If there are user changes to submit, 'changeset' will be the
// changeset, else it will be null.
let toSubmit;
@ -135,7 +139,9 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
// add forEach function to Array.prototype for IE8
if (!('forEach' in Array.prototype)) {
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;
// 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) {
let authorAttr;
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
var cs = Changeset.unpack(userChangeset);
const cs = Changeset.unpack(userChangeset);
const iterator = Changeset.opIterator(cs.ops);
let op;
const assem = Changeset.mergingOpAssembler();
while (iterator.hasNext()) {
op = iterator.next();
if (op.opcode == '+') {
var newAttrs = '';
if (op.opcode === '+') {
let newAttrs = '';
op.attribs.split('*').forEach((attrNum) => {
if (!attrNum) return;
const attr = apool.getAttrib(parseInt(attrNum, 36));
if (!attr) return;
if ('author' == attr[0]) {
if ('author' === attr[0]) {
// replace that author with the current one
newAttrs += `*${authorAttr}`;
} else { newAttrs += `*${attrNum}`; } // overtake all other attribs as is
@ -181,7 +192,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
else toSubmit = userChangeset;
}
var cs = null;
let cs = null;
if (toSubmit) {
submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
@ -201,7 +212,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
};
return data;
},
applyPreparedChangesetToBase() {
applyPreparedChangesetToBase: () => {
if (!submittedChangeset) {
// violation of protocol; use prepareUserChangeset first
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
@ -210,13 +221,11 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
submittedChangeset = null;
},
setUserChangeNotificationCallback(callback) {
setUserChangeNotificationCallback: (callback) => {
changeCallback = callback;
},
hasUncommittedChanges() {
return !!(submittedChangeset || (!Changeset.isIdentity(userChangeset)));
},
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
};
}
};
exports.makeChangesetTracker = makeChangesetTracker;

View file

@ -24,7 +24,7 @@
const chat = require('./chat').chat;
const hooks = require('./pluginfw/hooks');
const browser = require('./browser');
const browser = require('./vendors/browser');
// Dependency fill on init. This exists for `pad.socket` only.
// 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.
ACE's ready callback does not need to have fired yet.
"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;
pad = _pad; // Inject pad to avoid a circular dependency.
@ -583,6 +583,6 @@ function getCollabClient(ace2editor, serverVars, initialUserInfo, options, _pad)
setUpSocket();
return self;
}
};
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;
} else {
state.lineAttributes.list = listType;
@ -315,6 +315,10 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
const localAttribs = state.localAttribs;
state.localAttribs = null;
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);
if (isBlock) _ensureColumnZero(state);
const startLine = lines.length() - 1;

View file

@ -24,7 +24,7 @@
const Security = require('./security');
const hooks = require('./pluginfw/hooks');
const _ = require('./underscore');
const _ = require('./vendors/underscore');
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
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
/**
* Copyright 2011 Peter Martischka, Primary Technology.
@ -16,28 +19,26 @@
* limitations under the License.
*/
/* global $, customStart */
function randomPadName() {
const randomPadName = () => {
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
// using the PRNG below
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_';
// 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.
const randomarray = new Uint8Array(string_length);
const randomarray = new Uint8Array(stringLength);
// use browser's PRNG to generate a "unique" sequence
const cryptoObj = window.crypto || window.msCrypto; // for IE 11
cryptoObj.getRandomValues(randomarray);
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)"
// we can save some cycles.
const rnum = Math.floor(randomarray[i] / 4);
randomstring += chars.substring(rnum, rnum + 1);
}
return randomstring;
}
};
$(() => {
$('#go2Name').submit(() => {
@ -55,7 +56,7 @@ $(() => {
});
// start the custom js
if (typeof customStart === 'function') customStart();
if (typeof window.customStart === 'function') window.customStart();
});
// @license-end

View file

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

View file

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

View file

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

View file

@ -18,15 +18,14 @@
const padutils = require('./pad_utils').padutils;
const hooks = require('./pluginfw/hooks');
const browser = require('./browser');
let myUserInfo = {};
let colorPickerOpen = false;
let colorPickerSetup = false;
const paduserlist = (function () {
const rowManager = (function () {
const paduserlist = (() => {
const rowManager = (() => {
// The row manager handles rendering rows of the user list and animating
// their insertion, removal, and reordering. It manipulates TD height
// and TD opacity.
@ -291,7 +290,7 @@ const paduserlist = (function () {
updateRow,
};
return self;
}()); // //////// rowManager
})(); // //////// rowManager
const otherUsersInfo = [];
const otherUsersData = [];
@ -347,7 +346,7 @@ const paduserlist = (function () {
let pad = undefined;
const self = {
init(myInitialUserInfo, _pad) {
init: (myInitialUserInfo, _pad) => {
pad = _pad;
self.setMyUserInfo(myInitialUserInfo);
@ -544,7 +543,7 @@ const paduserlist = (function () {
},
};
return self;
}());
})();
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,
// which appears to fix the issue.
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.parts = data.parts;
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@
*/
const Changeset = require('./Changeset');
const _ = require('./underscore');
const _ = require('./vendors/underscore');
const undoModule = (() => {
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
* 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
// Original can be found at:
// 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
* 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) 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
https://github.com/hernansartorio/jquery-nice-select
Made by Hernán Sartorio */
@ -60,14 +63,14 @@
.addClass($select.attr('class') || '')
.addClass($select.attr('disabled') ? 'disabled' : '')
.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 $options = $select.find('option');
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) {
var $option = $(this);
@ -94,31 +97,12 @@
var $dropdown = $(this);
$('.nice-select').not($dropdown).removeClass('open');
$dropdown.toggleClass('open');
if ($dropdown.hasClass('open')) {
$dropdown.find('.option');
$dropdown.find('.focus').removeClass('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 {
$dropdown.focus();
}

View file

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

View file

@ -1,5 +1,7 @@
function customStart() {
'use strict';
window.customStart = () => {
// define your javascript here
// jquery is available - except index.js
// 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();
$('.buttonicon').mousedown(function () { $(this).parent().addClass('pressed'); });
$('.buttonicon').mouseup(function () { $(this).parent().removeClass('pressed'); });
}
};

View file

@ -12,8 +12,20 @@
}
#sidedivinner>div .line-number {
line-height: 24px;
line-height: inherit;
font-family: RobotoMono;
display: inline-block;
color: #576273;
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
// jquery is available - except index.js
// 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
// jquery is available - except index.js
// 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
// jquery is available - except index.js
// you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
}
};

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<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">
body {
font-size:9pt;

View file

@ -4,10 +4,10 @@
<title data-l10n-id="admin.page-title">Admin Dashboard - Etherpad</title>
<meta name="viewport" content="width=device-width">
<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>
<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>
</head>
<body>

View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="../../static/css/admin.css">
<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>
</head>
<body>

View file

@ -4,12 +4,12 @@
<title data-l10n-id="admin_plugins.page-title">Plugin manager - Etherpad</title>
<meta name="viewport" content="width=device-width">
<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="../static/js/socketio.js"></script>
<script src="../static/js/admin/plugins.js"></script>
<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>
</head>
<body>

View file

@ -4,14 +4,14 @@
<title data-l10n-id="admin_settings.page-title">Settings - Etherpad</title>
<meta name="viewport" content="width=device-width">
<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="../static/js/socketio.js"></script>
<script src="../static/js/admin/minify.json.js"></script>
<script src="../static/js/admin/settings.js"></script>
<script src="../static/js/admin/jquery.autosize.js"></script>
<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>
</head>
<body>

View file

@ -11,9 +11,9 @@
<link rel="shortcut icon" href="<%=settings.favicon%>">
<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 src="static/js/jquery.js"></script>
<script src="static/js/vendors/jquery.js"></script>
<script src="static/js/index.js"></script>
<style>

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