mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-21 07:56:16 -04:00
Merge branch 'develop'
This commit is contained in:
commit
b99c2cae22
31 changed files with 528 additions and 306 deletions
3
.github/workflows/frontend-admin-tests.yml
vendored
3
.github/workflows/frontend-admin-tests.yml
vendored
|
@ -1,4 +1,5 @@
|
||||||
name: "Frontend admin tests"
|
# Leave the powered by Sauce Labs bit in as this means we get additional concurrency
|
||||||
|
name: "Frontend admin tests powered by Sauce Labs"
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
|
||||||
|
|
8
.github/workflows/frontend-tests.yml
vendored
8
.github/workflows/frontend-tests.yml
vendored
|
@ -1,4 +1,5 @@
|
||||||
name: "Frontend tests"
|
# Leave the powered by Sauce Labs bit in as this means we get additional concurrency
|
||||||
|
name: "Frontend tests powered by Sauce Labs"
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json
|
sed -e '/^ *"importExportRateLimiting":/,/^ *\}/ s/"max":.*/"max": 0/' -i settings.json
|
||||||
|
|
||||||
- uses: saucelabs/sauce-connect-action@v1.1.2
|
- uses: saucelabs/sauce-connect-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.SAUCE_USERNAME }}
|
username: ${{ secrets.SAUCE_USERNAME }}
|
||||||
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
|
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||||
|
@ -78,6 +79,7 @@ jobs:
|
||||||
ep_align
|
ep_align
|
||||||
ep_author_hover
|
ep_author_hover
|
||||||
ep_cursortrace
|
ep_cursortrace
|
||||||
|
ep_embedmedia
|
||||||
ep_font_size
|
ep_font_size
|
||||||
ep_hash_auth
|
ep_hash_auth
|
||||||
ep_headings2
|
ep_headings2
|
||||||
|
@ -114,7 +116,7 @@ jobs:
|
||||||
- name: Remove standard frontend test files, so only plugin tests are run
|
- name: Remove standard frontend test files, so only plugin tests are run
|
||||||
run: rm src/tests/frontend/specs/*
|
run: rm src/tests/frontend/specs/*
|
||||||
|
|
||||||
- uses: saucelabs/sauce-connect-action@v1.1.2
|
- uses: saucelabs/sauce-connect-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.SAUCE_USERNAME }}
|
username: ${{ secrets.SAUCE_USERNAME }}
|
||||||
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
|
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
|
||||||
|
|
32
.github/workflows/load-test.yml
vendored
32
.github/workflows/load-test.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
run: sudo npm install -g etherpad-load-test
|
run: sudo npm install -g etherpad-load-test
|
||||||
|
|
||||||
- name: Run load test
|
- name: Run load test
|
||||||
run: src/tests/frontend/travis/runnerLoadTest.sh
|
run: src/tests/frontend/travis/runnerLoadTest.sh 25 50
|
||||||
|
|
||||||
withplugins:
|
withplugins:
|
||||||
# run on pushes to any branch
|
# run on pushes to any branch
|
||||||
|
@ -80,4 +80,32 @@ jobs:
|
||||||
|
|
||||||
# configures some settings and runs npm run test
|
# configures some settings and runs npm run test
|
||||||
- name: Run load test
|
- name: Run load test
|
||||||
run: src/tests/frontend/travis/runnerLoadTest.sh
|
run: src/tests/frontend/travis/runnerLoadTest.sh 25 50
|
||||||
|
|
||||||
|
long:
|
||||||
|
# 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: long running
|
||||||
|
runs-on: ubuntu-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: src/bin/installDeps.sh
|
||||||
|
|
||||||
|
- name: Install etherpad-load-test
|
||||||
|
run: sudo npm install -g etherpad-load-test
|
||||||
|
|
||||||
|
|
||||||
|
# configures some settings and runs npm run test
|
||||||
|
- name: Run load test
|
||||||
|
run: src/tests/frontend/travis/runnerLoadTest.sh 5000 5
|
||||||
|
|
83
.github/workflows/major-version-git-pull-update.yml
vendored
Normal file
83
.github/workflows/major-version-git-pull-update.yml
vendored
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
name: "In-place git pull from master"
|
||||||
|
|
||||||
|
# any branch is useful for testing before a PR is submitted
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
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: Linux with Plugins
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
node: [10, 12, 14, 15]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout master repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
|
||||||
|
- 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: src/bin/installDeps.sh
|
||||||
|
|
||||||
|
- name: Run the backend tests
|
||||||
|
run: cd src && npm test
|
||||||
|
|
||||||
|
- name: Git fetch
|
||||||
|
run: git fetch
|
||||||
|
|
||||||
|
- name: Checkout this branch over master
|
||||||
|
run: git checkout "${GITHUB_SHA}"
|
||||||
|
|
||||||
|
- name: Install all dependencies and symlink for ep_etherpad-lite
|
||||||
|
run: src/bin/installDeps.sh
|
||||||
|
|
||||||
|
- name: Run the backend tests
|
||||||
|
run: cd src && npm test
|
||||||
|
|
||||||
|
- name: Install Cypress
|
||||||
|
run: npm install cypress -g
|
||||||
|
|
||||||
|
- name: Run Etherpad & Test Frontend
|
||||||
|
run: |
|
||||||
|
node src/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
|
||||||
|
cd src/tests/frontend
|
||||||
|
cypress run --spec cypress/integration/test.js --config-file cypress/cypress.json
|
6
.github/workflows/windows-zip.yml
vendored
6
.github/workflows/windows-zip.yml
vendored
|
@ -65,11 +65,13 @@ jobs:
|
||||||
- name: Extract Etherpad
|
- name: Extract Etherpad
|
||||||
run: 7z x etherpad-lite-win.zip -oetherpad
|
run: 7z x etherpad-lite-win.zip -oetherpad
|
||||||
|
|
||||||
- name: list
|
- name: Install Cypress
|
||||||
run: dir etherpad
|
run: npm install cypress -g
|
||||||
|
|
||||||
- name: Run Etherpad
|
- name: Run Etherpad
|
||||||
run: |
|
run: |
|
||||||
cd etherpad
|
cd etherpad
|
||||||
node node_modules\ep_etherpad-lite\node\server.js &
|
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
|
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
|
||||||
|
cd src\tests\frontend
|
||||||
|
cypress run --spec cypress\integration\test.js --config-file cypress\cypress.json
|
||||||
|
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,3 +1,28 @@
|
||||||
|
# 1.8.12
|
||||||
|
|
||||||
|
Special mention: Thanks to Sauce Labs for additional testing tunnels to help us grow! :)
|
||||||
|
|
||||||
|
### Security patches
|
||||||
|
|
||||||
|
* Fixed a regression in v1.8.11 which caused some pad names to cause Etherpad to restart.
|
||||||
|
|
||||||
|
### Notable fixes
|
||||||
|
|
||||||
|
* Fixed a bug in the `dirty` database driver that sometimes caused Node.js to
|
||||||
|
crash during shutdown and lose buffered database writes.
|
||||||
|
* Fixed a regression in v1.8.8 that caused "Uncaught TypeError: Cannot read
|
||||||
|
property '0' of undefined" with some plugins (#4885)
|
||||||
|
* Less warnings in server console for supported element types on import.
|
||||||
|
* Support Azure and other network share installations by using a
|
||||||
|
more truthful relative path.
|
||||||
|
|
||||||
|
### Notable enhancements
|
||||||
|
|
||||||
|
* Dependency updates
|
||||||
|
* Various Docker deployment improvements
|
||||||
|
* Various new translations
|
||||||
|
* Improvement of rendering of plugin hook list and error message handling
|
||||||
|
|
||||||
# 1.8.11
|
# 1.8.11
|
||||||
|
|
||||||
### Notable fixes
|
### Notable fixes
|
||||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -40,9 +40,20 @@ ENV NODE_ENV=production
|
||||||
#
|
#
|
||||||
# Running as non-root enables running this image in platforms like OpenShift
|
# Running as non-root enables running this image in platforms like OpenShift
|
||||||
# that do not allow images running as root.
|
# that do not allow images running as root.
|
||||||
RUN useradd --uid 5001 --create-home etherpad
|
#
|
||||||
|
# If any of the following args are set to the empty string, default
|
||||||
|
# values will be chosen.
|
||||||
|
ARG EP_HOME=
|
||||||
|
ARG EP_UID=5001
|
||||||
|
ARG EP_GID=0
|
||||||
|
ARG EP_SHELL=
|
||||||
|
RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \
|
||||||
|
useradd --system ${EP_UID:+--uid "${EP_UID}" --non-unique} --gid etherpad \
|
||||||
|
${EP_HOME:+--home-dir "${EP_HOME}"} --create-home \
|
||||||
|
${EP_SHELL:+--shell "${EP_SHELL}"} etherpad
|
||||||
|
|
||||||
RUN mkdir /opt/etherpad-lite && chown etherpad:0 /opt/etherpad-lite
|
ARG EP_DIR=/opt/etherpad-lite
|
||||||
|
RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}"
|
||||||
|
|
||||||
# install abiword for DOC/PDF/ODT export
|
# install abiword for DOC/PDF/ODT export
|
||||||
RUN [ -z "${INSTALL_ABIWORD}" ] || (apt update && apt -y install abiword && apt clean && rm -rf /var/lib/apt/lists/*)
|
RUN [ -z "${INSTALL_ABIWORD}" ] || (apt update && apt -y install abiword && apt clean && rm -rf /var/lib/apt/lists/*)
|
||||||
|
@ -53,24 +64,20 @@ RUN [ -z "${INSTALL_SOFFICE}" ] || (apt update && mkdir -p /usr/share/man/man1 &
|
||||||
|
|
||||||
USER etherpad
|
USER etherpad
|
||||||
|
|
||||||
WORKDIR /opt/etherpad-lite
|
WORKDIR "${EP_DIR}"
|
||||||
|
|
||||||
COPY --chown=etherpad:0 ./ ./
|
COPY --chown=etherpad:etherpad ./ ./
|
||||||
|
|
||||||
# install node dependencies for Etherpad
|
# install node dependencies for Etherpad
|
||||||
RUN src/bin/installDeps.sh && \
|
RUN src/bin/installDeps.sh && \
|
||||||
rm -rf ~/.npm/_cacache
|
rm -rf ~/.npm/_cacache
|
||||||
|
|
||||||
# Install the plugins, if ETHERPAD_PLUGINS is not empty.
|
RUN [ -z "${ETHERPAD_PLUGINS}" ] || npm install ${ETHERPAD_PLUGINS}
|
||||||
#
|
|
||||||
# Bash trick: in the for loop ${ETHERPAD_PLUGINS} is NOT quoted, in order to be
|
|
||||||
# able to split at spaces.
|
|
||||||
RUN for PLUGIN_NAME in ${ETHERPAD_PLUGINS}; do npm install "${PLUGIN_NAME}" || exit 1; done
|
|
||||||
|
|
||||||
# Copy the configuration file.
|
# Copy the configuration file.
|
||||||
COPY --chown=etherpad:0 ./settings.json.docker /opt/etherpad-lite/settings.json
|
COPY --chown=etherpad:etherpad ./settings.json.docker "${EP_DIR}"/settings.json
|
||||||
|
|
||||||
# Fix permissions for root group
|
# Fix group permissions
|
||||||
RUN chmod -R g=u .
|
RUN chmod -R g=u .
|
||||||
|
|
||||||
EXPOSE 9001
|
EXPOSE 9001
|
||||||
|
|
|
@ -23,10 +23,10 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
[](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml)
|
[](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/windows-zip.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml)
|
||||||
[](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml)
|
[](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) [](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) [](https://saucelabs.com/u/etherpad) [](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml)
|
||||||
|
|
||||||
### Engagement
|
### 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>    
|
<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> [](https://discord.com/invite/daEjfhw) [](https://static.etherpad.org/index.html)  
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
|
|
20
src/bin/plugins/stalePlugins.js
Normal file
20
src/bin/plugins/stalePlugins.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Returns a list of stale plugins and their authors email
|
||||||
|
|
||||||
|
const superagent = require('superagent');
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
const res = await superagent.get('https://static.etherpad.org/plugins.full.json');
|
||||||
|
const plugins = JSON.parse(res.text);
|
||||||
|
for (const plugin of Object.keys(plugins)) {
|
||||||
|
const name = plugins[plugin].data.name;
|
||||||
|
const date = new Date(plugins[plugin].time);
|
||||||
|
const diffTime = Math.abs(currentTime - date);
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
if (diffDays > (365*2)) {
|
||||||
|
console.log(`${name}, ${plugins[plugin].data.maintainers[0].email}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
|
@ -9,10 +9,12 @@
|
||||||
"Upwinxp"
|
"Upwinxp"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"admin_plugins.description": "Opis",
|
||||||
"admin_plugins.last-update": "Zadnja posodobitev",
|
"admin_plugins.last-update": "Zadnja posodobitev",
|
||||||
"admin_plugins.name": "Ime",
|
"admin_plugins.name": "Ime",
|
||||||
"admin_plugins.version": "Različica",
|
"admin_plugins.version": "Različica",
|
||||||
"admin_settings": "Nastavitve",
|
"admin_settings": "Nastavitve",
|
||||||
|
"admin_settings.current_save.value": "Shrani nastavitve",
|
||||||
"index.newPad": "Nov dokument",
|
"index.newPad": "Nov dokument",
|
||||||
"index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:",
|
"index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:",
|
||||||
"pad.toolbar.bold.title": "Krepko (Ctrl-B)",
|
"pad.toolbar.bold.title": "Krepko (Ctrl-B)",
|
||||||
|
|
|
@ -26,8 +26,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
epVersion,
|
epVersion,
|
||||||
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
|
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
|
||||||
installedParts: `<pre>${plugins.formatParts()}</pre>`,
|
installedParts: `<pre>${plugins.formatParts()}</pre>`,
|
||||||
installedServerHooks: `<div>${plugins.formatHooks()}</div>`,
|
installedServerHooks: `<div>${plugins.formatHooks('hooks', true)}</div>`,
|
||||||
installedClientHooks: `<div>${plugins.formatHooks('client_hooks')}</div>`,
|
installedClientHooks: `<div>${plugins.formatHooks('client_hooks', true)}</div>`,
|
||||||
latestVersion: UpdateCheck.getLatestVersion(),
|
latestVersion: UpdateCheck.getLatestVersion(),
|
||||||
req,
|
req,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -18,7 +18,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
// the pad id was sanitized, so we redirect to the sanitized version
|
// the pad id was sanitized, so we redirect to the sanitized version
|
||||||
const realURL = encodeURIComponent(sanitizedPadId) + new URL(req.url).search;
|
const realURL = encodeURIComponent(sanitizedPadId) + new URL(req.url, 'http://invalid.invalid').search;
|
||||||
res.header('Location', realURL);
|
res.header('Location', realURL);
|
||||||
res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
|
res.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@ exports.start = async () => {
|
||||||
.join(', ');
|
.join(', ');
|
||||||
logger.info(`Installed plugins: ${installedPlugins}`);
|
logger.info(`Installed plugins: ${installedPlugins}`);
|
||||||
logger.debug(`Installed parts:\n${plugins.formatParts()}`);
|
logger.debug(`Installed parts:\n${plugins.formatParts()}`);
|
||||||
logger.debug(`Installed hooks:\n${plugins.formatHooks()}`);
|
logger.debug(`Installed server-side hooks:\n${plugins.formatHooks('hooks', false)}`);
|
||||||
await hooks.aCallAll('loadSettings', {settings});
|
await hooks.aCallAll('loadSettings', {settings});
|
||||||
await hooks.aCallAll('createServer');
|
await hooks.aCallAll('createServer');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -18,15 +18,14 @@
|
||||||
|
|
||||||
const db = require('../db/DB');
|
const db = require('../db/DB');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
|
const supportedElems = require('../../static/js/contentcollector').supportedElems;
|
||||||
|
|
||||||
exports.setPadRaw = (padId, r) => {
|
exports.setPadRaw = (padId, r) => {
|
||||||
const records = JSON.parse(r);
|
const records = JSON.parse(r);
|
||||||
|
|
||||||
const blockElems = ['div', 'br', 'p', 'pre', 'li', 'author', 'lmkr', 'insertorder'];
|
|
||||||
|
|
||||||
// get supported block Elements from plugins, we will use this later.
|
// get supported block Elements from plugins, we will use this later.
|
||||||
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
|
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
|
||||||
blockElems.push(element);
|
supportedElems.push(element);
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(records).forEach(async (key) => {
|
Object.keys(records).forEach(async (key) => {
|
||||||
|
@ -65,7 +64,7 @@ exports.setPadRaw = (padId, r) => {
|
||||||
if (value.pool) {
|
if (value.pool) {
|
||||||
for (const attrib of Object.keys(value.pool.numToAttrib)) {
|
for (const attrib of Object.keys(value.pool.numToAttrib)) {
|
||||||
const attribName = value.pool.numToAttrib[attrib][0];
|
const attribName = value.pool.numToAttrib[attrib][0];
|
||||||
if (blockElems.indexOf(attribName) === -1) {
|
if (supportedElems.indexOf(attribName) === -1) {
|
||||||
console.warn('Plugin missing: ' +
|
console.warn('Plugin missing: ' +
|
||||||
`You might want to install a plugin to support this node name: ${attribName}`);
|
`You might want to install a plugin to support this node name: ${attribName}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,6 +123,15 @@ const sanitizePathname = (p) => {
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const compatPaths = {
|
||||||
|
'js/browser.js': 'js/vendors/browser.js',
|
||||||
|
'js/farbtastic.js': 'js/vendors/farbtastic.js',
|
||||||
|
'js/gritter.js': 'js/vendors/gritter.js',
|
||||||
|
'js/html10n.js': 'js/vendors/html10n.js',
|
||||||
|
'js/jquery.js': 'js/vendors/jquery.js',
|
||||||
|
'js/nice-select.js': 'js/vendors/nice-select.js',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creates the minifed javascript for the given minified name
|
* creates the minifed javascript for the given minified name
|
||||||
* @param req the Express request
|
* @param req the Express request
|
||||||
|
@ -139,11 +148,11 @@ const minify = async (req, res) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility for plugins that were written when jQuery lived at
|
// Backward compatibility for plugins that require() files from old paths.
|
||||||
// src/static/js/jquery.js.
|
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
|
||||||
if (['js/jquery.js', 'plugins/ep_etherpad-lite/static/js/jquery.js'].indexOf(filename) !== -1) {
|
if (newLocation != null) {
|
||||||
logger.warn(`request for deprecated jQuery path: ${filename}`);
|
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
|
||||||
filename = 'js/vendors/jquery.js';
|
filename = newLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle static files for plugins/libraries:
|
/* Handle static files for plugins/libraries:
|
||||||
|
@ -159,7 +168,7 @@ const minify = async (req, res) => {
|
||||||
if (plugins.plugins[library] && match[3]) {
|
if (plugins.plugins[library] && match[3]) {
|
||||||
const plugin = plugins.plugins[library];
|
const plugin = plugins.plugins[library];
|
||||||
const pluginPath = plugin.package.realPath;
|
const pluginPath = plugin.package.realPath;
|
||||||
filename = path.relative(ROOT_DIR, path.join(pluginPath, libraryPath));
|
filename = path.join(pluginPath, libraryPath);
|
||||||
// On Windows, path.relative converts forward slashes to backslashes. Convert them back
|
// 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
|
// 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
|
// and the forward slash characters as pathname component separators on Windows so this does
|
||||||
|
@ -211,44 +220,6 @@ const minify = async (req, res) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// find all includes in ace.js and embed them.
|
|
||||||
const getAceFile = async () => {
|
|
||||||
let data = await fs.readFile(path.join(ROOT_DIR, 'js/ace.js'), 'utf8');
|
|
||||||
|
|
||||||
// Find all includes in ace.js and embed them
|
|
||||||
const filenames = [];
|
|
||||||
if (settings.minify) {
|
|
||||||
const regex = /\$\$INCLUDE_[a-zA-Z_]+\((['"])([^'"]*)\1\)/gi;
|
|
||||||
// This logic can be simplified via String.prototype.matchAll() once support for Node.js
|
|
||||||
// v11.x and older is dropped.
|
|
||||||
let matches;
|
|
||||||
while ((matches = regex.exec(data)) != null) {
|
|
||||||
filenames.push(matches[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data += 'Ace2Editor.EMBEDED = Ace2Editor.EMBEDED || {};\n';
|
|
||||||
|
|
||||||
// Request the contents of the included file on the server-side and write
|
|
||||||
// them into the file.
|
|
||||||
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.join('/static/', filename);
|
|
||||||
resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?)
|
|
||||||
|
|
||||||
const [status, , body] = await requestURI(resourceURI, 'GET', {});
|
|
||||||
const error = !(status === 200 || status === 404);
|
|
||||||
if (!error) {
|
|
||||||
data += `Ace2Editor.EMBEDED[${JSON.stringify(filename)}] = ${
|
|
||||||
JSON.stringify(status === 200 ? body || '' : null)};\n`;
|
|
||||||
} else {
|
|
||||||
console.error(`getAceFile(): error getting ${resourceURI}. Status code: ${status}`);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check for the existance of the file and get the last modification date.
|
// Check for the existance of the file and get the last modification date.
|
||||||
const statFile = async (filename, dirStatLimit) => {
|
const statFile = async (filename, dirStatLimit) => {
|
||||||
/*
|
/*
|
||||||
|
@ -270,7 +241,7 @@ const statFile = async (filename, dirStatLimit) => {
|
||||||
} else {
|
} else {
|
||||||
let stats;
|
let stats;
|
||||||
try {
|
try {
|
||||||
stats = await fs.stat(path.join(ROOT_DIR, filename));
|
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
// Stat the directory instead.
|
// Stat the directory instead.
|
||||||
|
@ -357,9 +328,8 @@ const getFileCompressed = async (filename, contentType) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFile = async (filename) => {
|
const getFile = async (filename) => {
|
||||||
if (filename === 'js/ace.js') return await getAceFile();
|
|
||||||
if (filename === 'js/require-kernel.js') return requireDefinition();
|
if (filename === 'js/require-kernel.js') return requireDefinition();
|
||||||
return await fs.readFile(path.join(ROOT_DIR, filename));
|
return await fs.readFile(path.resolve(ROOT_DIR, filename));
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
|
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
|
||||||
|
|
|
@ -12,7 +12,7 @@ const compressJS = (content) => Terser.minify(content);
|
||||||
|
|
||||||
const compressCSS = (filename, ROOT_DIR) => new Promise((res, rej) => {
|
const compressCSS = (filename, ROOT_DIR) => new Promise((res, rej) => {
|
||||||
try {
|
try {
|
||||||
const absPath = path.join(ROOT_DIR, filename);
|
const absPath = path.resolve(ROOT_DIR, filename);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Changes done to migrate CleanCSS 3.x -> 4.x:
|
* Changes done to migrate CleanCSS 3.x -> 4.x:
|
||||||
|
|
53
src/package-lock.json
generated
53
src/package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ep_etherpad-lite",
|
"name": "ep_etherpad-lite",
|
||||||
"version": "1.8.11",
|
"version": "1.8.12",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1413,9 +1413,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"dirty": {
|
"dirty": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.1.tgz",
|
||||||
"integrity": "sha1-cO3SuZlUHcmXT9Ooy9DGcP4jYHg="
|
"integrity": "sha512-l/SMZcT+MjqOPpjarzJ8nQdxtxurURJM7js1l0Q2TQWtNbPzDYzkK++HlbT+XmM+adPFNdb3SOlVz9Jr7Df7xQ=="
|
||||||
},
|
},
|
||||||
"doctrine": {
|
"doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
@ -1808,11 +1808,28 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-config-etherpad": {
|
"eslint-config-etherpad": {
|
||||||
"version": "1.0.25",
|
"version": "1.0.26",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.25.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.26.tgz",
|
||||||
"integrity": "sha512-KYTGf08dlwvsg05Y2hm0zurCwVMyZrsxGRnPEhL2wclk26xhnPYfNfruQQqk7nghfWFLrAL+VscnkZLCQEPBXQ==",
|
"integrity": "sha512-xPnDnJIpQuYJNRYGIHIucct0U6CtciyZKItpet+NqoGJgxUMkwAXgD5bzuXQvd9u4I2aj/kRU1BIL2DbAGe+pA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"eslint-plugin-cypress": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-1SergF1sGbVhsf7MYfOLiBhdOg6wqyeV9pXUAIDIffYTGMN3dTBQS9nFAzhLsHhO+Bn0GaVM1Ecm71XUidQ7VA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"globals": "^11.12.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"globals": {
|
||||||
|
"version": "11.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
|
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"eslint-plugin-es": {
|
"eslint-plugin-es": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
|
||||||
|
@ -2093,9 +2110,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"express-rate-limit": {
|
"express-rate-limit": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.5.tgz",
|
||||||
"integrity": "sha512-cjQH+oDrEPXxc569XvxhHC6QXqJiuBT6BhZ70X3bdAImcnHnTNMVuMAJaT0TXPoRiEErUrVPRcOTpZpM36VbOQ=="
|
"integrity": "sha512-fv9mf4hWRKZHVlY8ChVNYnGxa49m0zQ6CrJxNiXe2IjJPqicrqoA/JOyBbvs4ufSSLZ6NTzhtgEyLcdfbe+Q6Q=="
|
||||||
},
|
},
|
||||||
"express-session": {
|
"express-session": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
|
@ -7522,11 +7539,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.19.0",
|
"version": "1.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
|
||||||
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
|
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-core-module": "^2.1.0",
|
"is-core-module": "^2.2.0",
|
||||||
"path-parse": "^1.0.6"
|
"path-parse": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -8419,14 +8436,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ueberdb2": {
|
"ueberdb2": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.3.2.tgz",
|
||||||
"integrity": "sha512-uhUSJfI5sNWdiXxae0kOg88scaMIKcV0CVeojwPQzgm93vQVuGyCqS1g1i3gTZel6SwmXRFtYtfmtAmiEe+HBQ==",
|
"integrity": "sha512-7Ub5jDsIS+qjjsNV7yp1CHXHVe2K9ZUpwaHi9BZf3ai0DxtuHOfMada1wxL6iyEjwYXh/Nsu80iyId51wHFf4A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"async": "^3.2.0",
|
"async": "^3.2.0",
|
||||||
"cassandra-driver": "^4.5.1",
|
"cassandra-driver": "^4.5.1",
|
||||||
"channels": "0.0.4",
|
"channels": "0.0.4",
|
||||||
"dirty": "^1.1.0",
|
"dirty": "^1.1.1",
|
||||||
"elasticsearch": "^16.7.1",
|
"elasticsearch": "^16.7.1",
|
||||||
"mongodb": "^3.6.3",
|
"mongodb": "^3.6.3",
|
||||||
"mssql": "^7.0.0-beta.2",
|
"mssql": "^7.0.0-beta.2",
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
"etherpad-require-kernel": "1.0.9",
|
"etherpad-require-kernel": "1.0.9",
|
||||||
"etherpad-yajsml": "0.0.4",
|
"etherpad-yajsml": "0.0.4",
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
"express-rate-limit": "5.2.3",
|
"express-rate-limit": "5.2.5",
|
||||||
"express-session": "1.17.1",
|
"express-session": "1.17.1",
|
||||||
"find-root": "1.1.0",
|
"find-root": "1.1.0",
|
||||||
"formidable": "1.2.2",
|
"formidable": "1.2.2",
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
"rehype": "^10.0.0",
|
"rehype": "^10.0.0",
|
||||||
"rehype-minify-whitespace": "^4.0.5",
|
"rehype-minify-whitespace": "^4.0.5",
|
||||||
"request": "2.88.2",
|
"request": "2.88.2",
|
||||||
"resolve": "1.19.0",
|
"resolve": "1.20.0",
|
||||||
"security": "1.0.0",
|
"security": "1.0.0",
|
||||||
"semver": "5.7.1",
|
"semver": "5.7.1",
|
||||||
"socket.io": "^2.4.1",
|
"socket.io": "^2.4.1",
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
"threads": "^1.4.0",
|
"threads": "^1.4.0",
|
||||||
"tiny-worker": "^2.3.0",
|
"tiny-worker": "^2.3.0",
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"ueberdb2": "^1.3.1",
|
"ueberdb2": "^1.3.2",
|
||||||
"underscore": "1.12.0",
|
"underscore": "1.12.0",
|
||||||
"unorm": "1.6.0",
|
"unorm": "1.6.0",
|
||||||
"wtfnode": "^0.8.4"
|
"wtfnode": "^0.8.4"
|
||||||
|
@ -79,7 +79,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.20.0",
|
"eslint": "^7.20.0",
|
||||||
"eslint-config-etherpad": "^1.0.25",
|
"eslint-config-etherpad": "^1.0.26",
|
||||||
|
"eslint-plugin-cypress": "^2.11.2",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
"eslint-plugin-mocha": "^8.0.0",
|
"eslint-plugin-mocha": "^8.0.0",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
@ -155,9 +156,10 @@
|
||||||
],
|
],
|
||||||
"excludedFiles": [
|
"excludedFiles": [
|
||||||
"**/.eslintrc.js",
|
"**/.eslintrc.js",
|
||||||
"tests/frontend/travis/**/*",
|
"tests/frontend/cypress/**/*",
|
||||||
"tests/frontend/helper.js",
|
"tests/frontend/helper.js",
|
||||||
"tests/frontend/helper/**/*",
|
"tests/frontend/helper/**/*",
|
||||||
|
"tests/frontend/travis/**/*",
|
||||||
"tests/ratelimit/**/*"
|
"tests/ratelimit/**/*"
|
||||||
],
|
],
|
||||||
"extends": "etherpad/tests",
|
"extends": "etherpad/tests",
|
||||||
|
@ -195,6 +197,7 @@
|
||||||
],
|
],
|
||||||
"excludedFiles": [
|
"excludedFiles": [
|
||||||
"**/.eslintrc.js",
|
"**/.eslintrc.js",
|
||||||
|
"tests/frontend/cypress/**/*",
|
||||||
"tests/frontend/helper.js",
|
"tests/frontend/helper.js",
|
||||||
"tests/frontend/helper/**/*",
|
"tests/frontend/helper/**/*",
|
||||||
"tests/frontend/travis/**/*"
|
"tests/frontend/travis/**/*"
|
||||||
|
@ -215,6 +218,12 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"tests/frontend/cypress/**/*"
|
||||||
|
],
|
||||||
|
"extends": "etherpad/tests/cypress"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"files": [
|
"files": [
|
||||||
"tests/frontend/travis/**/*"
|
"tests/frontend/travis/**/*"
|
||||||
|
@ -237,6 +246,6 @@
|
||||||
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
||||||
"test-container": "mocha --timeout 5000 tests/container/specs/api"
|
"test-container": "mocha --timeout 5000 tests/container/specs/api"
|
||||||
},
|
},
|
||||||
"version": "1.8.11",
|
"version": "1.8.12",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,16 +27,20 @@
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
const pluginUtils = require('./pluginfw/shared');
|
const pluginUtils = require('./pluginfw/shared');
|
||||||
|
|
||||||
|
const debugLog = (...args) => {};
|
||||||
|
window.debugLog = debugLog;
|
||||||
|
|
||||||
|
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
|
||||||
|
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
|
||||||
|
// errors out unless given an absolute URL for a JavaScript-created element.
|
||||||
|
const absUrl = (url) => new URL(url, window.location.href).href;
|
||||||
|
|
||||||
const scriptTag =
|
const scriptTag =
|
||||||
(source) => `<script type="text/javascript">\n${source.replace(/<\//g, '<\\/')}</script>`;
|
(source) => `<script type="text/javascript">\n${source.replace(/<\//g, '<\\/')}</script>`;
|
||||||
|
|
||||||
const Ace2Editor = function () {
|
const Ace2Editor = function () {
|
||||||
const ace2 = Ace2Editor;
|
let info = {editor: this};
|
||||||
|
window.ace2EditorInfo = info; // Make it accessible to iframes.
|
||||||
let info = {
|
|
||||||
editor: this,
|
|
||||||
id: (ace2.registry.nextId++),
|
|
||||||
};
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
|
||||||
let actionsPendingInit = [];
|
let actionsPendingInit = [];
|
||||||
|
@ -52,8 +56,6 @@ const Ace2Editor = function () {
|
||||||
actionsPendingInit = [];
|
actionsPendingInit = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
ace2.registry[info.id] = info;
|
|
||||||
|
|
||||||
// The following functions (prefixed by 'ace_') are exposed by editor, but
|
// The following functions (prefixed by 'ace_') are exposed by editor, but
|
||||||
// execution is delayed until init is complete
|
// execution is delayed until init is complete
|
||||||
const aceFunctionsPendingInit = [
|
const aceFunctionsPendingInit = [
|
||||||
|
@ -89,8 +91,6 @@ const Ace2Editor = function () {
|
||||||
|
|
||||||
this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n';
|
this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n';
|
||||||
|
|
||||||
this.getFrame = () => info.frame || null;
|
|
||||||
|
|
||||||
this.getDebugProperty = (prop) => info.ace_getDebugProperty(prop);
|
this.getDebugProperty = (prop) => info.ace_getDebugProperty(prop);
|
||||||
|
|
||||||
this.getInInternationalComposition =
|
this.getInInternationalComposition =
|
||||||
|
@ -109,207 +109,140 @@ const Ace2Editor = function () {
|
||||||
// returns array of {error: <browser Error object>, time: +new Date()}
|
// returns array of {error: <browser Error object>, time: +new Date()}
|
||||||
this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : [];
|
this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : [];
|
||||||
|
|
||||||
const sortFilesByEmbeded = (files) => {
|
|
||||||
const embededFiles = [];
|
|
||||||
let remoteFiles = [];
|
|
||||||
|
|
||||||
if (Ace2Editor.EMBEDED) {
|
|
||||||
for (let i = 0, ii = files.length; i < ii; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
if (Object.prototype.hasOwnProperty.call(Ace2Editor.EMBEDED, file)) {
|
|
||||||
embededFiles.push(file);
|
|
||||||
} else {
|
|
||||||
remoteFiles.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
remoteFiles = files;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {embeded: embededFiles, remote: remoteFiles};
|
|
||||||
};
|
|
||||||
|
|
||||||
const pushStyleTagsFor = (buffer, files) => {
|
const pushStyleTagsFor = (buffer, files) => {
|
||||||
const sorted = sortFilesByEmbeded(files);
|
for (const file of files) {
|
||||||
const embededFiles = sorted.embeded;
|
buffer.push(`<link rel="stylesheet" type="text/css" href="${absUrl(encodeURI(file))}"/>`);
|
||||||
const remoteFiles = sorted.remote;
|
|
||||||
|
|
||||||
if (embededFiles.length > 0) {
|
|
||||||
buffer.push('<style type="text/css">');
|
|
||||||
for (const file of embededFiles) {
|
|
||||||
buffer.push((Ace2Editor.EMBEDED[file] || '').replace(/<\//g, '<\\/'));
|
|
||||||
}
|
|
||||||
buffer.push('</style>');
|
|
||||||
}
|
|
||||||
for (const file of remoteFiles) {
|
|
||||||
buffer.push(`<link rel="stylesheet" type="text/css" href="${encodeURI(file)}"/>`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.destroy = pendingInit(() => {
|
this.destroy = pendingInit(() => {
|
||||||
info.ace_dispose();
|
info.ace_dispose();
|
||||||
info.frame.parentNode.removeChild(info.frame);
|
info.frame.parentNode.removeChild(info.frame);
|
||||||
delete ace2.registry[info.id];
|
delete window.ace2EditorInfo;
|
||||||
info = null; // prevent IE 6 closure memory leaks
|
info = null; // prevent IE 6 closure memory leaks
|
||||||
});
|
});
|
||||||
|
|
||||||
this.init = function (containerId, initialCode, doneFunc) {
|
this.init = async function (containerId, initialCode) {
|
||||||
|
debugLog('Ace2Editor.init()');
|
||||||
this.importText(initialCode);
|
this.importText(initialCode);
|
||||||
|
|
||||||
info.onEditorReady = () => {
|
const includedCSS = [
|
||||||
loaded = true;
|
'../static/css/iframe_editor.css',
|
||||||
doActionsPendingInit();
|
`../static/css/pad.css?v=${clientVars.randomVersionString}`,
|
||||||
doneFunc();
|
...hooks.callAll('aceEditorCSS').map(
|
||||||
};
|
// Allow urls to external CSS - http(s):// and //some/path.css
|
||||||
|
(p) => /\/\//.test(p) ? p : `../static/plugins/${p}`),
|
||||||
|
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`,
|
||||||
|
];
|
||||||
|
|
||||||
(() => {
|
const doctype = '<!doctype html>';
|
||||||
const doctype = '<!doctype html>';
|
|
||||||
|
|
||||||
const iframeHTML = [];
|
const iframeHTML = [];
|
||||||
|
|
||||||
iframeHTML.push(doctype);
|
iframeHTML.push(doctype);
|
||||||
iframeHTML.push(`<html class='inner-editor ${clientVars.skinVariants}'><head>`);
|
iframeHTML.push(`<html class='inner-editor ${clientVars.skinVariants}'><head>`);
|
||||||
|
pushStyleTagsFor(iframeHTML, includedCSS);
|
||||||
|
const requireKernelUrl =
|
||||||
|
absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
|
||||||
|
iframeHTML.push(`<script type="text/javascript" src="${requireKernelUrl}"></script>`);
|
||||||
|
// Pre-fetch modules to improve load performance.
|
||||||
|
for (const module of ['ace2_inner', 'ace2_common']) {
|
||||||
|
const url = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
|
||||||
|
`?callback=require.define&v=${clientVars.randomVersionString}`);
|
||||||
|
iframeHTML.push(`<script type="text/javascript" src="${url}"></script>`);
|
||||||
|
}
|
||||||
|
|
||||||
// calls to these functions ($$INCLUDE_...) are replaced when this file is processed
|
iframeHTML.push(scriptTag(`(async () => {
|
||||||
// and compressed, putting the compressed code from the named file directly into the
|
parent.parent.debugLog('Ace2Editor.init() inner frame ready');
|
||||||
// source here.
|
const require = window.require;
|
||||||
// these lines must conform to a specific format because they are passed by the build script:
|
require.setRootURI(${JSON.stringify(absUrl('../javascripts/src'))});
|
||||||
let includedCSS = [];
|
require.setLibraryURI(${JSON.stringify(absUrl('../javascripts/lib'))});
|
||||||
let $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); };
|
require.setGlobalKeyPath('require');
|
||||||
$$INCLUDE_CSS('../static/css/iframe_editor.css');
|
|
||||||
|
|
||||||
// disableCustomScriptsAndStyles can be used to disable loading of custom scripts
|
// intentially moved before requiring client_plugins to save a 307
|
||||||
if (!clientVars.disableCustomScriptsAndStyles) {
|
window.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
|
||||||
$$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`);
|
window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
|
||||||
}
|
window.plugins.adoptPluginsFromAncestorsOf(window);
|
||||||
|
|
||||||
let additionalCSS = hooks.callAll('aceEditorCSS').map((path) => {
|
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
|
||||||
if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
return `../static/plugins/${path}`;
|
|
||||||
});
|
|
||||||
includedCSS = includedCSS.concat(additionalCSS);
|
|
||||||
$$INCLUDE_CSS(
|
|
||||||
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`);
|
|
||||||
|
|
||||||
pushStyleTagsFor(iframeHTML, includedCSS);
|
parent.parent.debugLog('Ace2Editor.init() waiting for plugins');
|
||||||
iframeHTML.push(`<script type="text/javascript" src="../static/js/require-kernel.js?v=${clientVars.randomVersionString}"></script>`);
|
await new Promise((resolve, reject) => window.plugins.ensure(
|
||||||
// fill the cache
|
(err) => err != null ? reject(err) : resolve()));
|
||||||
iframeHTML.push(`<script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define&v=${clientVars.randomVersionString}"></script>`);
|
parent.parent.debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
|
||||||
iframeHTML.push(`<script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define&v=${clientVars.randomVersionString}"></script>`);
|
const editorInfo = parent.parent.ace2EditorInfo;
|
||||||
|
await new Promise((resolve, reject) => window.Ace2Inner.init(
|
||||||
|
editorInfo, (err) => err != null ? reject(err) : resolve()));
|
||||||
|
parent.parent.debugLog('Ace2Editor.init() Ace2Inner.init() returned');
|
||||||
|
editorInfo.onEditorReady();
|
||||||
|
})();`));
|
||||||
|
|
||||||
iframeHTML.push(scriptTag(
|
iframeHTML.push('<style type="text/css" title="dynamicsyntax"></style>');
|
||||||
`\n\
|
|
||||||
require.setRootURI("../javascripts/src");\n\
|
|
||||||
require.setLibraryURI("../javascripts/lib");\n\
|
|
||||||
require.setGlobalKeyPath("require");\n\
|
|
||||||
\n\
|
|
||||||
// intentially moved before requiring client_plugins to save a 307
|
|
||||||
var Ace2Inner = require("ep_etherpad-lite/static/js/ace2_inner");\n\
|
|
||||||
var plugins = require("ep_etherpad-lite/static/js/pluginfw/client_plugins");\n\
|
|
||||||
plugins.adoptPluginsFromAncestorsOf(window);\n\
|
|
||||||
\n\
|
|
||||||
$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK\n\
|
|
||||||
\n\
|
|
||||||
plugins.ensure(function () {\n\
|
|
||||||
Ace2Inner.init();\n\
|
|
||||||
});\n\
|
|
||||||
`));
|
|
||||||
|
|
||||||
iframeHTML.push('<style type="text/css" title="dynamicsyntax"></style>');
|
hooks.callAll('aceInitInnerdocbodyHead', {
|
||||||
|
iframeHTML,
|
||||||
|
});
|
||||||
|
|
||||||
hooks.callAll('aceInitInnerdocbodyHead', {
|
iframeHTML.push('</head><body id="innerdocbody" class="innerdocbody" role="application" ' +
|
||||||
iframeHTML,
|
'spellcheck="false"> </body></html>');
|
||||||
});
|
|
||||||
|
|
||||||
iframeHTML.push('</head><body id="innerdocbody" class="innerdocbody" role="application" ' +
|
const outerScript = `(async () => {
|
||||||
'class="syntax" spellcheck="false"> </body></html>');
|
await new Promise((resolve) => { window.onload = () => resolve(); });
|
||||||
|
parent.debugLog('Ace2Editor.init() outer frame ready');
|
||||||
|
window.onload = null;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.name = 'ace_inner';
|
||||||
|
iframe.title = 'pad';
|
||||||
|
iframe.scrolling = 'no';
|
||||||
|
iframe.frameBorder = 0;
|
||||||
|
iframe.allowTransparency = true; // for IE
|
||||||
|
iframe.ace_outerWin = window;
|
||||||
|
document.body.insertBefore(iframe, document.body.firstChild);
|
||||||
|
const doc = iframe.contentWindow.document;
|
||||||
|
doc.open();
|
||||||
|
doc.write(${JSON.stringify(iframeHTML.join('\n'))});
|
||||||
|
doc.close();
|
||||||
|
parent.debugLog('Ace2Editor.init() waiting for inner frame');
|
||||||
|
})();`;
|
||||||
|
|
||||||
// eslint-disable-next-line node/no-unsupported-features/es-builtins
|
const outerHTML =
|
||||||
const gt = typeof globalThis === 'object' ? globalThis : window;
|
[doctype, `<html class="inner-editor outerdoc ${clientVars.skinVariants}"><head>`];
|
||||||
gt.ChildAccessibleAce2Editor = Ace2Editor;
|
pushStyleTagsFor(outerHTML, includedCSS);
|
||||||
|
|
||||||
const outerScript = `\
|
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
|
||||||
editorId = ${JSON.stringify(info.id)};\n\
|
// (throbs busy while typing)
|
||||||
editorInfo = parent.ChildAccessibleAce2Editor.registry[editorId];\n\
|
const pluginNames = pluginUtils.clientPluginNames();
|
||||||
window.onload = function () {\n\
|
outerHTML.push(
|
||||||
window.onload = null;\n\
|
'<style type="text/css" title="dynamicsyntax"></style>',
|
||||||
setTimeout(function () {\n\
|
'<link rel="stylesheet" type="text/css" href="data:text/css,"/>',
|
||||||
var iframe = document.createElement("IFRAME");\n\
|
scriptTag(outerScript),
|
||||||
iframe.name = "ace_inner";\n\
|
'</head>',
|
||||||
iframe.title = "pad";\n\
|
'<body id="outerdocbody" class="outerdocbody ', pluginNames.join(' '), '">',
|
||||||
iframe.scrolling = "no";\n\
|
'<div id="sidediv" class="sidediv"><!-- --></div>',
|
||||||
var outerdocbody = document.getElementById("outerdocbody");\n\
|
'<div id="linemetricsdiv">x</div>',
|
||||||
iframe.frameBorder = 0;\n\
|
'</body></html>');
|
||||||
iframe.allowTransparency = true; // for IE\n\
|
|
||||||
outerdocbody.insertBefore(iframe, outerdocbody.firstChild);\n\
|
|
||||||
iframe.ace_outerWin = window;\n\
|
|
||||||
readyFunc = function () {\n\
|
|
||||||
editorInfo.onEditorReady();\n\
|
|
||||||
readyFunc = null;\n\
|
|
||||||
editorInfo = null;\n\
|
|
||||||
};\n\
|
|
||||||
var doc = iframe.contentWindow.document;\n\
|
|
||||||
doc.open();\n\
|
|
||||||
var text = (${JSON.stringify(iframeHTML.join('\n'))});\n\
|
|
||||||
doc.write(text);\n\
|
|
||||||
doc.close();\n\
|
|
||||||
}, 0);\n\
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const outerHTML =
|
const outerFrame = document.createElement('IFRAME');
|
||||||
[doctype, `<html class="inner-editor outerdoc ${clientVars.skinVariants}"><head>`];
|
outerFrame.name = 'ace_outer';
|
||||||
|
outerFrame.frameBorder = 0; // for IE
|
||||||
|
outerFrame.title = 'Ether';
|
||||||
|
info.frame = outerFrame;
|
||||||
|
document.getElementById(containerId).appendChild(outerFrame);
|
||||||
|
|
||||||
includedCSS = [];
|
const editorDocument = outerFrame.contentWindow.document;
|
||||||
$$INCLUDE_CSS = (filename) => { includedCSS.push(filename); };
|
|
||||||
$$INCLUDE_CSS('../static/css/iframe_editor.css');
|
|
||||||
$$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`);
|
|
||||||
|
|
||||||
|
|
||||||
additionalCSS = hooks.callAll('aceEditorCSS').map((path) => {
|
|
||||||
if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
return `../static/plugins/${path}`;
|
|
||||||
});
|
|
||||||
includedCSS = includedCSS.concat(additionalCSS);
|
|
||||||
$$INCLUDE_CSS(
|
|
||||||
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`);
|
|
||||||
|
|
||||||
pushStyleTagsFor(outerHTML, includedCSS);
|
|
||||||
|
|
||||||
// bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
|
|
||||||
// (throbs busy while typing)
|
|
||||||
const pluginNames = pluginUtils.clientPluginNames();
|
|
||||||
outerHTML.push(
|
|
||||||
'<style type="text/css" title="dynamicsyntax"></style>',
|
|
||||||
'<link rel="stylesheet" type="text/css" href="data:text/css,"/>',
|
|
||||||
scriptTag(outerScript),
|
|
||||||
'</head>',
|
|
||||||
'<body id="outerdocbody" class="outerdocbody ', pluginNames.join(' '), '">',
|
|
||||||
'<div id="sidediv" class="sidediv"><!-- --></div>',
|
|
||||||
'<div id="linemetricsdiv">x</div>',
|
|
||||||
'</body></html>');
|
|
||||||
|
|
||||||
const outerFrame = document.createElement('IFRAME');
|
|
||||||
outerFrame.name = 'ace_outer';
|
|
||||||
outerFrame.frameBorder = 0; // for IE
|
|
||||||
outerFrame.title = 'Ether';
|
|
||||||
info.frame = outerFrame;
|
|
||||||
document.getElementById(containerId).appendChild(outerFrame);
|
|
||||||
|
|
||||||
const editorDocument = outerFrame.contentWindow.document;
|
|
||||||
|
|
||||||
|
debugLog('Ace2Editor.init() waiting for outer frame');
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
info.onEditorReady = (err) => err != null ? reject(err) : resolve();
|
||||||
editorDocument.open();
|
editorDocument.open();
|
||||||
editorDocument.write(outerHTML.join(''));
|
editorDocument.write(outerHTML.join(''));
|
||||||
editorDocument.close();
|
editorDocument.close();
|
||||||
})();
|
});
|
||||||
|
loaded = true;
|
||||||
|
doActionsPendingInit();
|
||||||
|
debugLog('Ace2Editor.init() done');
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
Ace2Editor.registry = {
|
|
||||||
nextId: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Ace2Editor = Ace2Editor;
|
exports.Ace2Editor = Ace2Editor;
|
||||||
|
|
|
@ -30,7 +30,7 @@ const htmlPrettyEscape = Ace2Common.htmlPrettyEscape;
|
||||||
const noop = Ace2Common.noop;
|
const noop = Ace2Common.noop;
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
|
||||||
function Ace2Inner() {
|
function Ace2Inner(editorInfo) {
|
||||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
||||||
|
@ -57,7 +57,6 @@ function Ace2Inner() {
|
||||||
let thisAuthor = '';
|
let thisAuthor = '';
|
||||||
|
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
const editorInfo = parent.editorInfo;
|
|
||||||
|
|
||||||
const focus = () => {
|
const focus = () => {
|
||||||
window.focus();
|
window.focus();
|
||||||
|
@ -3894,9 +3893,9 @@ function Ace2Inner() {
|
||||||
documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset);
|
documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset);
|
||||||
|
|
||||||
editorInfo.ace_performDocumentApplyAttributesToRange =
|
editorInfo.ace_performDocumentApplyAttributesToRange =
|
||||||
(...args) => documentAttributeManager.setAttributesOnRange(args);
|
(...args) => documentAttributeManager.setAttributesOnRange(...args);
|
||||||
|
|
||||||
this.init = () => {
|
this.init = (cb) => {
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
doc = document; // defined as a var in scope outside
|
doc = document; // defined as a var in scope outside
|
||||||
inCallStack('setup', () => {
|
inCallStack('setup', () => {
|
||||||
|
@ -3928,14 +3927,12 @@ function Ace2Inner() {
|
||||||
documentAttributeManager,
|
documentAttributeManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
scheduler.setTimeout(() => {
|
scheduler.setTimeout(cb, 0);
|
||||||
parent.readyFunc(); // defined in code that sets up the inner iframe
|
|
||||||
}, 0);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.init = () => {
|
exports.init = (editorInfo, cb) => {
|
||||||
const editor = new Ace2Inner();
|
const editor = new Ace2Inner(editorInfo);
|
||||||
editor.init();
|
editor.init(cb);
|
||||||
};
|
};
|
||||||
|
|
|
@ -55,6 +55,28 @@ const getAttribute = (n, a) => {
|
||||||
if (n.attribs != null) return n.attribs[a];
|
if (n.attribs != null) return n.attribs[a];
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
// supportedElems are Supported natively within Etherpad and don't require a plugin
|
||||||
|
const supportedElems = [
|
||||||
|
'author',
|
||||||
|
'b',
|
||||||
|
'bold',
|
||||||
|
'br',
|
||||||
|
'div',
|
||||||
|
'font',
|
||||||
|
'i',
|
||||||
|
'insertorder',
|
||||||
|
'italic',
|
||||||
|
'li',
|
||||||
|
'lmkr',
|
||||||
|
'ol',
|
||||||
|
'p',
|
||||||
|
'pre',
|
||||||
|
'strong',
|
||||||
|
's',
|
||||||
|
'span',
|
||||||
|
'u',
|
||||||
|
'ul',
|
||||||
|
];
|
||||||
|
|
||||||
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => {
|
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => {
|
||||||
const _blockElems = {
|
const _blockElems = {
|
||||||
|
@ -66,6 +88,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
||||||
|
|
||||||
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
|
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
|
||||||
_blockElems[element] = 1;
|
_blockElems[element] = 1;
|
||||||
|
supportedElems.push(element);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
|
const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
|
||||||
|
@ -315,9 +338,11 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
||||||
const localAttribs = state.localAttribs;
|
const localAttribs = state.localAttribs;
|
||||||
state.localAttribs = null;
|
state.localAttribs = null;
|
||||||
const isBlock = isBlockElement(node);
|
const isBlock = isBlockElement(node);
|
||||||
if (!isBlock && node.name && (node.name !== 'body') && (node.name !== 'br')) {
|
if (!isBlock && node.name && (node.name !== 'body')) {
|
||||||
console.warn('Plugin missing: ' +
|
if (supportedElems.indexOf(node.name) === -1) {
|
||||||
|
console.warn('Plugin missing: ' +
|
||||||
`You might want to install a plugin to support this node name: ${node.name}`);
|
`You might want to install a plugin to support this node name: ${node.name}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const isEmpty = _isEmpty(node, state);
|
const isEmpty = _isEmpty(node, state);
|
||||||
if (isBlock) _ensureColumnZero(state);
|
if (isBlock) _ensureColumnZero(state);
|
||||||
|
@ -701,3 +726,4 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
||||||
|
|
||||||
exports.sanitizeUnicode = sanitizeUnicode;
|
exports.sanitizeUnicode = sanitizeUnicode;
|
||||||
exports.makeContentCollector = makeContentCollector;
|
exports.makeContentCollector = makeContentCollector;
|
||||||
|
exports.supportedElems = supportedElems;
|
||||||
|
|
|
@ -56,7 +56,8 @@ const padeditor = (() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.ace = new Ace2Editor();
|
self.ace = new Ace2Editor();
|
||||||
self.ace.init('editorcontainer', '', aceReady);
|
self.ace.init('editorcontainer', '').then(
|
||||||
|
() => aceReady(), (err) => { throw err || new Error(err); });
|
||||||
self.ace.setProperty('wraps', true);
|
self.ace.setProperty('wraps', true);
|
||||||
if (pad.getIsDebugEnabled()) {
|
if (pad.getIsDebugEnabled()) {
|
||||||
self.ace.setProperty('dmesg', pad.dmesg);
|
self.ace.setProperty('dmesg', pad.dmesg);
|
||||||
|
|
|
@ -28,16 +28,48 @@ exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
|
||||||
|
|
||||||
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n');
|
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n');
|
||||||
|
|
||||||
exports.formatHooks = (hookSetName) => {
|
exports.formatHooks = (hookSetName, html) => {
|
||||||
const res = [];
|
let hooks = new Map();
|
||||||
const hooks = pluginUtils.extractHooks(defs.parts, hookSetName || 'hooks');
|
for (const [pluginName, def] of Object.entries(defs.plugins)) {
|
||||||
for (const registeredHooks of Object.values(hooks)) {
|
for (const part of def.parts) {
|
||||||
for (const hook of registeredHooks) {
|
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
|
||||||
res.push(`<dt>${hook.hook_name}</dt><dd>${hook.hook_fn_name} ` +
|
let hookEntry = hooks.get(hookName);
|
||||||
`from ${hook.part.full_name}</dd>`);
|
if (!hookEntry) {
|
||||||
|
hookEntry = new Map();
|
||||||
|
hooks.set(hookName, hookEntry);
|
||||||
|
}
|
||||||
|
let pluginEntry = hookEntry.get(pluginName);
|
||||||
|
if (!pluginEntry) {
|
||||||
|
pluginEntry = new Map();
|
||||||
|
hookEntry.set(pluginName, pluginEntry);
|
||||||
|
}
|
||||||
|
pluginEntry.set(part.name, hookFnName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `<dl>${res.join('\n')}</dl>`;
|
const lines = [];
|
||||||
|
const sortStringKeys = (a, b) => String(a[0]).localeCompare(b[0]);
|
||||||
|
if (html) lines.push('<dl>');
|
||||||
|
hooks = new Map([...hooks].sort(sortStringKeys));
|
||||||
|
for (const [hookName, hookEntry] of hooks) {
|
||||||
|
lines.push(html ? ` <dt>${hookName}:</dt><dd><dl>` : ` ${hookName}:`);
|
||||||
|
const sortedHookEntry = new Map([...hookEntry].sort(sortStringKeys));
|
||||||
|
hooks.set(hookName, sortedHookEntry);
|
||||||
|
for (const [pluginName, pluginEntry] of sortedHookEntry) {
|
||||||
|
lines.push(html ? ` <dt>${pluginName}:</dt><dd><dl>` : ` ${pluginName}:`);
|
||||||
|
const sortedPluginEntry = new Map([...pluginEntry].sort(sortStringKeys));
|
||||||
|
sortedHookEntry.set(pluginName, sortedPluginEntry);
|
||||||
|
for (const [partName, hookFnName] of sortedPluginEntry) {
|
||||||
|
lines.push(html
|
||||||
|
? ` <dt>${partName}:</dt><dd>${hookFnName}</dd>`
|
||||||
|
: ` ${partName}: ${hookFnName}`);
|
||||||
|
}
|
||||||
|
if (html) lines.push(' </dl></dd>');
|
||||||
|
}
|
||||||
|
if (html) lines.push(' </dl></dd>');
|
||||||
|
}
|
||||||
|
if (html) lines.push('</dl>');
|
||||||
|
return lines.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
const callInit = async () => {
|
const callInit = async () => {
|
||||||
|
|
|
@ -55,9 +55,10 @@ const extractHooks = (parts, hookSetName, normalizer) => {
|
||||||
try {
|
try {
|
||||||
hookFn = loadFn(hookFnName, hookName);
|
hookFn = loadFn(hookFnName, hookName);
|
||||||
if (!hookFn) throw new Error('Not a function');
|
if (!hookFn) throw new Error('Not a function');
|
||||||
} catch (exc) {
|
} catch (err) {
|
||||||
console.error(`Failed to load '${hookFnName}' for ` +
|
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
|
||||||
`'${part.full_name}/${hookSetName}/${hookName}': ${exc.toString()}`);
|
`part "${part.name}" hook set "${hookSetName}" hook "${hookName}": ` +
|
||||||
|
`${err.stack || err}`);
|
||||||
}
|
}
|
||||||
if (hookFn) {
|
if (hookFn) {
|
||||||
if (hooks[hookName] == null) hooks[hookName] = [];
|
if (hooks[hookName] == null) hooks[hookName] = [];
|
||||||
|
|
|
@ -170,7 +170,7 @@
|
||||||
|
|
||||||
<h2 data-l10n-id="pad.settings.about">About</h2>
|
<h2 data-l10n-id="pad.settings.about">About</h2>
|
||||||
<span data-l10n-id="pad.settings.poweredBy">Powered by</span>
|
<span data-l10n-id="pad.settings.poweredBy">Powered by</span>
|
||||||
<a href="https://etherpad.org">Etherpad-lite</a>
|
<a href="https://etherpad.org">Etherpad</a>
|
||||||
<% if (settings.exposeVersion) { %>(commit <%=settings.getGitCommit()%>)<% } %>
|
<% if (settings.exposeVersion) { %>(commit <%=settings.getGitCommit()%>)<% } %>
|
||||||
</div></div>
|
</div></div>
|
||||||
|
|
||||||
|
|
24
src/tests/backend/specs/pads-with-spaces.js
Normal file
24
src/tests/backend/specs/pads-with-spaces.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('../assert-legacy').strict;
|
||||||
|
|
||||||
|
let agent;
|
||||||
|
|
||||||
|
describe(__filename, function () {
|
||||||
|
before(async function () {
|
||||||
|
agent = await common.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pads with spaces, regression test for #4883', async function () {
|
||||||
|
await agent.get('/p/pads with spaces')
|
||||||
|
.expect(302)
|
||||||
|
.expect('location', 'pads_with_spaces');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pads with spaces and query, regression test for #4883', async function () {
|
||||||
|
await agent.get('/p/pads with spaces?showChat=true&noColors=false')
|
||||||
|
.expect(302)
|
||||||
|
.expect('location', 'pads_with_spaces?showChat=true&noColors=false');
|
||||||
|
});
|
||||||
|
});
|
5
src/tests/frontend/cypress/.gitignore
vendored
Normal file
5
src/tests/frontend/cypress/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
fixtures/*
|
||||||
|
plugins/*
|
||||||
|
support/*
|
||||||
|
videos/*
|
||||||
|
screenshots/*
|
10
src/tests/frontend/cypress/README.md
Normal file
10
src/tests/frontend/cypress/README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Cypress Etherpad guide
|
||||||
|
We don't install Etherpad as a dev dep or dep within Etherpad because it's not
|
||||||
|
our core Frontend testing tool
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
```
|
||||||
|
npm i -g cypress
|
||||||
|
cd src/tests/frontend/cypress/
|
||||||
|
cypress open
|
||||||
|
```
|
3
src/tests/frontend/cypress/cypress.json
Normal file
3
src/tests/frontend/cypress/cypress.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"baseUrl": "http://127.0.0.1:9001"
|
||||||
|
}
|
23
src/tests/frontend/cypress/integration/test.js
Normal file
23
src/tests/frontend/cypress/integration/test.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
Cypress.Commands.add('iframe', {prevSubject: 'element'},
|
||||||
|
($iframe) => new Cypress.Promise((resolve) => {
|
||||||
|
$iframe.ready(() => {
|
||||||
|
resolve($iframe.contents().find('body'));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe(__filename, () => {
|
||||||
|
it('Pad content exists', () => {
|
||||||
|
cy.visit('http://127.0.0.1:9001/p/test');
|
||||||
|
cy.wait(10000); // wait for Minified JS to be built...
|
||||||
|
cy.get('iframe[name="ace_outer"]', {timeout: 10000}).iframe()
|
||||||
|
.find('.line-number:first')
|
||||||
|
.should('have.text', '1');
|
||||||
|
cy.get('iframe[name="ace_outer"]').iframe()
|
||||||
|
.find('iframe[name="ace_inner"]').iframe()
|
||||||
|
.find('.ace-line:first')
|
||||||
|
.should('be.visible')
|
||||||
|
.should('have.text', 'Welcome to Etherpad!');
|
||||||
|
});
|
||||||
|
});
|
|
@ -42,7 +42,9 @@ try curl http://localhost:9001/p/minifyme -f -s >/dev/null
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
log "Running the load tests..."
|
log "Running the load tests..."
|
||||||
etherpad-loadtest -d 25
|
# -d is duration of test, -a is number of authors to test with
|
||||||
|
# by specifying the number of authors we set the overall rate of messages
|
||||||
|
etherpad-loadtest -d $1 -a $2
|
||||||
exit_code=$?
|
exit_code=$?
|
||||||
|
|
||||||
kill "$ep_pid" && wait "$ep_pid"
|
kill "$ep_pid" && wait "$ep_pid"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue