Merge branch 'develop'

This commit is contained in:
John McLear 2021-03-05 07:28:47 +00:00
commit b99c2cae22
No known key found for this signature in database
GPG key ID: 599378BB471BCE1C
31 changed files with 528 additions and 306 deletions

View file

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

View file

@ -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]
@ -35,7 +36,7 @@ jobs:
run: |
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:
username: ${{ secrets.SAUCE_USERNAME }}
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}
@ -78,6 +79,7 @@ jobs:
ep_align
ep_author_hover
ep_cursortrace
ep_embedmedia
ep_font_size
ep_hash_auth
ep_headings2
@ -114,7 +116,7 @@ jobs:
- 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
- uses: saucelabs/sauce-connect-action@v1
with:
username: ${{ secrets.SAUCE_USERNAME }}
accessKey: ${{ secrets.SAUCE_ACCESS_KEY }}

View file

@ -28,7 +28,7 @@ jobs:
run: sudo npm install -g etherpad-load-test
- name: Run load test
run: src/tests/frontend/travis/runnerLoadTest.sh
run: src/tests/frontend/travis/runnerLoadTest.sh 25 50
withplugins:
# run on pushes to any branch
@ -80,4 +80,32 @@ jobs:
# configures some settings and runs npm run 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

View 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

View file

@ -65,11 +65,13 @@ jobs:
- name: Extract Etherpad
run: 7z x etherpad-lite-win.zip -oetherpad
- name: list
run: dir etherpad
- name: Install Cypress
run: npm install cypress -g
- 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
cd src\tests\frontend
cypress run --spec cypress\integration\test.js --config-file cypress\cypress.json

View file

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

View file

@ -40,9 +40,20 @@ ENV NODE_ENV=production
#
# Running as non-root enables running this image in platforms like OpenShift
# 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
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
WORKDIR /opt/etherpad-lite
WORKDIR "${EP_DIR}"
COPY --chown=etherpad:0 ./ ./
COPY --chown=etherpad:etherpad ./ ./
# install node dependencies for Etherpad
RUN src/bin/installDeps.sh && \
rm -rf ~/.npm/_cacache
# Install the plugins, if ETHERPAD_PLUGINS is not empty.
#
# 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
RUN [ -z "${ETHERPAD_PLUGINS}" ] || npm install ${ETHERPAD_PLUGINS}
# 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 .
EXPOSE 9001

View file

@ -23,10 +23,10 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha
### 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)
[![Frontend admin tests powered by Sauce Labs](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 powered by Sauce Labs](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) [![Sauce Test Status](https://saucelabs.com/buildstatus/etherpad.svg)](https://saucelabs.com/u/etherpad) [![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)
<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)](https://discord.com/invite/daEjfhw) [![Etherpad plugins](https://img.shields.io/endpoint?url=https%3A%2F%2Fstatic.etherpad.org%2Fshields.json&color=%2344b492 "Etherpad plugins")](https://static.etherpad.org/index.html) ![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

View 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}`)
}
}
})();

View file

@ -9,10 +9,12 @@
"Upwinxp"
]
},
"admin_plugins.description": "Opis",
"admin_plugins.last-update": "Zadnja posodobitev",
"admin_plugins.name": "Ime",
"admin_plugins.version": "Različica",
"admin_settings": "Nastavitve",
"admin_settings.current_save.value": "Shrani nastavitve",
"index.newPad": "Nov dokument",
"index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:",
"pad.toolbar.bold.title": "Krepko (Ctrl-B)",

View file

@ -26,8 +26,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
epVersion,
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
installedParts: `<pre>${plugins.formatParts()}</pre>`,
installedServerHooks: `<div>${plugins.formatHooks()}</div>`,
installedClientHooks: `<div>${plugins.formatHooks('client_hooks')}</div>`,
installedServerHooks: `<div>${plugins.formatHooks('hooks', true)}</div>`,
installedClientHooks: `<div>${plugins.formatHooks('client_hooks', true)}</div>`,
latestVersion: UpdateCheck.getLatestVersion(),
req,
}));

View file

@ -18,7 +18,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
next();
} else {
// 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.status(302).send(`You should be redirected to <a href="${realURL}">${realURL}</a>`);
}

View file

@ -144,7 +144,7 @@ exports.start = async () => {
.join(', ');
logger.info(`Installed plugins: ${installedPlugins}`);
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('createServer');
} catch (err) {

View file

@ -18,15 +18,14 @@
const db = require('../db/DB');
const hooks = require('../../static/js/pluginfw/hooks');
const supportedElems = require('../../static/js/contentcollector').supportedElems;
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);
supportedElems.push(element);
});
Object.keys(records).forEach(async (key) => {
@ -65,7 +64,7 @@ exports.setPadRaw = (padId, r) => {
if (value.pool) {
for (const attrib of Object.keys(value.pool.numToAttrib)) {
const attribName = value.pool.numToAttrib[attrib][0];
if (blockElems.indexOf(attribName) === -1) {
if (supportedElems.indexOf(attribName) === -1) {
console.warn('Plugin missing: ' +
`You might want to install a plugin to support this node name: ${attribName}`);
}

View file

@ -123,6 +123,15 @@ const sanitizePathname = (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
* @param req the Express request
@ -139,11 +148,11 @@ const minify = async (req, res) => {
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';
// Backward compatibility for plugins that require() files from old paths.
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
if (newLocation != null) {
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
filename = newLocation;
}
/* Handle static files for plugins/libraries:
@ -159,7 +168,7 @@ 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, path.join(pluginPath, libraryPath));
filename = 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
@ -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.
const statFile = async (filename, dirStatLimit) => {
/*
@ -270,7 +241,7 @@ const statFile = async (filename, dirStatLimit) => {
} else {
let stats;
try {
stats = await fs.stat(path.join(ROOT_DIR, filename));
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
} catch (err) {
if (err.code === 'ENOENT') {
// Stat the directory instead.
@ -357,9 +328,8 @@ 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(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)));

View file

@ -12,7 +12,7 @@ const compressJS = (content) => Terser.minify(content);
const compressCSS = (filename, ROOT_DIR) => new Promise((res, rej) => {
try {
const absPath = path.join(ROOT_DIR, filename);
const absPath = path.resolve(ROOT_DIR, filename);
/*
* Changes done to migrate CleanCSS 3.x -> 4.x:

53
src/package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "ep_etherpad-lite",
"version": "1.8.11",
"version": "1.8.12",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1413,9 +1413,9 @@
"dev": true
},
"dirty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz",
"integrity": "sha1-cO3SuZlUHcmXT9Ooy9DGcP4jYHg="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.1.tgz",
"integrity": "sha512-l/SMZcT+MjqOPpjarzJ8nQdxtxurURJM7js1l0Q2TQWtNbPzDYzkK++HlbT+XmM+adPFNdb3SOlVz9Jr7Df7xQ=="
},
"doctrine": {
"version": "3.0.0",
@ -1808,11 +1808,28 @@
}
},
"eslint-config-etherpad": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.25.tgz",
"integrity": "sha512-KYTGf08dlwvsg05Y2hm0zurCwVMyZrsxGRnPEhL2wclk26xhnPYfNfruQQqk7nghfWFLrAL+VscnkZLCQEPBXQ==",
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/eslint-config-etherpad/-/eslint-config-etherpad-1.0.26.tgz",
"integrity": "sha512-xPnDnJIpQuYJNRYGIHIucct0U6CtciyZKItpet+NqoGJgxUMkwAXgD5bzuXQvd9u4I2aj/kRU1BIL2DbAGe+pA==",
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
@ -2093,9 +2110,9 @@
}
},
"express-rate-limit": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.3.tgz",
"integrity": "sha512-cjQH+oDrEPXxc569XvxhHC6QXqJiuBT6BhZ70X3bdAImcnHnTNMVuMAJaT0TXPoRiEErUrVPRcOTpZpM36VbOQ=="
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.5.tgz",
"integrity": "sha512-fv9mf4hWRKZHVlY8ChVNYnGxa49m0zQ6CrJxNiXe2IjJPqicrqoA/JOyBbvs4ufSSLZ6NTzhtgEyLcdfbe+Q6Q=="
},
"express-session": {
"version": "1.17.1",
@ -7522,11 +7539,11 @@
}
},
"resolve": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"requires": {
"is-core-module": "^2.1.0",
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
}
},
@ -8419,14 +8436,14 @@
}
},
"ueberdb2": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.3.1.tgz",
"integrity": "sha512-uhUSJfI5sNWdiXxae0kOg88scaMIKcV0CVeojwPQzgm93vQVuGyCqS1g1i3gTZel6SwmXRFtYtfmtAmiEe+HBQ==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.3.2.tgz",
"integrity": "sha512-7Ub5jDsIS+qjjsNV7yp1CHXHVe2K9ZUpwaHi9BZf3ai0DxtuHOfMada1wxL6iyEjwYXh/Nsu80iyId51wHFf4A==",
"requires": {
"async": "^3.2.0",
"cassandra-driver": "^4.5.1",
"channels": "0.0.4",
"dirty": "^1.1.0",
"dirty": "^1.1.1",
"elasticsearch": "^16.7.1",
"mongodb": "^3.6.3",
"mssql": "^7.0.0-beta.2",

View file

@ -41,7 +41,7 @@
"etherpad-require-kernel": "1.0.9",
"etherpad-yajsml": "0.0.4",
"express": "4.17.1",
"express-rate-limit": "5.2.3",
"express-rate-limit": "5.2.5",
"express-session": "1.17.1",
"find-root": "1.1.0",
"formidable": "1.2.2",
@ -61,7 +61,7 @@
"rehype": "^10.0.0",
"rehype-minify-whitespace": "^4.0.5",
"request": "2.88.2",
"resolve": "1.19.0",
"resolve": "1.20.0",
"security": "1.0.0",
"semver": "5.7.1",
"socket.io": "^2.4.1",
@ -69,7 +69,7 @@
"threads": "^1.4.0",
"tiny-worker": "^2.3.0",
"tinycon": "0.6.8",
"ueberdb2": "^1.3.1",
"ueberdb2": "^1.3.2",
"underscore": "1.12.0",
"unorm": "1.6.0",
"wtfnode": "^0.8.4"
@ -79,7 +79,8 @@
},
"devDependencies": {
"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-mocha": "^8.0.0",
"eslint-plugin-node": "^11.1.0",
@ -155,9 +156,10 @@
],
"excludedFiles": [
"**/.eslintrc.js",
"tests/frontend/travis/**/*",
"tests/frontend/cypress/**/*",
"tests/frontend/helper.js",
"tests/frontend/helper/**/*",
"tests/frontend/travis/**/*",
"tests/ratelimit/**/*"
],
"extends": "etherpad/tests",
@ -195,6 +197,7 @@
],
"excludedFiles": [
"**/.eslintrc.js",
"tests/frontend/cypress/**/*",
"tests/frontend/helper.js",
"tests/frontend/helper/**/*",
"tests/frontend/travis/**/*"
@ -215,6 +218,12 @@
}
]
},
{
"files": [
"tests/frontend/cypress/**/*"
],
"extends": "etherpad/tests/cypress"
},
{
"files": [
"tests/frontend/travis/**/*"
@ -237,6 +246,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.11",
"version": "1.8.12",
"license": "Apache-2.0"
}

View file

@ -27,16 +27,20 @@
const hooks = require('./pluginfw/hooks');
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 =
(source) => `<script type="text/javascript">\n${source.replace(/<\//g, '<\\/')}</script>`;
const Ace2Editor = function () {
const ace2 = Ace2Editor;
let info = {
editor: this,
id: (ace2.registry.nextId++),
};
let info = {editor: this};
window.ace2EditorInfo = info; // Make it accessible to iframes.
let loaded = false;
let actionsPendingInit = [];
@ -52,8 +56,6 @@ const Ace2Editor = function () {
actionsPendingInit = [];
};
ace2.registry[info.id] = info;
// The following functions (prefixed by 'ace_') are exposed by editor, but
// execution is delayed until init is complete
const aceFunctionsPendingInit = [
@ -89,8 +91,6 @@ const Ace2Editor = function () {
this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n';
this.getFrame = () => info.frame || null;
this.getDebugProperty = (prop) => info.ace_getDebugProperty(prop);
this.getInInternationalComposition =
@ -109,207 +109,140 @@ const Ace2Editor = function () {
// returns array of {error: <browser Error object>, time: +new Date()}
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 sorted = sortFilesByEmbeded(files);
const embededFiles = sorted.embeded;
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)}"/>`);
for (const file of files) {
buffer.push(`<link rel="stylesheet" type="text/css" href="${absUrl(encodeURI(file))}"/>`);
}
};
this.destroy = pendingInit(() => {
info.ace_dispose();
info.frame.parentNode.removeChild(info.frame);
delete ace2.registry[info.id];
delete window.ace2EditorInfo;
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);
info.onEditorReady = () => {
loaded = true;
doActionsPendingInit();
doneFunc();
};
const includedCSS = [
'../static/css/iframe_editor.css',
`../static/css/pad.css?v=${clientVars.randomVersionString}`,
...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(`<html class='inner-editor ${clientVars.skinVariants}'><head>`);
iframeHTML.push(doctype);
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
// and compressed, putting the compressed code from the named file directly into the
// source here.
// these lines must conform to a specific format because they are passed by the build script:
let includedCSS = [];
let $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); };
$$INCLUDE_CSS('../static/css/iframe_editor.css');
iframeHTML.push(scriptTag(`(async () => {
parent.parent.debugLog('Ace2Editor.init() inner frame ready');
const require = window.require;
require.setRootURI(${JSON.stringify(absUrl('../javascripts/src'))});
require.setLibraryURI(${JSON.stringify(absUrl('../javascripts/lib'))});
require.setGlobalKeyPath('require');
// disableCustomScriptsAndStyles can be used to disable loading of custom scripts
if (!clientVars.disableCustomScriptsAndStyles) {
$$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`);
}
// intentially moved before requiring client_plugins to save a 307
window.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
window.plugins.adoptPluginsFromAncestorsOf(window);
let 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}`);
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
pushStyleTagsFor(iframeHTML, includedCSS);
iframeHTML.push(`<script type="text/javascript" src="../static/js/require-kernel.js?v=${clientVars.randomVersionString}"></script>`);
// fill the cache
iframeHTML.push(`<script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/ace2_inner.js?callback=require.define&v=${clientVars.randomVersionString}"></script>`);
iframeHTML.push(`<script type="text/javascript" src="../javascripts/lib/ep_etherpad-lite/static/js/ace2_common.js?callback=require.define&v=${clientVars.randomVersionString}"></script>`);
parent.parent.debugLog('Ace2Editor.init() waiting for plugins');
await new Promise((resolve, reject) => window.plugins.ensure(
(err) => err != null ? reject(err) : resolve()));
parent.parent.debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
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(
`\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>');
iframeHTML.push('<style type="text/css" title="dynamicsyntax"></style>');
hooks.callAll('aceInitInnerdocbodyHead', {
iframeHTML,
});
hooks.callAll('aceInitInnerdocbodyHead', {
iframeHTML,
});
iframeHTML.push('</head><body id="innerdocbody" class="innerdocbody" role="application" ' +
'spellcheck="false">&nbsp;</body></html>');
iframeHTML.push('</head><body id="innerdocbody" class="innerdocbody" role="application" ' +
'class="syntax" spellcheck="false">&nbsp;</body></html>');
const outerScript = `(async () => {
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 gt = typeof globalThis === 'object' ? globalThis : window;
gt.ChildAccessibleAce2Editor = Ace2Editor;
const outerHTML =
[doctype, `<html class="inner-editor outerdoc ${clientVars.skinVariants}"><head>`];
pushStyleTagsFor(outerHTML, includedCSS);
const outerScript = `\
editorId = ${JSON.stringify(info.id)};\n\
editorInfo = parent.ChildAccessibleAce2Editor.registry[editorId];\n\
window.onload = function () {\n\
window.onload = null;\n\
setTimeout(function () {\n\
var iframe = document.createElement("IFRAME");\n\
iframe.name = "ace_inner";\n\
iframe.title = "pad";\n\
iframe.scrolling = "no";\n\
var outerdocbody = document.getElementById("outerdocbody");\n\
iframe.frameBorder = 0;\n\
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\
}`;
// 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 outerHTML =
[doctype, `<html class="inner-editor outerdoc ${clientVars.skinVariants}"><head>`];
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);
includedCSS = [];
$$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;
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.write(outerHTML.join(''));
editorDocument.close();
})();
});
loaded = true;
doActionsPendingInit();
debugLog('Ace2Editor.init() done');
};
};
Ace2Editor.registry = {
nextId: 1,
};
exports.Ace2Editor = Ace2Editor;

View file

@ -30,7 +30,7 @@ const htmlPrettyEscape = Ace2Common.htmlPrettyEscape;
const noop = Ace2Common.noop;
const hooks = require('./pluginfw/hooks');
function Ace2Inner() {
function Ace2Inner(editorInfo) {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils;
const makeContentCollector = require('./contentcollector').makeContentCollector;
@ -57,7 +57,6 @@ function Ace2Inner() {
let thisAuthor = '';
let disposed = false;
const editorInfo = parent.editorInfo;
const focus = () => {
window.focus();
@ -3894,9 +3893,9 @@ function Ace2Inner() {
documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset);
editorInfo.ace_performDocumentApplyAttributesToRange =
(...args) => documentAttributeManager.setAttributesOnRange(args);
(...args) => documentAttributeManager.setAttributesOnRange(...args);
this.init = () => {
this.init = (cb) => {
$(document).ready(() => {
doc = document; // defined as a var in scope outside
inCallStack('setup', () => {
@ -3928,14 +3927,12 @@ function Ace2Inner() {
documentAttributeManager,
});
scheduler.setTimeout(() => {
parent.readyFunc(); // defined in code that sets up the inner iframe
}, 0);
scheduler.setTimeout(cb, 0);
});
};
}
exports.init = () => {
const editor = new Ace2Inner();
editor.init();
exports.init = (editorInfo, cb) => {
const editor = new Ace2Inner(editorInfo);
editor.init(cb);
};

View file

@ -55,6 +55,28 @@ const getAttribute = (n, a) => {
if (n.attribs != null) return n.attribs[a];
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 _blockElems = {
@ -66,6 +88,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
_blockElems[element] = 1;
supportedElems.push(element);
});
const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
@ -315,9 +338,11 @@ 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: ' +
if (!isBlock && node.name && (node.name !== 'body')) {
if (supportedElems.indexOf(node.name) === -1) {
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);
@ -701,3 +726,4 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
exports.sanitizeUnicode = sanitizeUnicode;
exports.makeContentCollector = makeContentCollector;
exports.supportedElems = supportedElems;

View file

@ -56,7 +56,8 @@ const padeditor = (() => {
};
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);
if (pad.getIsDebugEnabled()) {
self.ace.setProperty('dmesg', pad.dmesg);

View file

@ -28,16 +28,48 @@ exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n');
exports.formatHooks = (hookSetName) => {
const res = [];
const hooks = pluginUtils.extractHooks(defs.parts, hookSetName || 'hooks');
for (const registeredHooks of Object.values(hooks)) {
for (const hook of registeredHooks) {
res.push(`<dt>${hook.hook_name}</dt><dd>${hook.hook_fn_name} ` +
`from ${hook.part.full_name}</dd>`);
exports.formatHooks = (hookSetName, html) => {
let hooks = new Map();
for (const [pluginName, def] of Object.entries(defs.plugins)) {
for (const part of def.parts) {
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
let hookEntry = hooks.get(hookName);
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 () => {

View file

@ -55,9 +55,10 @@ const extractHooks = (parts, hookSetName, normalizer) => {
try {
hookFn = loadFn(hookFnName, hookName);
if (!hookFn) throw new Error('Not a function');
} catch (exc) {
console.error(`Failed to load '${hookFnName}' for ` +
`'${part.full_name}/${hookSetName}/${hookName}': ${exc.toString()}`);
} catch (err) {
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
`part "${part.name}" hook set "${hookSetName}" hook "${hookName}": ` +
`${err.stack || err}`);
}
if (hookFn) {
if (hooks[hookName] == null) hooks[hookName] = [];

View file

@ -170,7 +170,7 @@
<h2 data-l10n-id="pad.settings.about">About</h2>
<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()%>)<% } %>
</div></div>

View 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
View file

@ -0,0 +1,5 @@
fixtures/*
plugins/*
support/*
videos/*
screenshots/*

View 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
```

View file

@ -0,0 +1,3 @@
{
"baseUrl": "http://127.0.0.1:9001"
}

View 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!');
});
});

View file

@ -42,7 +42,9 @@ try curl http://localhost:9001/p/minifyme -f -s >/dev/null
sleep 10
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=$?
kill "$ep_pid" && wait "$ep_pid"