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