diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml
index f2e2215aa..bff9228a7 100644
--- a/.github/workflows/frontend-admin-tests.yml
+++ b/.github/workflows/frontend-admin-tests.yml
@@ -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]
diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml
index 1b57f6514..bc138416c 100644
--- a/.github/workflows/frontend-tests.yml
+++ b/.github/workflows/frontend-tests.yml
@@ -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 }}
diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml
index 787ef1ddd..f4cbbb58d 100644
--- a/.github/workflows/load-test.yml
+++ b/.github/workflows/load-test.yml
@@ -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
diff --git a/.github/workflows/major-version-git-pull-update.yml b/.github/workflows/major-version-git-pull-update.yml
new file mode 100644
index 000000000..5d3b0d748
--- /dev/null
+++ b/.github/workflows/major-version-git-pull-update.yml
@@ -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
diff --git a/.github/workflows/windows-zip.yml b/.github/workflows/windows-zip.yml
index 6e5f80bd4..42a99a191 100644
--- a/.github/workflows/windows-zip.yml
+++ b/.github/workflows/windows-zip.yml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 906212a53..12a9fd431 100644
--- a/CHANGELOG.md
+++ b/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
### Notable fixes
diff --git a/Dockerfile b/Dockerfile
index 29e2b5abc..660aacf9c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/README.md b/README.md
index 02eeac3f6..4bc972285 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,10 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha
### 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/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
-
   
+
[](https://discord.com/invite/daEjfhw) [](https://static.etherpad.org/index.html)  
# Installation
diff --git a/src/bin/plugins/stalePlugins.js b/src/bin/plugins/stalePlugins.js
new file mode 100644
index 000000000..8b2427f27
--- /dev/null
+++ b/src/bin/plugins/stalePlugins.js
@@ -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}`)
+ }
+ }
+})();
diff --git a/src/locales/sl.json b/src/locales/sl.json
index be11d4a76..7a768c058 100644
--- a/src/locales/sl.json
+++ b/src/locales/sl.json
@@ -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)",
diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js
index 7c0db0973..543f04c0f 100644
--- a/src/node/hooks/express/adminplugins.js
+++ b/src/node/hooks/express/adminplugins.js
@@ -26,8 +26,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
epVersion,
installedPlugins: `
${plugins.formatPlugins().replace(/, /g, '\n')}`,
installedParts: `${plugins.formatParts()}`,
- installedServerHooks: `${plugins.formatHooks()}
`,
- installedClientHooks: `${plugins.formatHooks('client_hooks')}
`,
+ installedServerHooks: `${plugins.formatHooks('hooks', true)}
`,
+ installedClientHooks: `${plugins.formatHooks('client_hooks', true)}
`,
latestVersion: UpdateCheck.getLatestVersion(),
req,
}));
diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js
index 30297479a..b805fc4ba 100644
--- a/src/node/hooks/express/padurlsanitize.js
+++ b/src/node/hooks/express/padurlsanitize.js
@@ -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 ${realURL}`);
}
diff --git a/src/node/server.js b/src/node/server.js
index aec0d442f..fc62b4471 100755
--- a/src/node/server.js
+++ b/src/node/server.js
@@ -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) {
diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js
index 7f5c2ab2d..d47443733 100644
--- a/src/node/utils/ImportEtherpad.js
+++ b/src/node/utils/ImportEtherpad.js
@@ -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}`);
}
diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js
index 4874432b1..7969f9c14 100644
--- a/src/node/utils/Minify.js
+++ b/src/node/utils/Minify.js
@@ -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)));
diff --git a/src/node/utils/MinifyWorker.js b/src/node/utils/MinifyWorker.js
index 31b5ec319..1ef6490ee 100644
--- a/src/node/utils/MinifyWorker.js
+++ b/src/node/utils/MinifyWorker.js
@@ -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:
diff --git a/src/package-lock.json b/src/package-lock.json
index f22457e10..14421f869 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -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",
diff --git a/src/package.json b/src/package.json
index 6f3e51612..ada568d95 100644
--- a/src/package.json
+++ b/src/package.json
@@ -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"
}
diff --git a/src/static/js/ace.js b/src/static/js/ace.js
index 4038096ee..44556dd0f 100644
--- a/src/static/js/ace.js
+++ b/src/static/js/ace.js
@@ -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) => ``;
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: , 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('');
- }
- for (const file of remoteFiles) {
- buffer.push(``);
+ for (const file of files) {
+ buffer.push(``);
}
};
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 = '';
+ const doctype = '';
- const iframeHTML = [];
+ const iframeHTML = [];
- iframeHTML.push(doctype);
- iframeHTML.push(``);
+ iframeHTML.push(doctype);
+ iframeHTML.push(``);
+ pushStyleTagsFor(iframeHTML, includedCSS);
+ const requireKernelUrl =
+ absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
+ iframeHTML.push(``);
+ // 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(``);
+ }
- // 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(``);
- // fill the cache
- iframeHTML.push(``);
- iframeHTML.push(``);
+ 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('');
- iframeHTML.push('');
+ hooks.callAll('aceInitInnerdocbodyHead', {
+ iframeHTML,
+ });
- hooks.callAll('aceInitInnerdocbodyHead', {
- iframeHTML,
- });
+ iframeHTML.push(' ');
- iframeHTML.push(' ');
+ 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, ``];
+ 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(
+ '',
+ '',
+ scriptTag(outerScript),
+ '',
+ '',
+ '',
+ 'x
',
+ '');
- const outerHTML =
- [doctype, ``];
+ 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(
- '',
- '',
- scriptTag(outerScript),
- '',
- '',
- '',
- 'x
',
- '');
-
- 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;
diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js
index 4858aa1b4..8fef627b5 100644
--- a/src/static/js/ace2_inner.js
+++ b/src/static/js/ace2_inner.js
@@ -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);
};
diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js
index c250008bb..cf921cd47 100644
--- a/src/static/js/contentcollector.js
+++ b/src/static/js/contentcollector.js
@@ -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;
diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js
index a32af1644..75481fe5e 100644
--- a/src/static/js/pad_editor.js
+++ b/src/static/js/pad_editor.js
@@ -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);
diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js
index b705fb73a..74fbbafc8 100644
--- a/src/static/js/pluginfw/plugins.js
+++ b/src/static/js/pluginfw/plugins.js
@@ -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(`${hook.hook_name}${hook.hook_fn_name} ` +
- `from ${hook.part.full_name}`);
+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 `${res.join('\n')}
`;
+ const lines = [];
+ const sortStringKeys = (a, b) => String(a[0]).localeCompare(b[0]);
+ if (html) lines.push('');
+ hooks = new Map([...hooks].sort(sortStringKeys));
+ for (const [hookName, hookEntry] of hooks) {
+ lines.push(html ? ` - ${hookName}:
` : ` ${hookName}:`);
+ const sortedHookEntry = new Map([...hookEntry].sort(sortStringKeys));
+ hooks.set(hookName, sortedHookEntry);
+ for (const [pluginName, pluginEntry] of sortedHookEntry) {
+ lines.push(html ? ` - ${pluginName}:
` : ` ${pluginName}:`);
+ const sortedPluginEntry = new Map([...pluginEntry].sort(sortStringKeys));
+ sortedHookEntry.set(pluginName, sortedPluginEntry);
+ for (const [partName, hookFnName] of sortedPluginEntry) {
+ lines.push(html
+ ? ` - ${partName}:
- ${hookFnName}
`
+ : ` ${partName}: ${hookFnName}`);
+ }
+ if (html) lines.push('
');
+ }
+ if (html) lines.push('
');
+ }
+ if (html) lines.push('
');
+ return lines.join('\n');
};
const callInit = async () => {
diff --git a/src/static/js/pluginfw/shared.js b/src/static/js/pluginfw/shared.js
index a78697a2a..2c81ccd81 100644
--- a/src/static/js/pluginfw/shared.js
+++ b/src/static/js/pluginfw/shared.js
@@ -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] = [];
diff --git a/src/templates/pad.html b/src/templates/pad.html
index 39815c720..26243806a 100644
--- a/src/templates/pad.html
+++ b/src/templates/pad.html
@@ -170,7 +170,7 @@
About
Powered by
- Etherpad-lite
+ Etherpad
<% if (settings.exposeVersion) { %>(commit <%=settings.getGitCommit()%>)<% } %>
diff --git a/src/tests/backend/specs/pads-with-spaces.js b/src/tests/backend/specs/pads-with-spaces.js
new file mode 100644
index 000000000..0db99865b
--- /dev/null
+++ b/src/tests/backend/specs/pads-with-spaces.js
@@ -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');
+ });
+});
diff --git a/src/tests/frontend/cypress/.gitignore b/src/tests/frontend/cypress/.gitignore
new file mode 100644
index 000000000..b3bf4d3e2
--- /dev/null
+++ b/src/tests/frontend/cypress/.gitignore
@@ -0,0 +1,5 @@
+fixtures/*
+plugins/*
+support/*
+videos/*
+screenshots/*
diff --git a/src/tests/frontend/cypress/README.md b/src/tests/frontend/cypress/README.md
new file mode 100644
index 000000000..4cc3f8121
--- /dev/null
+++ b/src/tests/frontend/cypress/README.md
@@ -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
+```
diff --git a/src/tests/frontend/cypress/cypress.json b/src/tests/frontend/cypress/cypress.json
new file mode 100644
index 000000000..780d73fae
--- /dev/null
+++ b/src/tests/frontend/cypress/cypress.json
@@ -0,0 +1,3 @@
+{
+ "baseUrl": "http://127.0.0.1:9001"
+}
diff --git a/src/tests/frontend/cypress/integration/test.js b/src/tests/frontend/cypress/integration/test.js
new file mode 100644
index 000000000..893d4b669
--- /dev/null
+++ b/src/tests/frontend/cypress/integration/test.js
@@ -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!');
+ });
+});
diff --git a/src/tests/frontend/travis/runnerLoadTest.sh b/src/tests/frontend/travis/runnerLoadTest.sh
index 3fce737bc..8a813c20a 100755
--- a/src/tests/frontend/travis/runnerLoadTest.sh
+++ b/src/tests/frontend/travis/runnerLoadTest.sh
@@ -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"