From 377883db9825c4d6b9b29f968761fa6843efb87f Mon Sep 17 00:00:00 2001 From: webzwo0i Date: Sun, 28 Feb 2021 04:34:43 +0100 Subject: [PATCH 01/45] fix pads with spaces (#4884) --- src/node/hooks/express/padurlsanitize.js | 2 +- src/tests/backend/specs/pads-with-spaces.js | 24 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/tests/backend/specs/pads-with-spaces.js 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/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'); + }); +}); From 16e6496eb440b80bc59e95a46ff7aff874ccccd7 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 28 Feb 2021 01:28:11 -0500 Subject: [PATCH 02/45] deps: Update ueberdb2 to fix dirty DB bug --- CHANGELOG.md | 7 +++++++ src/package-lock.json | 14 +++++++------- src/package.json | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 906212a53..494545892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.8.12 + +### Notable fixes + +* Fixed a bug in the `dirty` database driver that sometimes caused Node.js to + crash during shutdown and lose buffered database writes. + # 1.8.11 ### Notable fixes diff --git a/src/package-lock.json b/src/package-lock.json index f22457e10..26c65378b 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -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", @@ -8419,14 +8419,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..1a0eb74d4 100644 --- a/src/package.json +++ b/src/package.json @@ -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" From 3667f2ca0ed91f010f4c39aad26fba780189b5d4 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 28 Feb 2021 00:15:28 -0500 Subject: [PATCH 03/45] Ace2Inner: Fix missing spread operator on `args` This fixes a bug that was introduced in commit c38c34bef47a74467394797b34165641a87ac620. --- CHANGELOG.md | 2 ++ src/static/js/ace2_inner.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 494545892..331a1b98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * 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) # 1.8.11 diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 4858aa1b4..5ae4f0660 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -3894,7 +3894,7 @@ function Ace2Inner() { documentAttributeManager = new AttributeManager(rep, performDocumentApplyChangeset); editorInfo.ace_performDocumentApplyAttributesToRange = - (...args) => documentAttributeManager.setAttributesOnRange(args); + (...args) => documentAttributeManager.setAttributesOnRange(...args); this.init = () => { $(document).ready(() => { From b0d78d662e2471c9ca55433abcdc7d76a6471efa Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 28 Feb 2021 09:26:43 +0000 Subject: [PATCH 04/45] tests: include ep_embedmedia in tests (#4889) --- .github/workflows/frontend-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 1b57f6514..6be1d8529 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -78,6 +78,7 @@ jobs: ep_align ep_author_hover ep_cursortrace + ep_embedmedia ep_font_size ep_hash_auth ep_headings2 From 99b3918f2c4167e6ab2ec18df739cd4f99dbe67b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 28 Feb 2021 05:21:45 -0500 Subject: [PATCH 05/45] Minify: Compatibility for all `vendors/*.js` files --- src/node/utils/Minify.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 4874432b1..f48260216 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: From a6ac0940cdcd7419789c59185a9a8ca3c9d65e5c Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 28 Feb 2021 11:15:51 +0000 Subject: [PATCH 06/45] Link discord and plugins badge with a meaningful url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02eeac3f6..a196c7197 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha [![Frontend admin tests](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml) [![Frontend tests](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml) [![Windows Installer](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml/badge.svg?color=%2344b492)](https://github.com/ether/etherpad-lite/actions/workflows/windows-installer.yml) ### Engagement -Docker Pulls ![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) +Docker Pulls [![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 From 237bab7e3e2395dc84958e5ba9634f6b153206ed Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 28 Feb 2021 15:51:39 +0000 Subject: [PATCH 07/45] branding: change etherpad lite to etherpad in Pad settings modal --- src/templates/pad.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()%>)<% } %> From 35ae08ca7774f3d944644b7cd798bd45c1b1df34 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 28 Feb 2021 16:05:16 +0000 Subject: [PATCH 08/45] tests: run a long(1+ hr) load test --- .github/workflows/load-test.yml | 32 +++++++++++++++++++-- src/tests/frontend/travis/runnerLoadTest.sh | 4 ++- 2 files changed, 33 insertions(+), 3 deletions(-) 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/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" From b3d7f5d63ea125ea6223b2a536f6ec124e8706fd Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 28 Feb 2021 13:29:47 +0000 Subject: [PATCH 09/45] pluginTools: stalePlugins.js Outputs a list of pluginnames and email address for maintainers to contact. Useful for me to bump folks to maintain there stuff and stop it getting stale :) --- src/bin/plugins/stalePlugins.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/bin/plugins/stalePlugins.js 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}`) + } + } +})(); From b0f16bb1f1d41ae6de25be9cee45956643dd4a96 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 1 Mar 2021 13:02:43 +0000 Subject: [PATCH 10/45] Use stable sauce version --- .github/workflows/frontend-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 6be1d8529..ed205e004 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -35,7 +35,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 }} @@ -115,7 +115,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 }} From 64e9e7fcda79e8e354a783aeff66d69ad6d79faf Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 1 Mar 2021 14:31:55 +0000 Subject: [PATCH 11/45] tests: Frontend test Windows ZIP (#4894) * tests: Frontend test Windows ZIP This PR introduces Frontend testing within Github actions! We're depending a lot on saucelabs recently and that's fine but sometimes we just want to quickly do a frontend simple test on a weird environment (IE windows build) so this PR solves that problem. Things to note. It still builds the windows .zip if the cypress tests fail. It does not add any heavy deps to Etherpad as cypress must be installed in CI. Cypress is responsible for running the Etherpad instance. It's up to us how much we use this or not, I know it introduces a bunch of technical debt but I tried to keep that a minimum by compartmentalizing things and documenting where required. * Update .github/workflows/windows-zip.yml Co-authored-by: Richard Hansen * remove timeouts * Move folder structure up a level * Update windows-zip.yml * Update test.js Co-authored-by: Richard Hansen --- .github/workflows/windows-zip.yml | 6 +++-- src/tests/frontend/cypress/.gitignore | 5 +++++ src/tests/frontend/cypress/README.md | 10 +++++++++ src/tests/frontend/cypress/cypress.json | 3 +++ .../frontend/cypress/integration/test.js | 22 +++++++++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/tests/frontend/cypress/.gitignore create mode 100644 src/tests/frontend/cypress/README.md create mode 100644 src/tests/frontend/cypress/cypress.json create mode 100644 src/tests/frontend/cypress/integration/test.js 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/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..300a555d5 --- /dev/null +++ b/src/tests/frontend/cypress/integration/test.js @@ -0,0 +1,22 @@ +'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', async () => { + cy.visit('http://127.0.0.1:9001/p/test'); + 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!'); + }); +}); From c9c8b27854077268ebb38413024dba6c1bcda236 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 28 Feb 2021 21:53:41 -0500 Subject: [PATCH 12/45] ace: Delete unused `Ace2Editor.getFrame()` method --- src/static/js/ace.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 4038096ee..e7e07cb75 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -89,8 +89,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 = From b3416c4eeb02c3ece4df11189cc009ca18a3a503 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 25 Feb 2021 22:29:11 -0500 Subject: [PATCH 13/45] ace: Delete ignored class attribute --- src/static/js/ace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index e7e07cb75..dd3fb832f 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -222,7 +222,7 @@ plugins.ensure(function () {\n\ }); iframeHTML.push(' '); + 'spellcheck="false"> '); // eslint-disable-next-line node/no-unsupported-features/es-builtins const gt = typeof globalThis === 'object' ? globalThis : window; From c9b1f17f250b8e34a88eab79d7c8c46a85502cb8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 25 Feb 2021 22:32:17 -0500 Subject: [PATCH 14/45] ace: Format script strings for readability --- src/static/js/ace.js | 88 ++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index dd3fb832f..c5d517ba8 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -197,23 +197,22 @@ const Ace2Editor = function () { iframeHTML.push(``); iframeHTML.push(``); - 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(scriptTag(` + require.setRootURI("../javascripts/src"); + require.setLibraryURI("../javascripts/lib"); + require.setGlobalKeyPath("require"); + + // intentially moved before requiring client_plugins to save a 307 + var Ace2Inner = require("ep_etherpad-lite/static/js/ace2_inner"); + var plugins = require("ep_etherpad-lite/static/js/pluginfw/client_plugins"); + plugins.adoptPluginsFromAncestorsOf(window); + + $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK + + plugins.ensure(function () { + Ace2Inner.init(); + }); + `)); iframeHTML.push(''); @@ -228,33 +227,34 @@ plugins.ensure(function () {\n\ const gt = typeof globalThis === 'object' ? globalThis : window; gt.ChildAccessibleAce2Editor = Ace2Editor; - 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\ -}`; + const outerScript = ` + editorId = ${JSON.stringify(info.id)}; + editorInfo = parent.ChildAccessibleAce2Editor.registry[editorId]; + window.onload = function () { + window.onload = null; + setTimeout(function () { + var iframe = document.createElement("IFRAME"); + iframe.name = "ace_inner"; + iframe.title = "pad"; + iframe.scrolling = "no"; + var outerdocbody = document.getElementById("outerdocbody"); + iframe.frameBorder = 0; + iframe.allowTransparency = true; // for IE + outerdocbody.insertBefore(iframe, outerdocbody.firstChild); + iframe.ace_outerWin = window; + readyFunc = function () { + editorInfo.onEditorReady(); + readyFunc = null; + editorInfo = null; + }; + var doc = iframe.contentWindow.document; + doc.open(); + var text = (${JSON.stringify(iframeHTML.join('\n'))}); + doc.write(text); + doc.close(); + }, 0); + } + `; const outerHTML = [doctype, ``]; From 3a311d21828b57fbcc97d6feeca5c59253f49d2a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Feb 2021 01:09:19 -0500 Subject: [PATCH 15/45] ace: Lint and simplify script strings --- src/static/js/ace.js | 58 +++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index c5d517ba8..65608bb74 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -197,22 +197,21 @@ const Ace2Editor = function () { iframeHTML.push(``); iframeHTML.push(``); - iframeHTML.push(scriptTag(` - require.setRootURI("../javascripts/src"); - require.setLibraryURI("../javascripts/lib"); - require.setGlobalKeyPath("require"); + iframeHTML.push(scriptTag(`(() => { + const require = window.require; + require.setRootURI('../javascripts/src'); + require.setLibraryURI('../javascripts/lib'); + require.setGlobalKeyPath('require'); // intentially moved before requiring client_plugins to save a 307 - var Ace2Inner = require("ep_etherpad-lite/static/js/ace2_inner"); - var plugins = require("ep_etherpad-lite/static/js/pluginfw/client_plugins"); - plugins.adoptPluginsFromAncestorsOf(window); + 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); - $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").jQuery; // Expose jQuery #HACK + window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; - plugins.ensure(function () { - Ace2Inner.init(); - }); - `)); + window.plugins.ensure(() => { window.Ace2Inner.init(); }); + })();`)); iframeHTML.push(''); @@ -227,34 +226,31 @@ const Ace2Editor = function () { const gt = typeof globalThis === 'object' ? globalThis : window; gt.ChildAccessibleAce2Editor = Ace2Editor; - const outerScript = ` - editorId = ${JSON.stringify(info.id)}; - editorInfo = parent.ChildAccessibleAce2Editor.registry[editorId]; - window.onload = function () { + const outerScript = `(() => { + window.editorInfo = parent.ChildAccessibleAce2Editor.registry[${JSON.stringify(info.id)}]; + window.onload = () => { window.onload = null; - setTimeout(function () { - var iframe = document.createElement("IFRAME"); - iframe.name = "ace_inner"; - iframe.title = "pad"; - iframe.scrolling = "no"; - var outerdocbody = document.getElementById("outerdocbody"); + setTimeout(() => { + const iframe = document.createElement('iframe'); + iframe.name = 'ace_inner'; + iframe.title = 'pad'; + iframe.scrolling = 'no'; iframe.frameBorder = 0; iframe.allowTransparency = true; // for IE - outerdocbody.insertBefore(iframe, outerdocbody.firstChild); iframe.ace_outerWin = window; - readyFunc = function () { - editorInfo.onEditorReady(); - readyFunc = null; - editorInfo = null; + document.body.insertBefore(iframe, document.body.firstChild); + window.readyFunc = () => { + delete window.readyFunc; + window.editorInfo.onEditorReady(); + delete window.editorInfo; }; - var doc = iframe.contentWindow.document; + const doc = iframe.contentWindow.document; doc.open(); - var text = (${JSON.stringify(iframeHTML.join('\n'))}); - doc.write(text); + doc.write(${JSON.stringify(iframeHTML.join('\n'))}); doc.close(); }, 0); } - `; + })();`; const outerHTML = [doctype, ``]; From d84447290e23117f1a46496346cda65f9619d181 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Feb 2021 01:37:33 -0500 Subject: [PATCH 16/45] ace: Delete unnecessary IIFE --- src/static/js/ace.js | 252 +++++++++++++++++++++---------------------- 1 file changed, 125 insertions(+), 127 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 65608bb74..8833f3671 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -160,145 +160,143 @@ const Ace2Editor = function () { doneFunc(); }; - (() => { - const doctype = ''; + const doctype = ''; - const iframeHTML = []; + const iframeHTML = []; - iframeHTML.push(doctype); - iframeHTML.push(``); + iframeHTML.push(doctype); + 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'); + // 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'); - // disableCustomScriptsAndStyles can be used to disable loading of custom scripts - if (!clientVars.disableCustomScriptsAndStyles) { - $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); - } - - 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}`); - - pushStyleTagsFor(iframeHTML, includedCSS); - iframeHTML.push(``); - // fill the cache - iframeHTML.push(``); - iframeHTML.push(``); - - iframeHTML.push(scriptTag(`(() => { - const require = window.require; - require.setRootURI('../javascripts/src'); - require.setLibraryURI('../javascripts/lib'); - require.setGlobalKeyPath('require'); - - // 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); - - window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; - - window.plugins.ensure(() => { window.Ace2Inner.init(); }); - })();`)); - - iframeHTML.push(''); - - hooks.callAll('aceInitInnerdocbodyHead', { - iframeHTML, - }); - - iframeHTML.push(' '); - - // eslint-disable-next-line node/no-unsupported-features/es-builtins - const gt = typeof globalThis === 'object' ? globalThis : window; - gt.ChildAccessibleAce2Editor = Ace2Editor; - - const outerScript = `(() => { - window.editorInfo = parent.ChildAccessibleAce2Editor.registry[${JSON.stringify(info.id)}]; - window.onload = () => { - window.onload = null; - setTimeout(() => { - 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); - window.readyFunc = () => { - delete window.readyFunc; - window.editorInfo.onEditorReady(); - delete window.editorInfo; - }; - const doc = iframe.contentWindow.document; - doc.open(); - doc.write(${JSON.stringify(iframeHTML.join('\n'))}); - doc.close(); - }, 0); - } - })();`; - - const outerHTML = - [doctype, ``]; - - includedCSS = []; - $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); }; - $$INCLUDE_CSS('../static/css/iframe_editor.css'); + // disableCustomScriptsAndStyles can be used to disable loading of custom scripts + if (!clientVars.disableCustomScriptsAndStyles) { $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); + } + + 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}`); + + pushStyleTagsFor(iframeHTML, includedCSS); + iframeHTML.push(``); + // fill the cache + iframeHTML.push(``); + iframeHTML.push(``); + + iframeHTML.push(scriptTag(`(() => { + const require = window.require; + require.setRootURI('../javascripts/src'); + require.setLibraryURI('../javascripts/lib'); + require.setGlobalKeyPath('require'); + + // 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); + + window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; + + window.plugins.ensure(() => { window.Ace2Inner.init(); }); + })();`)); + + iframeHTML.push(''); + + hooks.callAll('aceInitInnerdocbodyHead', { + iframeHTML, + }); + + iframeHTML.push(' '); + + // eslint-disable-next-line node/no-unsupported-features/es-builtins + const gt = typeof globalThis === 'object' ? globalThis : window; + gt.ChildAccessibleAce2Editor = Ace2Editor; + + const outerScript = `(() => { + window.editorInfo = parent.ChildAccessibleAce2Editor.registry[${JSON.stringify(info.id)}]; + window.onload = () => { + window.onload = null; + setTimeout(() => { + 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); + window.readyFunc = () => { + delete window.readyFunc; + window.editorInfo.onEditorReady(); + delete window.editorInfo; + }; + const doc = iframe.contentWindow.document; + doc.open(); + doc.write(${JSON.stringify(iframeHTML.join('\n'))}); + doc.close(); + }, 0); + } + })();`; + + const outerHTML = + [doctype, ``]; + + 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}`); + 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); + 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
', - ''); + // 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 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; - editorDocument.open(); - editorDocument.write(outerHTML.join('')); - editorDocument.close(); - })(); + editorDocument.open(); + editorDocument.write(outerHTML.join('')); + editorDocument.close(); }; }; From 54df7f372836d1c4bfc8f11c594b47425f1e543e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Feb 2021 01:40:58 -0500 Subject: [PATCH 17/45] ace: Delete unused `clientVars.disableCustomScriptsAndStyles` --- src/static/js/ace.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 8833f3671..7bf541153 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -174,12 +174,7 @@ const Ace2Editor = function () { let includedCSS = []; let $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); }; $$INCLUDE_CSS('../static/css/iframe_editor.css'); - - // disableCustomScriptsAndStyles can be used to disable loading of custom scripts - if (!clientVars.disableCustomScriptsAndStyles) { - $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); - } - + $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); let additionalCSS = hooks.callAll('aceEditorCSS').map((path) => { if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css return path; From 94c221586c3dca083d8f84a35028f929076e62fb Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Feb 2021 01:44:38 -0500 Subject: [PATCH 18/45] ace: Factor out duplicated `$$INCLUDE_CSS` code --- src/static/js/ace.js | 53 ++++++++++++++------------------------------ 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 7bf541153..a2b1207b2 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -160,31 +160,29 @@ const Ace2Editor = function () { doneFunc(); }; + // 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: + const includedCSS = []; + const $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); }; + $$INCLUDE_CSS('../static/css/iframe_editor.css'); + $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); + includedCSS.push(...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}`; + })); + $$INCLUDE_CSS( + `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); + const doctype = ''; const iframeHTML = []; iframeHTML.push(doctype); 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'); - $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); - 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}`); - pushStyleTagsFor(iframeHTML, includedCSS); iframeHTML.push(``); // fill the cache @@ -248,23 +246,6 @@ const Ace2Editor = function () { const outerHTML = [doctype, ``]; - - 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 From 9cfc2fb80196da8f99f10688692cbb5be81598be Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Fri, 26 Feb 2021 01:52:16 -0500 Subject: [PATCH 19/45] ace: Simplify the `aceEditorCSS` hook map function --- src/static/js/ace.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index a2b1207b2..f69887e17 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -168,12 +168,9 @@ const Ace2Editor = function () { const $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); }; $$INCLUDE_CSS('../static/css/iframe_editor.css'); $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); - includedCSS.push(...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.push(...hooks.callAll('aceEditorCSS').map( + // Allow urls to external CSS - http(s):// and //some/path.css + (p) => /\/\//.test(p) ? p : `../static/plugins/${p}`)); $$INCLUDE_CSS( `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); From 6fe015412981885284054a06e001802f4cb0d112 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 28 Feb 2021 19:59:11 -0500 Subject: [PATCH 20/45] ace: Use absolute URLs when building iframes This isn't strictly necessary right now, but will become necessary (due to a Safari quirk) when we change to building the iframes programmatically (vs. the current `document.write()` approach). --- src/static/js/ace.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index f69887e17..eaea9d8aa 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -27,6 +27,11 @@ const hooks = require('./pluginfw/hooks'); const pluginUtils = require('./pluginfw/shared'); +// 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) => ``; @@ -140,7 +145,7 @@ const Ace2Editor = function () { buffer.push(''); } for (const file of remoteFiles) { - buffer.push(``); + buffer.push(``); } }; @@ -181,15 +186,20 @@ const Ace2Editor = function () { iframeHTML.push(doctype); iframeHTML.push(``); pushStyleTagsFor(iframeHTML, includedCSS); - iframeHTML.push(``); - // fill the cache - iframeHTML.push(``); - iframeHTML.push(``); + 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(``); + } iframeHTML.push(scriptTag(`(() => { const require = window.require; - require.setRootURI('../javascripts/src'); - require.setLibraryURI('../javascripts/lib'); + require.setRootURI(${JSON.stringify(absUrl('../javascripts/src'))}); + require.setLibraryURI(${JSON.stringify(absUrl('../javascripts/lib'))}); require.setGlobalKeyPath('require'); // intentially moved before requiring client_plugins to save a 307 From e57829183db6eec90b11b5da825a914c5768b4d9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 27 Feb 2021 21:02:43 -0500 Subject: [PATCH 21/45] ace: Pass objects to Ace2Inner via function args --- src/static/js/ace.js | 2 +- src/static/js/ace2_inner.js | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index eaea9d8aa..4a6c4060a 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -209,7 +209,7 @@ const Ace2Editor = function () { window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; - window.plugins.ensure(() => { window.Ace2Inner.init(); }); + window.plugins.ensure(() => { window.Ace2Inner.init(parent.editorInfo, parent.readyFunc); }); })();`)); iframeHTML.push(''); diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 5ae4f0660..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(); @@ -3896,7 +3895,7 @@ function Ace2Inner() { editorInfo.ace_performDocumentApplyAttributesToRange = (...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); }; From 159fd5bdebec0d9fcbc97168007e56848359ee87 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 28 Feb 2021 21:04:45 -0500 Subject: [PATCH 22/45] ace: Simplify passing of `editorInfo` --- src/static/js/ace.js | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 4a6c4060a..2255a7a9a 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -36,12 +36,8 @@ 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 = []; @@ -57,8 +53,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 = [ @@ -152,7 +146,7 @@ const Ace2Editor = function () { 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 }); @@ -209,7 +203,10 @@ const Ace2Editor = function () { window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; - window.plugins.ensure(() => { window.Ace2Inner.init(parent.editorInfo, parent.readyFunc); }); + window.plugins.ensure(() => { + const editorInfo = parent.parent.ace2EditorInfo; + window.Ace2Inner.init(editorInfo, editorInfo.onEditorReady); + }); })();`)); iframeHTML.push(''); @@ -221,12 +218,7 @@ const Ace2Editor = function () { iframeHTML.push(' '); - // eslint-disable-next-line node/no-unsupported-features/es-builtins - const gt = typeof globalThis === 'object' ? globalThis : window; - gt.ChildAccessibleAce2Editor = Ace2Editor; - const outerScript = `(() => { - window.editorInfo = parent.ChildAccessibleAce2Editor.registry[${JSON.stringify(info.id)}]; window.onload = () => { window.onload = null; setTimeout(() => { @@ -238,11 +230,6 @@ const Ace2Editor = function () { iframe.allowTransparency = true; // for IE iframe.ace_outerWin = window; document.body.insertBefore(iframe, document.body.firstChild); - window.readyFunc = () => { - delete window.readyFunc; - window.editorInfo.onEditorReady(); - delete window.editorInfo; - }; const doc = iframe.contentWindow.document; doc.open(); doc.write(${JSON.stringify(iframeHTML.join('\n'))}); @@ -283,8 +270,4 @@ const Ace2Editor = function () { }; }; -Ace2Editor.registry = { - nextId: 1, -}; - exports.Ace2Editor = Ace2Editor; From c696732838121b056d03cfed5760064697106775 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 27 Feb 2021 20:33:56 -0500 Subject: [PATCH 23/45] ace: Asyncify `Ace2Editor.init()` --- src/static/js/ace.js | 65 ++++++++++++++++++------------------- src/static/js/pad_editor.js | 3 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 2255a7a9a..8613d9e37 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -150,15 +150,9 @@ const Ace2Editor = function () { info = null; // prevent IE 6 closure memory leaks }); - this.init = function (containerId, initialCode, doneFunc) { + this.init = async function (containerId, initialCode) { this.importText(initialCode); - info.onEditorReady = () => { - loaded = true; - doActionsPendingInit(); - doneFunc(); - }; - // 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. @@ -190,7 +184,7 @@ const Ace2Editor = function () { iframeHTML.push(``); } - iframeHTML.push(scriptTag(`(() => { + iframeHTML.push(scriptTag(`(async () => { const require = window.require; require.setRootURI(${JSON.stringify(absUrl('../javascripts/src'))}); require.setLibraryURI(${JSON.stringify(absUrl('../javascripts/lib'))}); @@ -203,10 +197,12 @@ const Ace2Editor = function () { window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; - window.plugins.ensure(() => { - const editorInfo = parent.parent.ace2EditorInfo; - window.Ace2Inner.init(editorInfo, editorInfo.onEditorReady); - }); + await new Promise((resolve, reject) => window.plugins.ensure( + (err) => err != null ? reject(err) : resolve())); + const editorInfo = parent.parent.ace2EditorInfo; + await new Promise((resolve, reject) => window.Ace2Inner.init( + editorInfo, (err) => err != null ? reject(err) : resolve())); + editorInfo.onEditorReady(); })();`)); iframeHTML.push(''); @@ -218,24 +214,22 @@ const Ace2Editor = function () { iframeHTML.push(' '); - const outerScript = `(() => { - window.onload = () => { - window.onload = null; - setTimeout(() => { - 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(); - }, 0); - } + const outerScript = `(async () => { + await new Promise((resolve) => { window.onload = () => resolve(); }); + 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(); })();`; const outerHTML = @@ -264,9 +258,14 @@ const Ace2Editor = function () { const editorDocument = outerFrame.contentWindow.document; - editorDocument.open(); - editorDocument.write(outerHTML.join('')); - editorDocument.close(); + await new Promise((resolve, reject) => { + info.onEditorReady = (err) => err != null ? reject(err) : resolve(); + editorDocument.open(); + editorDocument.write(outerHTML.join('')); + editorDocument.close(); + }); + loaded = true; + doActionsPendingInit(); }; }; 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); From a17f9bf3cfc745a44d0e57b77912e346ffd3ce1c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Thu, 25 Feb 2021 21:11:30 -0500 Subject: [PATCH 24/45] ace: Build the outer and inner iframes programmatically This makes the code easier to read and it silences Chrome's `document.write()` warning: https://developers.google.com/web/updates/2016/08/removing-document-write --- src/static/js/ace.js | 285 ++++++++++++++++++++++++++++--------------- 1 file changed, 185 insertions(+), 100 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 8613d9e37..59bf0f863 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -32,12 +32,78 @@ const pluginUtils = require('./pluginfw/shared'); // 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 eventFired = async (obj, event, cleanups = [], predicate = () => true) => { + if (typeof cleanups === 'function') { + predicate = cleanups; + cleanups = []; + } + await new Promise((resolve, reject) => { + let cleanup; + const successCb = () => { + if (!predicate()) return; + cleanup(); + resolve(); + }; + const errorCb = () => { + const err = new Error(`Ace2Editor.init() error event while waiting for ${event} event`); + cleanup(); + reject(err); + }; + cleanup = () => { + cleanup = () => {}; + obj.removeEventListener(event, successCb); + obj.removeEventListener('error', errorCb); + }; + cleanups.push(cleanup); + obj.addEventListener(event, successCb); + obj.addEventListener('error', errorCb); + }); +}; + +const pollCondition = async (predicate, cleanups, pollPeriod, timeout) => { + let done = false; + cleanups.push(() => { done = true; }); + // Pause a tick to give the predicate a chance to become true before adding latency. + await new Promise((resolve) => setTimeout(resolve, 0)); + const start = Date.now(); + while (!done && !predicate()) { + if (Date.now() - start > timeout) throw new Error('timeout'); + await new Promise((resolve) => setTimeout(resolve, pollPeriod)); + } +}; + +// Resolves when the frame's document is ready to be mutated: +// - Firefox seems to replace the frame's contentWindow.document object with a different object +// after the frame is created so we need to wait for the window's load event before continuing. +// - Chrome doesn't need any waiting (not even next tick), but on Windows it never seems to fire +// any events. Eventually the document's readyState becomes 'complete' (even though it never +// fires a readystatechange event), so this function waits for that to happen to avoid returning +// too soon on Firefox. +// - Safari behaves like Chrome. +// I'm not sure how other browsers behave, so this function throws the kitchen sink at the problem. +// Maybe one day we'll find a concise general solution. +const frameReady = async (frame) => { + // Can't do `const doc = frame.contentDocument;` because Firefox seems to asynchronously replace + // the document object after the frame is first created for some reason. ¯\_(ツ)_/¯ + const doc = () => frame.contentDocument; + const cleanups = []; + try { + await Promise.race([ + eventFired(frame, 'load', cleanups), + eventFired(frame.contentWindow, 'load', cleanups), + eventFired(doc(), 'load', cleanups), + eventFired(doc(), 'DOMContentLoaded', cleanups), + eventFired(doc(), 'readystatechange', cleanups, () => doc.readyState === 'complete'), + // If all else fails, poll. + pollCondition(() => doc().readyState === 'complete', cleanups, 10, 5000), + ]); + } finally { + for (const cleanup of cleanups) cleanup(); + } +}; const Ace2Editor = function () { let info = {editor: this}; - window.ace2EditorInfo = info; // Make it accessible to iframes. let loaded = false; let actionsPendingInit = []; @@ -126,27 +192,30 @@ const Ace2Editor = function () { return {embeded: embededFiles, remote: remoteFiles}; }; - const pushStyleTagsFor = (buffer, files) => { + const addStyleTagsFor = (doc, files) => { const sorted = sortFilesByEmbeded(files); const embededFiles = sorted.embeded; const remoteFiles = sorted.remote; if (embededFiles.length > 0) { - buffer.push(''); + const css = embededFiles.map((f) => Ace2Editor.EMBEDED[f]).join('\n'); + const style = doc.createElement('style'); + style.type = 'text/css'; + style.appendChild(doc.createTextNode(css)); + doc.head.appendChild(style); } for (const file of remoteFiles) { - buffer.push(``); + const link = doc.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = absUrl(encodeURI(file)); + doc.head.appendChild(link); } }; this.destroy = pendingInit(() => { info.ace_dispose(); info.frame.parentNode.removeChild(info.frame); - delete window.ace2EditorInfo; info = null; // prevent IE 6 closure memory leaks }); @@ -167,103 +236,119 @@ const Ace2Editor = function () { $$INCLUDE_CSS( `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); - const doctype = ''; + const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== ''); - const iframeHTML = []; - - 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(``); - } - - iframeHTML.push(scriptTag(`(async () => { - const require = window.require; - require.setRootURI(${JSON.stringify(absUrl('../javascripts/src'))}); - require.setLibraryURI(${JSON.stringify(absUrl('../javascripts/lib'))}); - require.setGlobalKeyPath('require'); - - // 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); - - window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; - - await new Promise((resolve, reject) => window.plugins.ensure( - (err) => err != null ? reject(err) : resolve())); - const editorInfo = parent.parent.ace2EditorInfo; - await new Promise((resolve, reject) => window.Ace2Inner.init( - editorInfo, (err) => err != null ? reject(err) : resolve())); - editorInfo.onEditorReady(); - })();`)); - - iframeHTML.push(''); - - hooks.callAll('aceInitInnerdocbodyHead', { - iframeHTML, - }); - - iframeHTML.push(' '); - - const outerScript = `(async () => { - await new Promise((resolve) => { window.onload = () => resolve(); }); - 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(); - })();`; - - const outerHTML = - [doctype, ``]; - 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'); + 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 outerWindow = outerFrame.contentWindow; - const editorDocument = outerFrame.contentWindow.document; + // For some unknown reason Firefox replaces outerWindow.document with a new Document object some + // time between running the above code and firing the outerWindow load event. Work around it by + // waiting until the load event fires before mutating the Document object. + await frameReady(outerFrame); - await new Promise((resolve, reject) => { - info.onEditorReady = (err) => err != null ? reject(err) : resolve(); - editorDocument.open(); - editorDocument.write(outerHTML.join('')); - editorDocument.close(); - }); + // This must be done after the Window's load event. See above comment. + const outerDocument = outerWindow.document; + + // tag + outerDocument.documentElement.classList.add('inner-editor', 'outerdoc', ...skinVariants); + + // tag + addStyleTagsFor(outerDocument, includedCSS); + const outerStyle = outerDocument.createElement('style'); + outerStyle.type = 'text/css'; + outerStyle.title = 'dynamicsyntax'; + outerDocument.head.appendChild(outerStyle); + const link = outerDocument.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = 'data:text/css,'; + outerDocument.head.appendChild(link); + + // tag + outerDocument.body.id = 'outerdocbody'; + outerDocument.body.classList.add('outerdocbody', ...pluginUtils.clientPluginNames()); + const sideDiv = outerDocument.createElement('div'); + sideDiv.id = 'sidediv'; + sideDiv.classList.add('sidediv'); + outerDocument.body.appendChild(sideDiv); + const lineMetricsDiv = outerDocument.createElement('div'); + lineMetricsDiv.id = 'linemetricsdiv'; + lineMetricsDiv.appendChild(outerDocument.createTextNode('x')); + outerDocument.body.appendChild(lineMetricsDiv); + + const innerFrame = outerDocument.createElement('iframe'); + innerFrame.name = 'ace_inner'; + innerFrame.title = 'pad'; + innerFrame.scrolling = 'no'; + innerFrame.frameBorder = 0; + innerFrame.allowTransparency = true; // for IE + innerFrame.ace_outerWin = outerWindow; + outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild); + const innerWindow = innerFrame.contentWindow; + + // Wait before mutating the inner document. See above comment recarding outerWindow load. + await frameReady(innerFrame); + + // This must be done after the Window's load event. See above comment. + const innerDocument = innerWindow.document; + + // tag + innerDocument.documentElement.classList.add('inner-editor', ...skinVariants); + + // tag + addStyleTagsFor(innerDocument, includedCSS); + const requireKernel = innerDocument.createElement('script'); + requireKernel.type = 'text/javascript'; + requireKernel.src = + absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`); + innerDocument.head.appendChild(requireKernel); + // Pre-fetch modules to improve load performance. + for (const module of ['ace2_inner', 'ace2_common']) { + const script = innerDocument.createElement('script'); + script.type = 'text/javascript'; + script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` + + `?callback=require.define&v=${clientVars.randomVersionString}`); + innerDocument.head.appendChild(script); + } + const innerStyle = innerDocument.createElement('style'); + innerStyle.type = 'text/css'; + innerStyle.title = 'dynamicsyntax'; + innerDocument.head.appendChild(innerStyle); + const headLines = []; + hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines}); + const tmp = innerDocument.createElement('div'); + tmp.innerHTML = headLines.join('\n'); + while (tmp.firstChild) innerDocument.head.appendChild(tmp.firstChild); + + // tag + innerDocument.body.id = 'innerdocbody'; + innerDocument.body.classList.add('innerdocbody'); + innerDocument.body.setAttribute('role', 'application'); + innerDocument.body.setAttribute('spellcheck', 'false'); + innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //   + + await eventFired(requireKernel, 'load'); + const require = innerWindow.require; + require.setRootURI(absUrl('../javascripts/src')); + require.setLibraryURI(absUrl('../javascripts/lib')); + require.setGlobalKeyPath('require'); + + // intentially moved before requiring client_plugins to save a 307 + innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner'); + innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); + innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow); + + innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; + + await new Promise((resolve, reject) => innerWindow.plugins.ensure( + (err) => err != null ? reject(err) : resolve())); + await new Promise((resolve, reject) => innerWindow.Ace2Inner.init( + info, (err) => err != null ? reject(err) : resolve())); loaded = true; doActionsPendingInit(); }; From 66d3ac37833896b0ab8f278f60386c0838088393 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 28 Feb 2021 19:37:14 -0500 Subject: [PATCH 25/45] ace: Debug logging --- src/static/js/ace.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 59bf0f863..3fc9ac775 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -27,6 +27,8 @@ const hooks = require('./pluginfw/hooks'); const pluginUtils = require('./pluginfw/shared'); +const debugLog = (...args) => {}; + // 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. @@ -41,11 +43,13 @@ const eventFired = async (obj, event, cleanups = [], predicate = () => true) => let cleanup; const successCb = () => { if (!predicate()) return; + debugLog(`Ace2Editor.init() ${event} event on`, obj); cleanup(); resolve(); }; const errorCb = () => { const err = new Error(`Ace2Editor.init() error event while waiting for ${event} event`); + debugLog(`${err} on object`, obj); cleanup(); reject(err); }; @@ -69,7 +73,9 @@ const pollCondition = async (predicate, cleanups, pollPeriod, timeout) => { while (!done && !predicate()) { if (Date.now() - start > timeout) throw new Error('timeout'); await new Promise((resolve) => setTimeout(resolve, pollPeriod)); + debugLog('Ace2Editor.init() polling'); } + if (!done) debugLog('Ace2Editor.init() poll condition became true'); }; // Resolves when the frame's document is ready to be mutated: @@ -220,6 +226,7 @@ const Ace2Editor = function () { }); this.init = async function (containerId, initialCode) { + debugLog('Ace2Editor.init()'); this.importText(initialCode); // calls to these functions ($$INCLUDE_...) are replaced when this file is processed @@ -249,7 +256,9 @@ const Ace2Editor = function () { // For some unknown reason Firefox replaces outerWindow.document with a new Document object some // time between running the above code and firing the outerWindow load event. Work around it by // waiting until the load event fires before mutating the Document object. + debugLog('Ace2Editor.init() waiting for outer frame'); await frameReady(outerFrame); + debugLog('Ace2Editor.init() outer frame ready'); // This must be done after the Window's load event. See above comment. const outerDocument = outerWindow.document; @@ -292,7 +301,9 @@ const Ace2Editor = function () { const innerWindow = innerFrame.contentWindow; // Wait before mutating the inner document. See above comment recarding outerWindow load. + debugLog('Ace2Editor.init() waiting for inner frame'); await frameReady(innerFrame); + debugLog('Ace2Editor.init() inner frame ready'); // This must be done after the Window's load event. See above comment. const innerDocument = innerWindow.document; @@ -332,7 +343,9 @@ const Ace2Editor = function () { innerDocument.body.setAttribute('spellcheck', 'false'); innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //   + debugLog('Ace2Editor.init() waiting for require kernel load'); await eventFired(requireKernel, 'load'); + debugLog('Ace2Editor.init() require kernel loaded'); const require = innerWindow.require; require.setRootURI(absUrl('../javascripts/src')); require.setLibraryURI(absUrl('../javascripts/lib')); @@ -345,12 +358,16 @@ const Ace2Editor = function () { innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; + debugLog('Ace2Editor.init() waiting for plugins'); await new Promise((resolve, reject) => innerWindow.plugins.ensure( (err) => err != null ? reject(err) : resolve())); + debugLog('Ace2Editor.init() waiting for Ace2Inner.init()'); await new Promise((resolve, reject) => innerWindow.Ace2Inner.init( info, (err) => err != null ? reject(err) : resolve())); + debugLog('Ace2Editor.init() Ace2Inner.init() returned'); loaded = true; doActionsPendingInit(); + debugLog('Ace2Editor.init() done'); }; }; From b0862cd030127f35d5b8ef92411911474d7b31a6 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Mar 2021 02:21:22 -0500 Subject: [PATCH 26/45] ace: Delete all `$$INCLUDE_CSS` logic The intention of the deleted code was to reduce the number of fetches, but it only saved a single fetch due to implementation flaws. The right way to reduce the number of fetches is to use a bundling technology such as webpack, and this change makes it easier to do so. --- src/node/utils/Minify.js | 39 ----------------------------- src/static/js/ace.js | 54 +++++++--------------------------------- 2 files changed, 9 insertions(+), 84 deletions(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index f48260216..954463464 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -220,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) => { /* @@ -366,7 +328,6 @@ 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)); }; diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 3fc9ac775..6d07ecbad 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -178,39 +178,8 @@ 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 addStyleTagsFor = (doc, files) => { - const sorted = sortFilesByEmbeded(files); - const embededFiles = sorted.embeded; - const remoteFiles = sorted.remote; - - if (embededFiles.length > 0) { - const css = embededFiles.map((f) => Ace2Editor.EMBEDED[f]).join('\n'); - const style = doc.createElement('style'); - style.type = 'text/css'; - style.appendChild(doc.createTextNode(css)); - doc.head.appendChild(style); - } - for (const file of remoteFiles) { + for (const file of files) { const link = doc.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; @@ -229,19 +198,14 @@ const Ace2Editor = function () { debugLog('Ace2Editor.init()'); this.importText(initialCode); - // 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: - const includedCSS = []; - const $$INCLUDE_CSS = (filename) => { includedCSS.push(filename); }; - $$INCLUDE_CSS('../static/css/iframe_editor.css'); - $$INCLUDE_CSS(`../static/css/pad.css?v=${clientVars.randomVersionString}`); - includedCSS.push(...hooks.callAll('aceEditorCSS').map( - // Allow urls to external CSS - http(s):// and //some/path.css - (p) => /\/\//.test(p) ? p : `../static/plugins/${p}`)); - $$INCLUDE_CSS( - `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`); + 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 skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== ''); From a79f9efdb405d3b518974e63ab9ca9eb153597c9 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 1 Mar 2021 14:46:50 +0000 Subject: [PATCH 27/45] Include props to Sauce Labs for the tests they power. (#4897) * Include props to Sauce Labs for the tests they power. * include message in CI --- .github/workflows/frontend-admin-tests.yml | 3 ++- .github/workflows/frontend-tests.yml | 3 ++- README.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) 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 ed205e004..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] diff --git a/README.md b/README.md index a196c7197..abe90a166 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ 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) [![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 Docker Pulls [![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) From 8971166c58ad9f88eb4979cb75cb39f945287b89 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Mar 2021 15:06:47 -0500 Subject: [PATCH 28/45] lint: Set up Cypress config and fix issues --- src/package-lock.json | 23 ++++++++++++++++--- src/package.json | 13 +++++++++-- .../frontend/cypress/integration/test.js | 2 +- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 26c65378b..a507d27af 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -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", diff --git a/src/package.json b/src/package.json index 1a0eb74d4..e1af8a4bd 100644 --- a/src/package.json +++ b/src/package.json @@ -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/**/*" diff --git a/src/tests/frontend/cypress/integration/test.js b/src/tests/frontend/cypress/integration/test.js index 300a555d5..6574696db 100644 --- a/src/tests/frontend/cypress/integration/test.js +++ b/src/tests/frontend/cypress/integration/test.js @@ -8,7 +8,7 @@ Cypress.Commands.add('iframe', {prevSubject: 'element'}, })); describe(__filename, () => { - it('Pad content exists', async () => { + it('Pad content exists', () => { cy.visit('http://127.0.0.1:9001/p/test'); cy.get('iframe[name="ace_outer"]', {timeout: 10000}).iframe() .find('.line-number:first') From 797ffa5600a60932b92d7d961e64697c602abf0e Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Mar 2021 17:00:05 -0500 Subject: [PATCH 29/45] Minify: Avoid `path.relative()` Constructing a relative pathname on Windows is problematic because the two absolute pathnames might be on different drives (or UNC paths). Use `path.resolve()` instead of `path.join()` where appropriate to avoid the need to construct a relative path. --- src/node/utils/Minify.js | 6 +++--- src/node/utils/MinifyWorker.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 954463464..7969f9c14 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -168,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 @@ -241,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. @@ -329,7 +329,7 @@ const getFileCompressed = async (filename, contentType) => { const getFile = async (filename) => { 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: From 2fd06535b7fd999e2902825d5573dfcd3a7a6876 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Mon, 1 Mar 2021 21:53:42 +0000 Subject: [PATCH 30/45] fix: upgrade express-rate-limit from 5.2.3 to 5.2.5 Snyk has created this PR to upgrade express-rate-limit from 5.2.3 to 5.2.5. See this package in npm: https://www.npmjs.com/package/express-rate-limit See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 6 +++--- src/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index a507d27af..4a3b1aa6c 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -2110,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", diff --git a/src/package.json b/src/package.json index e1af8a4bd..a56a656ac 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", From 05b27cfd5da9f9c13016ab49301baf77a319c556 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 2 Mar 2021 06:54:08 +0000 Subject: [PATCH 31/45] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abe90a166..ba121cc90 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ 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 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) [![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://app.saucelabs.com/buildstatus/etherpad?color=%2344b492)](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 Docker Pulls [![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) From f24353e806ebd423d574f7ba6efdfb05492d2ff1 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 2 Mar 2021 07:00:05 +0000 Subject: [PATCH 32/45] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba121cc90..4bc972285 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ 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 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://app.saucelabs.com/buildstatus/etherpad?color=%2344b492)](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) +[![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 Docker Pulls [![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) From f95b09e0b6752a0d226d58d8b246831164dc9533 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 2 Mar 2021 17:14:47 +0000 Subject: [PATCH 33/45] Import: Import don't show warnings for supported elements --- src/node/utils/ImportEtherpad.js | 7 +++---- src/static/js/contentcollector.js | 30 ++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) 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/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; From 7e698baa80daf7b1133e5a85ebe3ccc5afa100a9 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 3 Mar 2021 03:10:49 -0500 Subject: [PATCH 34/45] pluginfw: Improve hook function load error message --- src/static/js/pluginfw/shared.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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] = []; From 0aad3b74da7ab9aa2cf560a42cd592a7a4c9b40b Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 3 Mar 2021 03:14:15 -0500 Subject: [PATCH 35/45] pluginfw: Improve rendering of hook list There are two main benefits: * HTML is no longer printed in the startup debug logs. * `require()` is no longer called on client-side files. This eliminates "Failed to load for : ReferenceError: window is not defined" errors when users visit `/admin/plugins/info`. --- src/node/hooks/express/adminplugins.js | 4 +-- src/node/server.js | 2 +- src/static/js/pluginfw/plugins.js | 48 +++++++++++++++++++++----- 3 files changed, 43 insertions(+), 11 deletions(-) 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/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/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 () => { From 912f0f195faf19b11a5db928b3846fbb09388004 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Wed, 3 Mar 2021 17:30:20 -0500 Subject: [PATCH 36/45] Revert "ace: Build the outer and inner iframes programmatically" This reverts commit a17f9bf3cfc745a44d0e57b77912e346ffd3ce1c, which caused a mysterious bug with the line numbers. Revert to avoid blocking a new release while I figure out the bug. --- src/static/js/ace.js | 296 +++++++++++++++---------------------------- 1 file changed, 103 insertions(+), 193 deletions(-) diff --git a/src/static/js/ace.js b/src/static/js/ace.js index 6d07ecbad..44556dd0f 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -28,88 +28,19 @@ 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 eventFired = async (obj, event, cleanups = [], predicate = () => true) => { - if (typeof cleanups === 'function') { - predicate = cleanups; - cleanups = []; - } - await new Promise((resolve, reject) => { - let cleanup; - const successCb = () => { - if (!predicate()) return; - debugLog(`Ace2Editor.init() ${event} event on`, obj); - cleanup(); - resolve(); - }; - const errorCb = () => { - const err = new Error(`Ace2Editor.init() error event while waiting for ${event} event`); - debugLog(`${err} on object`, obj); - cleanup(); - reject(err); - }; - cleanup = () => { - cleanup = () => {}; - obj.removeEventListener(event, successCb); - obj.removeEventListener('error', errorCb); - }; - cleanups.push(cleanup); - obj.addEventListener(event, successCb); - obj.addEventListener('error', errorCb); - }); -}; - -const pollCondition = async (predicate, cleanups, pollPeriod, timeout) => { - let done = false; - cleanups.push(() => { done = true; }); - // Pause a tick to give the predicate a chance to become true before adding latency. - await new Promise((resolve) => setTimeout(resolve, 0)); - const start = Date.now(); - while (!done && !predicate()) { - if (Date.now() - start > timeout) throw new Error('timeout'); - await new Promise((resolve) => setTimeout(resolve, pollPeriod)); - debugLog('Ace2Editor.init() polling'); - } - if (!done) debugLog('Ace2Editor.init() poll condition became true'); -}; - -// Resolves when the frame's document is ready to be mutated: -// - Firefox seems to replace the frame's contentWindow.document object with a different object -// after the frame is created so we need to wait for the window's load event before continuing. -// - Chrome doesn't need any waiting (not even next tick), but on Windows it never seems to fire -// any events. Eventually the document's readyState becomes 'complete' (even though it never -// fires a readystatechange event), so this function waits for that to happen to avoid returning -// too soon on Firefox. -// - Safari behaves like Chrome. -// I'm not sure how other browsers behave, so this function throws the kitchen sink at the problem. -// Maybe one day we'll find a concise general solution. -const frameReady = async (frame) => { - // Can't do `const doc = frame.contentDocument;` because Firefox seems to asynchronously replace - // the document object after the frame is first created for some reason. ¯\_(ツ)_/¯ - const doc = () => frame.contentDocument; - const cleanups = []; - try { - await Promise.race([ - eventFired(frame, 'load', cleanups), - eventFired(frame.contentWindow, 'load', cleanups), - eventFired(doc(), 'load', cleanups), - eventFired(doc(), 'DOMContentLoaded', cleanups), - eventFired(doc(), 'readystatechange', cleanups, () => doc.readyState === 'complete'), - // If all else fails, poll. - pollCondition(() => doc().readyState === 'complete', cleanups, 10, 5000), - ]); - } finally { - for (const cleanup of cleanups) cleanup(); - } -}; +const scriptTag = + (source) => ``; const Ace2Editor = function () { let info = {editor: this}; + window.ace2EditorInfo = info; // Make it accessible to iframes. let loaded = false; let actionsPendingInit = []; @@ -178,19 +109,16 @@ const Ace2Editor = function () { // returns array of {error: , time: +new Date()} this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : []; - const addStyleTagsFor = (doc, files) => { + const pushStyleTagsFor = (buffer, files) => { for (const file of files) { - const link = doc.createElement('link'); - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = absUrl(encodeURI(file)); - doc.head.appendChild(link); + buffer.push(``); } }; this.destroy = pendingInit(() => { info.ace_dispose(); info.frame.parentNode.removeChild(info.frame); + delete window.ace2EditorInfo; info = null; // prevent IE 6 closure memory leaks }); @@ -207,128 +135,110 @@ const Ace2Editor = function () { `../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`, ]; - const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== ''); + const doctype = ''; - const outerFrame = document.createElement('iframe'); + const iframeHTML = []; + + 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(``); + } + + 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'); + + // 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); + + window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; + + 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(''); + + hooks.callAll('aceInitInnerdocbodyHead', { + iframeHTML, + }); + + 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'); + })();`; + + const outerHTML = + [doctype, ``]; + 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 outerWindow = outerFrame.contentWindow; - // For some unknown reason Firefox replaces outerWindow.document with a new Document object some - // time between running the above code and firing the outerWindow load event. Work around it by - // waiting until the load event fires before mutating the Document object. + const editorDocument = outerFrame.contentWindow.document; + debugLog('Ace2Editor.init() waiting for outer frame'); - await frameReady(outerFrame); - debugLog('Ace2Editor.init() outer frame ready'); - - // This must be done after the Window's load event. See above comment. - const outerDocument = outerWindow.document; - - // tag - outerDocument.documentElement.classList.add('inner-editor', 'outerdoc', ...skinVariants); - - // tag - addStyleTagsFor(outerDocument, includedCSS); - const outerStyle = outerDocument.createElement('style'); - outerStyle.type = 'text/css'; - outerStyle.title = 'dynamicsyntax'; - outerDocument.head.appendChild(outerStyle); - const link = outerDocument.createElement('link'); - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = 'data:text/css,'; - outerDocument.head.appendChild(link); - - // tag - outerDocument.body.id = 'outerdocbody'; - outerDocument.body.classList.add('outerdocbody', ...pluginUtils.clientPluginNames()); - const sideDiv = outerDocument.createElement('div'); - sideDiv.id = 'sidediv'; - sideDiv.classList.add('sidediv'); - outerDocument.body.appendChild(sideDiv); - const lineMetricsDiv = outerDocument.createElement('div'); - lineMetricsDiv.id = 'linemetricsdiv'; - lineMetricsDiv.appendChild(outerDocument.createTextNode('x')); - outerDocument.body.appendChild(lineMetricsDiv); - - const innerFrame = outerDocument.createElement('iframe'); - innerFrame.name = 'ace_inner'; - innerFrame.title = 'pad'; - innerFrame.scrolling = 'no'; - innerFrame.frameBorder = 0; - innerFrame.allowTransparency = true; // for IE - innerFrame.ace_outerWin = outerWindow; - outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild); - const innerWindow = innerFrame.contentWindow; - - // Wait before mutating the inner document. See above comment recarding outerWindow load. - debugLog('Ace2Editor.init() waiting for inner frame'); - await frameReady(innerFrame); - debugLog('Ace2Editor.init() inner frame ready'); - - // This must be done after the Window's load event. See above comment. - const innerDocument = innerWindow.document; - - // tag - innerDocument.documentElement.classList.add('inner-editor', ...skinVariants); - - // tag - addStyleTagsFor(innerDocument, includedCSS); - const requireKernel = innerDocument.createElement('script'); - requireKernel.type = 'text/javascript'; - requireKernel.src = - absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`); - innerDocument.head.appendChild(requireKernel); - // Pre-fetch modules to improve load performance. - for (const module of ['ace2_inner', 'ace2_common']) { - const script = innerDocument.createElement('script'); - script.type = 'text/javascript'; - script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` + - `?callback=require.define&v=${clientVars.randomVersionString}`); - innerDocument.head.appendChild(script); - } - const innerStyle = innerDocument.createElement('style'); - innerStyle.type = 'text/css'; - innerStyle.title = 'dynamicsyntax'; - innerDocument.head.appendChild(innerStyle); - const headLines = []; - hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines}); - const tmp = innerDocument.createElement('div'); - tmp.innerHTML = headLines.join('\n'); - while (tmp.firstChild) innerDocument.head.appendChild(tmp.firstChild); - - // tag - innerDocument.body.id = 'innerdocbody'; - innerDocument.body.classList.add('innerdocbody'); - innerDocument.body.setAttribute('role', 'application'); - innerDocument.body.setAttribute('spellcheck', 'false'); - innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //   - - debugLog('Ace2Editor.init() waiting for require kernel load'); - await eventFired(requireKernel, 'load'); - debugLog('Ace2Editor.init() require kernel loaded'); - const require = innerWindow.require; - require.setRootURI(absUrl('../javascripts/src')); - require.setLibraryURI(absUrl('../javascripts/lib')); - require.setGlobalKeyPath('require'); - - // intentially moved before requiring client_plugins to save a 307 - innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner'); - innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); - innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow); - - innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; - - debugLog('Ace2Editor.init() waiting for plugins'); - await new Promise((resolve, reject) => innerWindow.plugins.ensure( - (err) => err != null ? reject(err) : resolve())); - debugLog('Ace2Editor.init() waiting for Ace2Inner.init()'); - await new Promise((resolve, reject) => innerWindow.Ace2Inner.init( - info, (err) => err != null ? reject(err) : resolve())); - debugLog('Ace2Editor.init() Ace2Inner.init() returned'); + 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'); From 21cdf0edaa711e6073c8d10917b06e68e7fef73d Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 4 Mar 2021 13:58:49 +0100 Subject: [PATCH 37/45] Localisation updates from https://translatewiki.net. --- src/locales/sl.json | 2 ++ 1 file changed, 2 insertions(+) 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)", From 34d9069874dc35b13c01343c37dd12200b200540 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 16 May 2020 17:34:57 +0000 Subject: [PATCH 38/45] Docker: Add args for the etherpad user's home, UID, GID, and shell Now one can create an `etherpad` user and group on the host system and set the container's UID and GID to match: adduser --system --group etherpad && uid=$(id -u etherpad) && gid=$(id -g etherpad) && docker build --build-arg EP_UID="${uid}" --build-arg EP_GID="${gid}" . This ensures that files created by user `etherpad` inside the container are owned by user `etherpad` outside the container. --- Dockerfile | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 29e2b5abc..3536e10c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,9 +40,19 @@ 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 +RUN mkdir /opt/etherpad-lite && chown etherpad:etherpad /opt/etherpad-lite # 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/*) @@ -55,7 +65,7 @@ USER etherpad WORKDIR /opt/etherpad-lite -COPY --chown=etherpad:0 ./ ./ +COPY --chown=etherpad:etherpad ./ ./ # install node dependencies for Etherpad RUN src/bin/installDeps.sh && \ @@ -68,9 +78,9 @@ RUN src/bin/installDeps.sh && \ RUN for PLUGIN_NAME in ${ETHERPAD_PLUGINS}; do npm install "${PLUGIN_NAME}" || exit 1; done # Copy the configuration file. -COPY --chown=etherpad:0 ./settings.json.docker /opt/etherpad-lite/settings.json +COPY --chown=etherpad:etherpad ./settings.json.docker /opt/etherpad-lite/settings.json -# Fix permissions for root group +# Fix group permissions RUN chmod -R g=u . EXPOSE 9001 From c07bfe10bd5488e6cd1a41d36f32d8dfcf7131e3 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 16 May 2020 17:48:27 +0000 Subject: [PATCH 39/45] Docker: Add arg for the Etherpad directory This makes it possible for users to match the directory inside the container to the host system (for convenience or to avoid confusion). --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3536e10c5..e76ca5c1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,8 @@ RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \ ${EP_HOME:+--home-dir "${EP_HOME}"} --create-home \ ${EP_SHELL:+--shell "${EP_SHELL}"} etherpad -RUN mkdir /opt/etherpad-lite && chown etherpad:etherpad /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/*) @@ -63,7 +64,7 @@ RUN [ -z "${INSTALL_SOFFICE}" ] || (apt update && mkdir -p /usr/share/man/man1 & USER etherpad -WORKDIR /opt/etherpad-lite +WORKDIR "${EP_DIR}" COPY --chown=etherpad:etherpad ./ ./ @@ -78,7 +79,7 @@ RUN src/bin/installDeps.sh && \ RUN for PLUGIN_NAME in ${ETHERPAD_PLUGINS}; do npm install "${PLUGIN_NAME}" || exit 1; done # Copy the configuration file. -COPY --chown=etherpad:etherpad ./settings.json.docker /opt/etherpad-lite/settings.json +COPY --chown=etherpad:etherpad ./settings.json.docker "${EP_DIR}"/settings.json # Fix group permissions RUN chmod -R g=u . From eccec0ba0aa7a87e85b6a72bf77b688678edcc53 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Tue, 2 Mar 2021 23:53:12 -0500 Subject: [PATCH 40/45] Docker: Invoke npm only once when installing plugins This speeds up the build process. --- Dockerfile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index e76ca5c1a..660aacf9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,11 +72,7 @@ COPY --chown=etherpad: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:etherpad ./settings.json.docker "${EP_DIR}"/settings.json From d0e257d8df45c62f9753de56b4f766bbaf6a04a6 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Thu, 4 Mar 2021 21:53:50 +0000 Subject: [PATCH 41/45] fix: upgrade resolve from 1.19.0 to 1.20.0 Snyk has created this PR to upgrade resolve from 1.19.0 to 1.20.0. See this package in npm: https://www.npmjs.com/package/resolve See this project in Snyk: https://app.snyk.io/org/johnmclear/project/d9a12bfb-7ccd-443f-9e22-f30d339cc8c5?utm_source=github&utm_medium=upgrade-pr --- src/package-lock.json | 8 ++++---- src/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 4a3b1aa6c..1d0ba9a00 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -7539,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" } }, diff --git a/src/package.json b/src/package.json index a56a656ac..02783b539 100644 --- a/src/package.json +++ b/src/package.json @@ -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", From 536db7553f980d5f1d5febeb550823ea65eb29a5 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 5 Mar 2021 06:38:50 +0000 Subject: [PATCH 42/45] tests: CI of updating from master > this commit. (#4912) * tests: CI of updating from master > this commit. In response to cypress eslint I thought I'd put some CI testing for if a PR might break automated upgrading. Matrix usage is probably overkill. * Update major-version-git-pull-update.yml * Name... * include a front end test * fix pathing * Clarity on what's happening * Update .github/workflows/major-version-git-pull-update.yml Co-authored-by: Richard Hansen * Update .github/workflows/major-version-git-pull-update.yml Co-authored-by: Richard Hansen * Update .github/workflows/major-version-git-pull-update.yml Co-authored-by: Richard Hansen Co-authored-by: Richard Hansen --- .../major-version-git-pull-update.yml | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .github/workflows/major-version-git-pull-update.yml 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 From 5ac90ab30f92cf71509263e46c71d35c58be36ed Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 5 Mar 2021 06:58:51 +0000 Subject: [PATCH 43/45] tests: Allow time for minification to complete Minification happens after the initial visit and request to pages. --- src/tests/frontend/cypress/integration/test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/frontend/cypress/integration/test.js b/src/tests/frontend/cypress/integration/test.js index 6574696db..893d4b669 100644 --- a/src/tests/frontend/cypress/integration/test.js +++ b/src/tests/frontend/cypress/integration/test.js @@ -10,6 +10,7 @@ Cypress.Commands.add('iframe', {prevSubject: 'element'}, 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'); From dabb4917edf3dd06a2e94d2b7b918aae8ba02f3d Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 5 Mar 2021 07:27:31 +0000 Subject: [PATCH 44/45] changelog 1.8.12 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 331a1b98e..12a9fd431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,27 @@ # 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 From de394f72a68115dea02a2cd4a870da15002aef32 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 5 Mar 2021 07:28:44 +0000 Subject: [PATCH 45/45] bump version --- src/package-lock.json | 2 +- src/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 1d0ba9a00..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": { diff --git a/src/package.json b/src/package.json index 02783b539..ada568d95 100644 --- a/src/package.json +++ b/src/package.json @@ -246,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" }