Added typescript to etherpad

* Fixed determining file extension.

* Added ts-node

* Fixed backend tests.

* Fixed frontend test runs.

* Fixed tests.

* Use script approach for starting etherpad.

* Change directory to src.

* Fixed env.

* Change directory

* Fixed build arg.

* Fixed docker build.

* Fixed.

* Fixed cypress file path.

* Fixed.

* Use latest node container.

* Fixed windows workflow.

* Use tsx and optimized docker image.

* Added workflow for type checks.

* Fixed.

* Added tsconfig.

* Converted more files to typescript.

* Removed commented keys.

* Typed caching middleware.

* Added script for checking the types.

* Moved SecretRotator to typescript.

* Fixed npm installation and moved to types folder.

* Use better scripts for watching typescript changes.

* Update windows.yml

* Fixed order of npm installation.

* Converted i18n.

* Added more types.

* Added more types.

* Fixed import.

* Fixed tests.

* Fixed tests.

* Fixed type checking test.

* Fixed stats

* Added express types.

* fixed.
This commit is contained in:
SamTV12345 2024-02-05 21:13:02 +01:00 committed by GitHub
parent c3202284bc
commit ead3c0ea38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 1259 additions and 612 deletions

View file

@ -0,0 +1,33 @@
name: "Perform type checks"
# any branch is useful for testing before a PR is submitted
on: [push, pull_request]
permissions:
contents: read
jobs:
performTypeCheck:
if: |
(github.event_name != 'pull_request')
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
name: perform type check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: |
src/package-lock.json
src/bin/doc/package-lock.json
-
name: Install all dependencies and symlink for ep_etherpad-lite
run: ./bin/installDeps.sh
working-directory: ./src
- name: Perform type check
working-directory: ./src
run: npm run ts-check

View file

@ -33,7 +33,7 @@ jobs:
- -
name: build docker image name: build docker image
run: | run: |
docker build -f Dockerfile -t epl-debian-slim . docker build -f Dockerfile -t epl-debian-slim --build-arg NODE_ENV=develop .
docker build -f src/tests/ratelimit/Dockerfile.nginx -t nginx-latest . docker build -f src/tests/ratelimit/Dockerfile.nginx -t nginx-latest .
docker build -f src/tests/ratelimit/Dockerfile.anotherip -t anotherip . docker build -f src/tests/ratelimit/Dockerfile.anotherip -t anotherip .
- -

View file

@ -87,13 +87,16 @@ jobs:
run: src/bin/installDeps.sh run: src/bin/installDeps.sh
- -
name: Run the backend tests name: Run the backend tests
run: cd src && npm test working-directory: ./src
run: npm test
- -
name: Install Cypress name: Install Cypress
run: cd src && npm install cypress --legacy-peer-deps working-directory: ./src
run: npm install cypress --legacy-peer-deps
- -
name: Run Etherpad & Test Frontend name: Run Etherpad & Test Frontend
working-directory: ./src
run: | run: |
node src/node/server.js & npm run dev &
curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test
./src/node_modules/cypress/bin/cypress run --config-file src/tests/frontend/cypress/cypress.config.js ./node_modules/cypress/bin/cypress run --config-file tests/frontend/cypress/cypress.config.js

View file

@ -113,14 +113,16 @@ jobs:
cache-dependency-path: | cache-dependency-path: |
etherpad/src/package-lock.json etherpad/src/package-lock.json
etherpad/src/bin/doc/package-lock.json etherpad/src/bin/doc/package-lock.json
- name: Install npm@6
run: npm install npm@6 -g
- -
name: Install Cypress name: Install Cypress
run: cd etherpad && cd src && npm install cypress --legacy-peer-deps run: cd etherpad && npm install cypress --legacy-peer-deps
- -
name: Run Etherpad name: Run Etherpad
run: | run: |
cd etherpad cd etherpad
node node_modules\ep_etherpad-lite\node\server.js & npm run prod --prefix ./src
curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test
src\node_modules\cypress\bin\cypress run --config-file src\tests\frontendcypress\cypress.config.js src\node_modules\cypress\bin\cypress run --config-file src\tests\frontendcypress\cypress.config.js
# On release, upload windows zip to GitHub release tab # On release, upload windows zip to GitHub release tab

View file

@ -4,7 +4,7 @@
# #
# Author: muxator # Author: muxator
FROM node:lts-alpine FROM node:alpine AS builder
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
ARG TIMEZONE= ARG TIMEZONE=
@ -63,7 +63,8 @@ ARG EP_UID=5001
ARG EP_GID=0 ARG EP_GID=0
ARG EP_SHELL= ARG EP_SHELL=
ENV NODE_ENV=production ARG NODE_ENV
ENV NODE_ENV=${NODE_ENV:-production}
RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \ RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \
useradd --system ${EP_UID:+--uid "${EP_UID}" --non-unique} --gid etherpad \ useradd --system ${EP_UID:+--uid "${EP_UID}" --non-unique} --gid etherpad \
@ -73,6 +74,7 @@ RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \
ARG EP_DIR=/opt/etherpad-lite ARG EP_DIR=/opt/etherpad-lite
RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}" RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}"
USER root
# the mkdir is needed for configuration of openjdk-11-jre-headless, see # the mkdir is needed for configuration of openjdk-11-jre-headless, see
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
RUN \ RUN \
@ -108,13 +110,16 @@ RUN { [ -z "${ETHERPAD_PLUGINS}" ] || \
COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json
# Fix group permissions # Fix group permissions
RUN chmod -R g=u . #RUN chmod -R g=u .
USER root USER root
RUN cd src && npm link RUN cd src && npm link
USER etherpad USER etherpad
WORKDIR /opt/etherpad-lite
HEALTHCHECK --interval=20s --timeout=3s CMD ["etherpad-healthcheck"] HEALTHCHECK --interval=20s --timeout=3s CMD ["etherpad-healthcheck"]
EXPOSE 9001 EXPOSE 9001
CMD ["etherpad"] CMD ["npm", "run", "prod", "--prefix", "./src"]

View file

@ -108,7 +108,7 @@ Update to the latest version with `git pull origin`, then run
If cloning to a subdirectory within another project, you may need to do the If cloning to a subdirectory within another project, you may need to do the
following: following:
1. Start the server manually (e.g. `node src/node/server.js`) 1. Start the server manually (e.g. `node src/node/server.ts`)
2. Edit the db `filename` in `settings.json` to the relative directory with 2. Edit the db `filename` in `settings.json` to the relative directory with
the file (e.g. `application/lib/etherpad-lite/var/dirty.db`) the file (e.g. `application/lib/etherpad-lite/var/dirty.db`)
3. Add auto-generated files to the main project `.gitignore` 3. Add auto-generated files to the main project `.gitignore`

View file

@ -2,7 +2,7 @@
These hooks are called on server-side. These hooks are called on server-side.
=== loadSettings === loadSettings
Called from: src/node/server.js Called from: src/node/server.ts
Things in context: Things in context:
@ -11,7 +11,7 @@ Things in context:
Use this hook to receive the global settings in your plugin. Use this hook to receive the global settings in your plugin.
=== shutdown === shutdown
Called from: src/node/server.js Called from: src/node/server.ts
Things in context: None Things in context: None

View file

@ -20,7 +20,7 @@ end script
script script
cd $EPHOME/ cd $EPHOME/
exec su -s /bin/sh -c 'exec "$0" "$@"' $EPUSER -- node src/node/server.js \ exec su -s /bin/sh -c 'exec "$0" "$@"' $EPUSER -- node src/node/server.ts \
>> $EPLOGS/access.log \ >> $EPLOGS/access.log \
2>> $EPLOGS/error.log 2>> $EPLOGS/error.log
echo "Etherpad is running on http://localhost:9001 - To change settings edit /opt/etherpad/settings.json" echo "Etherpad is running on http://localhost:9001 - To change settings edit /opt/etherpad/settings.json"

0
src/bin/push-after-release.sh Normal file → Executable file
View file

View file

@ -32,4 +32,4 @@ src/bin/installDeps.sh "$@" || exit 1
# Move to the node folder and start # Move to the node folder and start
log "Starting Etherpad..." log "Starting Etherpad..."
exec node src/node/server.js "$@" exec npm run dev --prefix ./src "$@"

View file

@ -10,4 +10,4 @@ Module file names start with a capital letter and uses camelCase
# Where does it start? # Where does it start?
server.js is started directly server.ts is started directly

View file

@ -24,7 +24,7 @@
const ueberDB = require('ueberdb2'); const ueberDB = require('ueberdb2');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const log4js = require('log4js'); const log4js = require('log4js');
const stats = require('../stats'); const stats = require('../stats')
const logger = log4js.getLogger('ueberDB'); const logger = log4js.getLogger('ueberDB');
@ -47,13 +47,13 @@ exports.init = async () => {
} }
for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) { for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) {
const f = exports.db[fn]; const f = exports.db[fn];
exports[fn] = async (...args) => await f.call(exports.db, ...args); exports[fn] = async (...args:string[]) => await f.call(exports.db, ...args);
Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f)); Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f));
Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f)); Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f));
} }
}; };
exports.shutdown = async (hookName, context) => { exports.shutdown = async (hookName: string, context:any) => {
if (exports.db != null) await exports.db.close(); if (exports.db != null) await exports.db.close();
exports.db = null; exports.db = null;
logger.log('Database closed'); logger.log('Database closed');

View file

@ -34,9 +34,10 @@ class SessionStore extends Store {
for (const {timeout} of this._expirations.values()) clearTimeout(timeout); for (const {timeout} of this._expirations.values()) clearTimeout(timeout);
} }
async _updateExpirations(sid, sess, updateDbExp = true) { async _updateExpirations(sid: string, sess: any, updateDbExp = true) {
const exp = this._expirations.get(sid) || {}; const exp = this._expirations.get(sid) || {};
clearTimeout(exp.timeout); clearTimeout(exp.timeout);
// @ts-ignore
const {cookie: {expires} = {}} = sess || {}; const {cookie: {expires} = {}} = sess || {};
if (expires) { if (expires) {
const sessExp = new Date(expires).getTime(); const sessExp = new Date(expires).getTime();
@ -63,23 +64,23 @@ class SessionStore extends Store {
return sess; return sess;
} }
async _write(sid, sess) { async _write(sid: string, sess: any) {
await DB.set(`sessionstorage:${sid}`, sess); await DB.set(`sessionstorage:${sid}`, sess);
} }
async _get(sid) { async _get(sid: string) {
logger.debug(`GET ${sid}`); logger.debug(`GET ${sid}`);
const s = await DB.get(`sessionstorage:${sid}`); const s = await DB.get(`sessionstorage:${sid}`);
return await this._updateExpirations(sid, s); return await this._updateExpirations(sid, s);
} }
async _set(sid, sess) { async _set(sid: string, sess:any) {
logger.debug(`SET ${sid}`); logger.debug(`SET ${sid}`);
sess = await this._updateExpirations(sid, sess); sess = await this._updateExpirations(sid, sess);
if (sess != null) await this._write(sid, sess); if (sess != null) await this._write(sid, sess);
} }
async _destroy(sid) { async _destroy(sid:string) {
logger.debug(`DESTROY ${sid}`); logger.debug(`DESTROY ${sid}`);
clearTimeout((this._expirations.get(sid) || {}).timeout); clearTimeout((this._expirations.get(sid) || {}).timeout);
this._expirations.delete(sid); this._expirations.delete(sid);
@ -89,7 +90,7 @@ class SessionStore extends Store {
// Note: express-session might call touch() before it calls set() for the first time. Ideally this // Note: express-session might call touch() before it calls set() for the first time. Ideally this
// would behave like set() in that case but it's OK if it doesn't -- express-session will call // would behave like set() in that case but it's OK if it doesn't -- express-session will call
// set() soon enough. // set() soon enough.
async _touch(sid, sess) { async _touch(sid: string, sess:any) {
logger.debug(`TOUCH ${sid}`); logger.debug(`TOUCH ${sid}`);
sess = await this._updateExpirations(sid, sess, false); sess = await this._updateExpirations(sid, sess, false);
if (sess == null) return; // Already expired. if (sess == null) return; // Already expired.

View file

@ -35,7 +35,7 @@ const log4js = require('log4js');
const messageLogger = log4js.getLogger('message'); const messageLogger = log4js.getLogger('message');
const accessLogger = log4js.getLogger('access'); const accessLogger = log4js.getLogger('access');
const hooks = require('../../static/js/pluginfw/hooks.js'); const hooks = require('../../static/js/pluginfw/hooks.js');
const stats = require('../stats'); const stats = require('../stats')
const assert = require('assert').strict; const assert = require('assert').strict;
const {RateLimiterMemory} = require('rate-limiter-flexible'); const {RateLimiterMemory} = require('rate-limiter-flexible');
const webaccess = require('../hooks/express/webaccess'); const webaccess = require('../hooks/express/webaccess');
@ -133,7 +133,7 @@ class Channels {
const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(socket, message)); const padChannels = new Channels((ch, {socket, message}) => handleUserChanges(socket, message));
/** /**
* This Method is called by server.js to tell the message handler on which socket it should send * This Method is called by server.ts to tell the message handler on which socket it should send
* @param socket_io The Socket * @param socket_io The Socket
*/ */
exports.setSocketIO = (socket_io) => { exports.setSocketIO = (socket_io) => {

View file

@ -22,7 +22,7 @@
const log4js = require('log4js'); const log4js = require('log4js');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const stats = require('../stats'); const stats = require('../../node/stats')
const logger = log4js.getLogger('socket.io'); const logger = log4js.getLogger('socket.io');

View file

@ -1,25 +1,31 @@
'use strict'; 'use strict';
const _ = require('underscore'); import {Socket} from "node:net";
const SecretRotator = require('../security/SecretRotator'); import type {MapArrayType} from "../types/MapType";
const cookieParser = require('cookie-parser');
const events = require('events'); import _ from 'underscore';
const express = require('express'); // @ts-ignore
const expressSession = require('express-session'); import cookieParser from 'cookie-parser';
const fs = require('fs'); import events from 'events';
import express from 'express';
// @ts-ignore
import expressSession from 'express-session';
import fs from 'fs';
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
const log4js = require('log4js'); import log4js from 'log4js';
const SessionStore = require('../db/SessionStore'); const SessionStore = require('../db/SessionStore');
const settings = require('../utils/Settings'); const settings = require('../utils/Settings');
const stats = require('../stats'); const stats = require('../stats')
const util = require('util'); import util from 'util';
const webaccess = require('./express/webaccess'); const webaccess = require('./express/webaccess');
let secretRotator = null; import SecretRotator from '../security/SecretRotator';
let secretRotator: SecretRotator|null = null;
const logger = log4js.getLogger('http'); const logger = log4js.getLogger('http');
let serverName; let serverName:string;
let sessionStore; let sessionStore: { shutdown: () => void; } | null;
const sockets = new Set(); const sockets:Set<Socket> = new Set();
const socketsEvents = new events.EventEmitter(); const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime'); const startTime = stats.settableGauge('httpStartTime');
@ -101,7 +107,7 @@ exports.restartServer = async () => {
console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- server key file: ${settings.ssl.key}`);
console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`);
const options = { const options: MapArrayType<any> = {
key: fs.readFileSync(settings.ssl.key), key: fs.readFileSync(settings.ssl.key),
cert: fs.readFileSync(settings.ssl.cert), cert: fs.readFileSync(settings.ssl.cert),
}; };
@ -163,7 +169,7 @@ exports.restartServer = async () => {
app.use((req, res, next) => { app.use((req, res, next) => {
const stopWatch = stats.timer('httpRequests').start(); const stopWatch = stats.timer('httpRequests').start();
const sendFn = res.send.bind(res); const sendFn = res.send.bind(res);
res.send = (...args) => { stopWatch.end(); sendFn(...args); }; res.send = (...args) => { stopWatch.end(); return sendFn(...args); };
next(); next();
}); });
@ -173,7 +179,7 @@ exports.restartServer = async () => {
// anyway. // anyway.
if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) {
app.use(log4js.connectLogger(logger, { app.use(log4js.connectLogger(logger, {
level: log4js.levels.DEBUG, level: log4js.levels.DEBUG.levelStr,
format: ':status, :method :url', format: ':status, :method :url',
})); }));
} }
@ -237,7 +243,7 @@ exports.restartServer = async () => {
hooks.aCallAll('expressConfigure', {app}), hooks.aCallAll('expressConfigure', {app}),
hooks.aCallAll('expressCreateServer', {app, server: exports.server}), hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
]); ]);
exports.server.on('connection', (socket) => { exports.server.on('connection', (socket:Socket) => {
sockets.add(socket); sockets.add(socket);
socketsEvents.emit('updated'); socketsEvents.emit('updated');
socket.on('close', () => { socket.on('close', () => {
@ -250,6 +256,6 @@ exports.restartServer = async () => {
logger.info('HTTP server listening for connections'); logger.info('HTTP server listening for connections');
}; };
exports.shutdown = async (hookName, context) => { exports.shutdown = async (hookName:string, context: any) => {
await closeServer(); await closeServer();
}; };

View file

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const eejs = require('../../eejs'); import {ArgsExpressType} from "../../types/ArgsExpressType";
const eejs = require('../../eejs');
/** /**
* Add the admin navigation link * Add the admin navigation link
@ -9,8 +10,8 @@ const eejs = require('../../eejs');
* @param {Function} cb the callback function * @param {Function} cb the callback function
* @return {*} * @return {*}
*/ */
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => {
args.app.get('/admin', (req, res) => { args.app.get('/admin', (req:any, res:any) => {
if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/'); if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/');
res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req})); res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req}));
}); });

View file

@ -1,5 +1,11 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import {Socket} from "node:net";
import {ErrorCaused} from "../../types/ErrorCaused";
import {QueryType} from "../../types/QueryType";
import {PluginType} from "../../types/Plugin";
const eejs = require('../../eejs'); const eejs = require('../../eejs');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const installer = require('../../../static/js/pluginfw/installer'); const installer = require('../../../static/js/pluginfw/installer');
@ -8,8 +14,8 @@ const plugins = require('../../../static/js/pluginfw/plugins');
const semver = require('semver'); const semver = require('semver');
const UpdateCheck = require('../../utils/UpdateCheck'); const UpdateCheck = require('../../utils/UpdateCheck');
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function) => {
args.app.get('/admin/plugins', (req, res) => { args.app.get('/admin/plugins', (req:any, res:any) => {
res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', { res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', {
plugins: pluginDefs.plugins, plugins: pluginDefs.plugins,
req, req,
@ -17,7 +23,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
})); }));
}); });
args.app.get('/admin/plugins/info', (req, res) => { args.app.get('/admin/plugins/info', (req:any, res:any) => {
const gitCommit = settings.getGitCommit(); const gitCommit = settings.getGitCommit();
const epVersion = settings.getEpVersion(); const epVersion = settings.getEpVersion();
@ -36,13 +42,14 @@ exports.expressCreateServer = (hookName, args, cb) => {
return cb(); return cb();
}; };
exports.socketio = (hookName, args, cb) => { exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
const io = args.io.of('/pluginfw/installer'); const io = args.io.of('/pluginfw/installer');
io.on('connection', (socket) => { io.on('connection', (socket:any) => {
// @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
if (!isAdmin) return; if (!isAdmin) return;
socket.on('getInstalled', (query) => { socket.on('getInstalled', (query:string) => {
// send currently installed plugins // send currently installed plugins
const installed = const installed =
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package); Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
@ -66,13 +73,14 @@ exports.socketio = (hookName, args, cb) => {
socket.emit('results:updatable', {updatable}); socket.emit('results:updatable', {updatable});
} catch (err) { } catch (err) {
console.warn(err.stack || err.toString()); const errc = err as ErrorCaused
console.warn(errc.stack || errc.toString());
socket.emit('results:updatable', {updatable: {}}); socket.emit('results:updatable', {updatable: {}});
} }
}); });
socket.on('getAvailable', async (query) => { socket.on('getAvailable', async (query:string) => {
try { try {
const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false); const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false);
socket.emit('results:available', results); socket.emit('results:available', results);
@ -82,7 +90,7 @@ exports.socketio = (hookName, args, cb) => {
} }
}); });
socket.on('search', async (query) => { socket.on('search', async (query: QueryType) => {
try { try {
const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
let res = Object.keys(results) let res = Object.keys(results)
@ -98,8 +106,8 @@ exports.socketio = (hookName, args, cb) => {
} }
}); });
socket.on('install', (pluginName) => { socket.on('install', (pluginName: string) => {
installer.install(pluginName, (err) => { installer.install(pluginName, (err: ErrorCaused) => {
if (err) console.warn(err.stack || err.toString()); if (err) console.warn(err.stack || err.toString());
socket.emit('finished:install', { socket.emit('finished:install', {
@ -110,8 +118,8 @@ exports.socketio = (hookName, args, cb) => {
}); });
}); });
socket.on('uninstall', (pluginName) => { socket.on('uninstall', (pluginName:string) => {
installer.uninstall(pluginName, (err) => { installer.uninstall(pluginName, (err:ErrorCaused) => {
if (err) console.warn(err.stack || err.toString()); if (err) console.warn(err.stack || err.toString());
socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null}); socket.emit('finished:uninstall', {plugin: pluginName, error: err ? err.message : null});
@ -128,11 +136,13 @@ exports.socketio = (hookName, args, cb) => {
* @param {String} dir The directory of the plugin * @param {String} dir The directory of the plugin
* @return {Object[]} * @return {Object[]}
*/ */
const sortPluginList = (plugins, property, /* ASC?*/dir) => plugins.sort((a, b) => { const sortPluginList = (plugins:PluginType[], property:string, /* ASC?*/dir:string): object[] => plugins.sort((a, b) => {
// @ts-ignore
if (a[property] < b[property]) { if (a[property] < b[property]) {
return dir ? -1 : 1; return dir ? -1 : 1;
} }
// @ts-ignore
if (a[property] > b[property]) { if (a[property] > b[property]) {
return dir ? 1 : -1; return dir ? 1 : -1;
} }

View file

@ -6,8 +6,8 @@ const hooks = require('../../../static/js/pluginfw/hooks');
const plugins = require('../../../static/js/pluginfw/plugins'); const plugins = require('../../../static/js/pluginfw/plugins');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
exports.expressCreateServer = (hookName, {app}) => { exports.expressCreateServer = (hookName:string, {app}:any) => {
app.get('/admin/settings', (req, res) => { app.get('/admin/settings', (req:any, res:any) => {
res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', {
req, req,
settings: '', settings: '',
@ -16,12 +16,13 @@ exports.expressCreateServer = (hookName, {app}) => {
}); });
}; };
exports.socketio = (hookName, {io}) => { exports.socketio = (hookName:string, {io}:any) => {
io.of('/settings').on('connection', (socket) => { io.of('/settings').on('connection', (socket: any ) => {
// @ts-ignore
const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request;
if (!isAdmin) return; if (!isAdmin) return;
socket.on('load', async (query) => { socket.on('load', async (query:string):Promise<any> => {
let data; let data;
try { try {
data = await fsp.readFile(settings.settingsFilename, 'utf8'); data = await fsp.readFile(settings.settingsFilename, 'utf8');
@ -36,7 +37,7 @@ exports.socketio = (hookName, {io}) => {
} }
}); });
socket.on('saveSettings', async (newSettings) => { socket.on('saveSettings', async (newSettings:string) => {
await fsp.writeFile(settings.settingsFilename, newSettings); await fsp.writeFile(settings.settingsFilename, newSettings);
socket.emit('saveprogress', 'saved'); socket.emit('saveprogress', 'saved');
}); });

View file

@ -6,15 +6,15 @@ const {Formidable} = require('formidable');
const apiHandler = require('../../handler/APIHandler'); const apiHandler = require('../../handler/APIHandler');
const util = require('util'); const util = require('util');
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
// The Etherpad client side sends information about how a disconnect happened // The Etherpad client side sends information about how a disconnect happened
app.post('/ep/pad/connection-diagnostic-info', async (req, res) => { app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => {
const [fields, files] = await (new Formidable({})).parse(req); const [fields, files] = await (new Formidable({})).parse(req);
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`); clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
res.end('OK'); res.end('OK');
}); });
const parseJserrorForm = async (req) => { const parseJserrorForm = async (req:any) => {
const form = new Formidable({ const form = new Formidable({
maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used. maxFileSize: 1, // Files are not expected. Not sure if 0 means unlimited, so 1 is used.
}); });
@ -23,11 +23,11 @@ exports.expressPreSession = async (hookName, {app}) => {
}; };
// The Etherpad client side sends information about client side javscript errors // The Etherpad client side sends information about client side javscript errors
app.post('/jserror', (req, res, next) => { app.post('/jserror', (req:any, res:any, next:Function) => {
(async () => { (async () => {
const data = JSON.parse(await parseJserrorForm(req)); const data = JSON.parse(await parseJserrorForm(req));
clientLogger.warn(`${data.msg} --`, { clientLogger.warn(`${data.msg} --`, {
[util.inspect.custom]: (depth, options) => { [util.inspect.custom]: (depth: number, options:any) => {
// Depth is forced to infinity to ensure that all of the provided data is logged. // Depth is forced to infinity to ensure that all of the provided data is logged.
options = Object.assign({}, options, {depth: Infinity, colors: true}); options = Object.assign({}, options, {depth: Infinity, colors: true});
return util.inspect(data, options); return util.inspect(data, options);
@ -38,7 +38,7 @@ exports.expressPreSession = async (hookName, {app}) => {
}); });
// Provide a possibility to query the latest available API version // Provide a possibility to query the latest available API version
app.get('/api', (req, res) => { app.get('/api', (req:any, res:any) => {
res.json({currentVersion: apiHandler.latestApiVersion}); res.json({currentVersion: apiHandler.latestApiVersion});
}); });
}; };

View file

@ -1,12 +1,15 @@
'use strict'; 'use strict';
const stats = require('../../stats'); import {ArgsExpressType} from "../../types/ArgsExpressType";
import {ErrorCaused} from "../../types/ErrorCaused";
exports.expressCreateServer = (hook_name, args, cb) => { const stats = require('../../stats')
exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => {
exports.app = args.app; exports.app = args.app;
// Handle errors // Handle errors
args.app.use((err, req, res, next) => { args.app.use((err:ErrorCaused, req:any, res:any, next:Function) => {
// if an error occurs Connect will pass it down // if an error occurs Connect will pass it down
// through these "error-handling" middleware // through these "error-handling" middleware
// allowing you to respond however you like // allowing you to respond however you like

View file

@ -1,5 +1,7 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
const hasPadAccess = require('../../padaccess'); const hasPadAccess = require('../../padaccess');
const settings = require('../../utils/Settings'); const settings = require('../../utils/Settings');
const exportHandler = require('../../handler/ExportHandler'); const exportHandler = require('../../handler/ExportHandler');
@ -10,10 +12,10 @@ const rateLimit = require('express-rate-limit');
const securityManager = require('../../db/SecurityManager'); const securityManager = require('../../db/SecurityManager');
const webaccess = require('./webaccess'); const webaccess = require('./webaccess');
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
const limiter = rateLimit({ const limiter = rateLimit({
...settings.importExportRateLimiting, ...settings.importExportRateLimiting,
handler: (request, response, next, options) => { handler: (request:any) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) { if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs // when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' + console.warn('Import/Export rate limiter triggered on ' +
@ -24,7 +26,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
// handle export requests // handle export requests
args.app.use('/p/:pad/:rev?/export/:type', limiter); args.app.use('/p/:pad/:rev?/export/:type', limiter);
args.app.get('/p/:pad/:rev?/export/:type', (req, res, next) => { args.app.get('/p/:pad/:rev?/export/:type', (req:any, res:any, next:Function) => {
(async () => { (async () => {
const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad'];
// send a 404 if we don't support this filetype // send a 404 if we don't support this filetype
@ -70,8 +72,9 @@ exports.expressCreateServer = (hookName, args, cb) => {
// handle import requests // handle import requests
args.app.use('/p/:pad/import', limiter); args.app.use('/p/:pad/import', limiter);
args.app.post('/p/:pad/import', (req, res, next) => { args.app.post('/p/:pad/import', (req:any, res:any, next:Function) => {
(async () => { (async () => {
// @ts-ignore
const {session: {user} = {}} = req; const {session: {user} = {}} = req;
const {accessStatus, authorID: authorId} = await securityManager.checkAccess( const {accessStatus, authorID: authorId} = await securityManager.checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, user); req.params.pad, req.cookies.sessionID, req.cookies.token, user);

View file

@ -1,5 +1,9 @@
'use strict'; 'use strict';
import {OpenAPIOperations, OpenAPISuccessResponse, SwaggerUIResource} from "../../types/SwaggerUIResource";
import {MapArrayType} from "../../types/MapType";
import {ErrorCaused} from "../../types/ErrorCaused";
/** /**
* node/hooks/express/openapi.js * node/hooks/express/openapi.js
* *
@ -52,8 +56,9 @@ const APIPathStyle = {
REST: 'rest', // restful paths e.g. /rest/group/create REST: 'rest', // restful paths e.g. /rest/group/create
}; };
// API resources - describe your API endpoints here // API resources - describe your API endpoints here
const resources = { const resources:SwaggerUIResource = {
// Group // Group
group: { group: {
create: { create: {
@ -372,7 +377,7 @@ const defaultResponses = {
}, },
}; };
const defaultResponseRefs = { const defaultResponseRefs:OpenAPISuccessResponse = {
200: { 200: {
$ref: '#/components/responses/Success', $ref: '#/components/responses/Success',
}, },
@ -388,16 +393,16 @@ const defaultResponseRefs = {
}; };
// convert to a dictionary of operation objects // convert to a dictionary of operation objects
const operations = {}; const operations: OpenAPIOperations = {};
for (const [resource, actions] of Object.entries(resources)) { for (const [resource, actions] of Object.entries(resources)) {
for (const [action, spec] of Object.entries(actions)) { for (const [action, spec] of Object.entries(actions)) {
const {operationId,responseSchema, ...operation} = spec; const {operationId,responseSchema, ...operation} = spec;
// add response objects // add response objects
const responses = {...defaultResponseRefs}; const responses:OpenAPISuccessResponse = {...defaultResponseRefs};
if (responseSchema) { if (responseSchema) {
responses[200] = cloneDeep(defaultResponses.Success); responses[200] = cloneDeep(defaultResponses.Success);
responses[200].content['application/json'].schema.properties.data = { responses[200].content!['application/json'].schema.properties.data = {
type: 'object', type: 'object',
properties: responseSchema, properties: responseSchema,
}; };
@ -414,7 +419,7 @@ for (const [resource, actions] of Object.entries(resources)) {
} }
} }
const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { const generateDefinitionForVersion = (version:string, style = APIPathStyle.FLAT) => {
const definition = { const definition = {
openapi: OPENAPI_VERSION, openapi: OPENAPI_VERSION,
info, info,
@ -490,7 +495,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// build operations // build operations
for (const funcName of Object.keys(apiHandler.version[version])) { for (const funcName of Object.keys(apiHandler.version[version])) {
let operation = {}; let operation:OpenAPIOperations = {};
if (operations[funcName]) { if (operations[funcName]) {
operation = {...operations[funcName]}; operation = {...operations[funcName]};
} else { } else {
@ -505,7 +510,9 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
operation.parameters = operation.parameters || []; operation.parameters = operation.parameters || [];
for (const paramName of apiHandler.version[version][funcName]) { for (const paramName of apiHandler.version[version][funcName]) {
operation.parameters.push({$ref: `#/components/parameters/${paramName}`}); operation.parameters.push({$ref: `#/components/parameters/${paramName}`});
// @ts-ignore
if (!definition.components.parameters[paramName]) { if (!definition.components.parameters[paramName]) {
// @ts-ignore
definition.components.parameters[paramName] = { definition.components.parameters[paramName] = {
name: paramName, name: paramName,
in: 'query', in: 'query',
@ -525,6 +532,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
// add to definition // add to definition
// NOTE: It may be confusing that every operation can be called with both GET and POST // NOTE: It may be confusing that every operation can be called with both GET and POST
// @ts-ignore
definition.paths[path] = { definition.paths[path] = {
get: { get: {
...operation, ...operation,
@ -539,7 +547,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => {
return definition; return definition;
}; };
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
// create openapi-backend handlers for each api version under /api/{version}/* // create openapi-backend handlers for each api version under /api/{version}/*
for (const version of Object.keys(apiHandler.version)) { for (const version of Object.keys(apiHandler.version)) {
// we support two different styles of api: flat + rest // we support two different styles of api: flat + rest
@ -552,7 +560,7 @@ exports.expressPreSession = async (hookName, {app}) => {
const definition = generateDefinitionForVersion(version, style); const definition = generateDefinitionForVersion(version, style);
// serve version specific openapi definition // serve version specific openapi definition
app.get(`${apiRoot}/openapi.json`, (req, res) => { app.get(`${apiRoot}/openapi.json`, (req:any, res:any) => {
// For openapi definitions, wide CORS is probably fine // For openapi definitions, wide CORS is probably fine
res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Origin', '*');
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
@ -561,7 +569,7 @@ exports.expressPreSession = async (hookName, {app}) => {
// serve latest openapi definition file under /api/openapi.json // serve latest openapi definition file under /api/openapi.json
const isLatestAPIVersion = version === apiHandler.latestApiVersion; const isLatestAPIVersion = version === apiHandler.latestApiVersion;
if (isLatestAPIVersion) { if (isLatestAPIVersion) {
app.get(`/${style}/openapi.json`, (req, res) => { app.get(`/${style}/openapi.json`, (req:any, res:any) => {
res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Origin', '*');
res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); res.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]});
}); });
@ -588,12 +596,12 @@ exports.expressPreSession = async (hookName, {app}) => {
// register operation handlers // register operation handlers
for (const funcName of Object.keys(apiHandler.version[version])) { for (const funcName of Object.keys(apiHandler.version[version])) {
const handler = async (c, req, res) => { const handler = async (c: any, req:any, res:any) => {
// parse fields from request // parse fields from request
const {header, params, query} = c.request; const {header, params, query} = c.request;
// read form data if method was POST // read form data if method was POST
let formData = {}; let formData:MapArrayType<any> = {};
if (c.request.method === 'post') { if (c.request.method === 'post') {
const form = new IncomingForm(); const form = new IncomingForm();
formData = (await form.parse(req))[0]; formData = (await form.parse(req))[0];
@ -615,18 +623,19 @@ exports.expressPreSession = async (hookName, {app}) => {
try { try {
data = await apiHandler.handle(version, funcName, fields, req, res); data = await apiHandler.handle(version, funcName, fields, req, res);
} catch (err) { } catch (err) {
const errCaused = err as ErrorCaused
// convert all errors to http errors // convert all errors to http errors
if (createHTTPError.isHttpError(err)) { if (createHTTPError.isHttpError(err)) {
// pass http errors thrown by handler forward // pass http errors thrown by handler forward
throw err; throw err;
} else if (err.name === 'apierror') { } else if (errCaused.name === 'apierror') {
// parameters were wrong and the api stopped execution, pass the error // parameters were wrong and the api stopped execution, pass the error
// convert to http error // convert to http error
throw new createHTTPError.BadRequest(err.message); throw new createHTTPError.BadRequest(errCaused.message);
} else { } else {
// an unknown error happened // an unknown error happened
// log it and throw internal error // log it and throw internal error
logger.error(err.stack || err.toString()); logger.error(errCaused.stack || errCaused.toString());
throw new createHTTPError.InternalError('internal error'); throw new createHTTPError.InternalError('internal error');
} }
} }
@ -649,7 +658,7 @@ exports.expressPreSession = async (hookName, {app}) => {
// start and bind to express // start and bind to express
api.init(); api.init();
app.use(apiRoot, async (req, res) => { app.use(apiRoot, async (req:any, res:any) => {
let response = null; let response = null;
try { try {
if (style === APIPathStyle.REST) { if (style === APIPathStyle.REST) {
@ -660,31 +669,33 @@ exports.expressPreSession = async (hookName, {app}) => {
// pass to openapi-backend handler // pass to openapi-backend handler
response = await api.handleRequest(req, req, res); response = await api.handleRequest(req, req, res);
} catch (err) { } catch (err) {
const errCaused = err as ErrorCaused
// handle http errors // handle http errors
res.statusCode = err.statusCode || 500; // @ts-ignore
res.statusCode = errCaused.statusCode || 500;
// convert to our json response format // convert to our json response format
// https://github.com/ether/etherpad-lite/tree/master/doc/api/http_api.md#response-format // https://github.com/ether/etherpad-lite/tree/master/doc/api/http_api.md#response-format
switch (res.statusCode) { switch (res.statusCode) {
case 403: // forbidden case 403: // forbidden
response = {code: 4, message: err.message, data: null}; response = {code: 4, message: errCaused.message, data: null};
break; break;
case 401: // unauthorized (no or wrong api key) case 401: // unauthorized (no or wrong api key)
response = {code: 4, message: err.message, data: null}; response = {code: 4, message: errCaused.message, data: null};
break; break;
case 404: // not found (no such function) case 404: // not found (no such function)
response = {code: 3, message: err.message, data: null}; response = {code: 3, message: errCaused.message, data: null};
break; break;
case 500: // server error (internal error) case 500: // server error (internal error)
response = {code: 2, message: err.message, data: null}; response = {code: 2, message: errCaused.message, data: null};
break; break;
case 400: // bad request (wrong parameters) case 400: // bad request (wrong parameters)
// respond with 200 OK to keep old behavior and pass tests // respond with 200 OK to keep old behavior and pass tests
res.statusCode = 200; // @TODO: this is bad api design res.statusCode = 200; // @TODO: this is bad api design
response = {code: 1, message: err.message, data: null}; response = {code: 1, message: errCaused.message, data: null};
break; break;
default: default:
response = {code: 1, message: err.message, data: null}; response = {code: 1, message: errCaused.message, data: null};
break; break;
} }
} }
@ -702,7 +713,7 @@ exports.expressPreSession = async (hookName, {app}) => {
* @param {APIPathStyle} style The style of the API path * @param {APIPathStyle} style The style of the API path
* @return {String} The root path for the API version * @return {String} The root path for the API version
*/ */
const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/${version}`; const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): string => `/${style}/${version}`;
/** /**
* Helper to generate an OpenAPI server object when serving definitions * Helper to generate an OpenAPI server object when serving definitions
@ -710,6 +721,8 @@ const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/
* @param {Request} req The express request object * @param {Request} req The express request object
* @return {url: String} The server object for the OpenAPI definition location * @return {url: String} The server object for the OpenAPI definition location
*/ */
const generateServerForApiVersion = (apiRoot, req) => ({ const generateServerForApiVersion = (apiRoot:string, req:any): {
url:string
} => ({
url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`, url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`,
}); });

View file

@ -1,10 +1,12 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
const padManager = require('../../db/PadManager'); const padManager = require('../../db/PadManager');
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
// redirects browser to the pad's sanitized url if needed. otherwise, renders the html // redirects browser to the pad's sanitized url if needed. otherwise, renders the html
args.app.param('pad', (req, res, next, padId) => { args.app.param('pad', (req:any, res:any, next:Function, padId:string) => {
(async () => { (async () => {
// ensure the padname is valid and the url doesn't end with a / // ensure the padname is valid and the url doesn't end with a /
if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) {

View file

@ -1,5 +1,7 @@
'use strict'; 'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
const events = require('events'); const events = require('events');
const express = require('../express'); const express = require('../express');
const log4js = require('log4js'); const log4js = require('log4js');
@ -10,7 +12,7 @@ const socketIORouter = require('../../handler/SocketIORouter');
const hooks = require('../../../static/js/pluginfw/hooks'); const hooks = require('../../../static/js/pluginfw/hooks');
const padMessageHandler = require('../../handler/PadMessageHandler'); const padMessageHandler = require('../../handler/PadMessageHandler');
let io; let io:any;
const logger = log4js.getLogger('socket.io'); const logger = log4js.getLogger('socket.io');
const sockets = new Set(); const sockets = new Set();
const socketsEvents = new events.EventEmitter(); const socketsEvents = new events.EventEmitter();
@ -46,7 +48,7 @@ exports.expressCloseServer = async () => {
logger.info('All socket.io clients have disconnected'); logger.info('All socket.io clients have disconnected');
}; };
exports.expressCreateServer = (hookName, args, cb) => { exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
// init socket.io and redirect all requests to the MessageHandler // init socket.io and redirect all requests to the MessageHandler
// there shouldn't be a browser that isn't compatible to all // there shouldn't be a browser that isn't compatible to all
// transports in this list at once // transports in this list at once
@ -77,7 +79,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, maxHttpBufferSize: settings.socketIo.maxHttpBufferSize,
}); });
io.on('connect', (socket) => { io.on('connect', (socket:any) => {
sockets.add(socket); sockets.add(socket);
socketsEvents.emit('updated'); socketsEvents.emit('updated');
socket.on('disconnect', () => { socket.on('disconnect', () => {
@ -86,7 +88,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
}); });
}); });
io.use((socket, next) => { io.use((socket:any, next: Function) => {
const req = socket.request; const req = socket.request;
// Express sets req.ip but socket.io does not. Replicate Express's behavior here. // Express sets req.ip but socket.io does not. Replicate Express's behavior here.
if (req.ip == null) { if (req.ip == null) {
@ -105,8 +107,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
express.sessionMiddleware(req, {}, next); express.sessionMiddleware(req, {}, next);
}); });
io.use((socket, next) => { io.use((socket:any, next:Function) => {
socket.conn.on('packet', (packet) => { socket.conn.on('packet', (packet:string) => {
// Tell express-session that the session is still active. The session store can use these // Tell express-session that the session is still active. The session store can use these
// touch events to defer automatic session cleanup, and if express-session is configured with // touch events to defer automatic session cleanup, and if express-session is configured with
// rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not // rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not

View file

@ -1,5 +1,8 @@
'use strict'; 'use strict';
import type {MapArrayType} from "../types/MapType";
import {I18nPluginDefs} from "../types/I18nPluginDefs";
const languages = require('languages4translatewiki'); const languages = require('languages4translatewiki');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -11,17 +14,17 @@ const settings = require('../utils/Settings');
// returns all existing messages merged together and grouped by langcode // returns all existing messages merged together and grouped by langcode
// {es: {"foo": "string"}, en:...} // {es: {"foo": "string"}, en:...}
const getAllLocales = () => { const getAllLocales = () => {
const locales2paths = {}; const locales2paths:MapArrayType<string[]> = {};
// Puts the paths of all locale files contained in a given directory // Puts the paths of all locale files contained in a given directory
// into `locales2paths` (files from various dirs are grouped by lang code) // into `locales2paths` (files from various dirs are grouped by lang code)
// (only json files with valid language code as name) // (only json files with valid language code as name)
const extractLangs = (dir) => { const extractLangs = (dir: string) => {
if (!existsSync(dir)) return; if (!existsSync(dir)) return;
let stat = fs.lstatSync(dir); let stat = fs.lstatSync(dir);
if (!stat.isDirectory() || stat.isSymbolicLink()) return; if (!stat.isDirectory() || stat.isSymbolicLink()) return;
fs.readdirSync(dir).forEach((file) => { fs.readdirSync(dir).forEach((file:string) => {
file = path.resolve(dir, file); file = path.resolve(dir, file);
stat = fs.lstatSync(file); stat = fs.lstatSync(file);
if (stat.isDirectory() || stat.isSymbolicLink()) return; if (stat.isDirectory() || stat.isSymbolicLink()) return;
@ -40,15 +43,15 @@ const getAllLocales = () => {
extractLangs(path.join(settings.root, 'src/locales')); extractLangs(path.join(settings.root, 'src/locales'));
// add plugins languages (if any) // add plugins languages (if any)
for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) { for (const {package: {path: pluginPath}} of Object.values<I18nPluginDefs>(pluginDefs.plugins)) {
// plugin locales should overwrite etherpad's core locales // plugin locales should overwrite etherpad's core locales
if (pluginPath.endsWith('/ep_etherpad-lite') === true) continue; if (pluginPath.endsWith('/ep_etherpad-lite')) continue;
extractLangs(path.join(pluginPath, 'locales')); extractLangs(path.join(pluginPath, 'locales'));
} }
// Build a locale index (merge all locale data other than user-supplied overrides) // Build a locale index (merge all locale data other than user-supplied overrides)
const locales = {}; const locales:MapArrayType<any> = {};
_.each(locales2paths, (files, langcode) => { _.each(locales2paths, (files: string[], langcode: string) => {
locales[langcode] = {}; locales[langcode] = {};
files.forEach((file) => { files.forEach((file) => {
@ -70,9 +73,9 @@ const getAllLocales = () => {
'for Customization for Administrators, under Localization.'); 'for Customization for Administrators, under Localization.');
if (settings.customLocaleStrings) { if (settings.customLocaleStrings) {
if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr; if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr;
_.each(settings.customLocaleStrings, (overrides, langcode) => { _.each(settings.customLocaleStrings, (overrides:MapArrayType<string> , langcode:string) => {
if (typeof overrides !== 'object') throw wrongFormatErr; if (typeof overrides !== 'object') throw wrongFormatErr;
_.each(overrides, (localeString, key) => { _.each(overrides, (localeString:string|object, key:string) => {
if (typeof localeString !== 'string') throw wrongFormatErr; if (typeof localeString !== 'string') throw wrongFormatErr;
const locale = locales[langcode]; const locale = locales[langcode];
@ -102,8 +105,8 @@ const getAllLocales = () => {
// returns a hash of all available languages availables with nativeName and direction // returns a hash of all available languages availables with nativeName and direction
// e.g. { es: {nativeName: "español", direction: "ltr"}, ... } // e.g. { es: {nativeName: "español", direction: "ltr"}, ... }
const getAvailableLangs = (locales) => { const getAvailableLangs = (locales:MapArrayType<any>) => {
const result = {}; const result:MapArrayType<string> = {};
for (const langcode of Object.keys(locales)) { for (const langcode of Object.keys(locales)) {
result[langcode] = languages.getLanguageInfo(langcode); result[langcode] = languages.getLanguageInfo(langcode);
} }
@ -111,7 +114,7 @@ const getAvailableLangs = (locales) => {
}; };
// returns locale index that will be served in /locales.json // returns locale index that will be served in /locales.json
const generateLocaleIndex = (locales) => { const generateLocaleIndex = (locales:MapArrayType<string>) => {
const result = _.clone(locales); // keep English strings const result = _.clone(locales); // keep English strings
for (const langcode of Object.keys(locales)) { for (const langcode of Object.keys(locales)) {
if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`; if (langcode !== 'en') result[langcode] = `locales/${langcode}.json`;
@ -120,13 +123,13 @@ const generateLocaleIndex = (locales) => {
}; };
exports.expressPreSession = async (hookName, {app}) => { exports.expressPreSession = async (hookName:string, {app}:any) => {
// regenerate locales on server restart // regenerate locales on server restart
const locales = getAllLocales(); const locales = getAllLocales();
const localeIndex = generateLocaleIndex(locales); const localeIndex = generateLocaleIndex(locales);
exports.availableLangs = getAvailableLangs(locales); exports.availableLangs = getAvailableLangs(locales);
app.get('/locales/:locale', (req, res) => { app.get('/locales/:locale', (req:any, res:any) => {
// works with /locale/en and /locale/en.json requests // works with /locale/en and /locale/en.json requests
const locale = req.params.locale.split('.')[0]; const locale = req.params.locale.split('.')[0];
if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) { if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) {
@ -138,7 +141,7 @@ exports.expressPreSession = async (hookName, {app}) => {
} }
}); });
app.get('/locales.json', (req, res) => { app.get('/locales.json', (req: any, res:any) => {
res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`);
res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.send(localeIndex); res.send(localeIndex);

View file

@ -2,7 +2,7 @@
const securityManager = require('./db/SecurityManager'); const securityManager = require('./db/SecurityManager');
// checks for padAccess // checks for padAccess
module.exports = async (req, res) => { module.exports = async (req: { params?: any; cookies?: any; session?: any; }, res: { status: (arg0: number) => { (): any; new(): any; send: { (arg0: string): void; new(): any; }; }; }) => {
const {session: {user} = {}} = req; const {session: {user} = {}} = req;
const accessObj = await securityManager.checkAccess( const accessObj = await securityManager.checkAccess(
req.params.pad, req.cookies.sessionID, req.cookies.token, user); req.params.pad, req.cookies.sessionID, req.cookies.token, user);

View file

@ -1,4 +1,7 @@
'use strict';
import {DeriveModel} from "../types/DeriveModel";
import {LegacyParams} from "../types/LegacyParams";
const {Buffer} = require('buffer'); const {Buffer} = require('buffer');
const crypto = require('./crypto'); const crypto = require('./crypto');
@ -6,22 +9,24 @@ const db = require('../db/DB');
const log4js = require('log4js'); const log4js = require('log4js');
class Kdf { class Kdf {
async generateParams() { throw new Error('not implemented'); } async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> { throw new Error('not implemented'); }
async derive(params, info) { throw new Error('not implemented'); } async derive(params: DeriveModel, info: any) { throw new Error('not implemented'); }
} }
class LegacyStaticSecret extends Kdf { class LegacyStaticSecret extends Kdf {
async derive(params, info) { return params; } async derive(params:any, info:any) { return params; }
} }
class Hkdf extends Kdf { class Hkdf extends Kdf {
constructor(digest, keyLen) { private readonly _digest: string
private readonly _keyLen: number
constructor(digest:string, keyLen:number) {
super(); super();
this._digest = digest; this._digest = digest;
this._keyLen = keyLen; this._keyLen = keyLen;
} }
async generateParams() { async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> {
const [secret, salt] = (await Promise.all([ const [secret, salt] = (await Promise.all([
crypto.randomBytes(this._keyLen), crypto.randomBytes(this._keyLen),
crypto.randomBytes(this._keyLen), crypto.randomBytes(this._keyLen),
@ -29,7 +34,7 @@ class Hkdf extends Kdf {
return {digest: this._digest, keyLen: this._keyLen, salt, secret}; return {digest: this._digest, keyLen: this._keyLen, salt, secret};
} }
async derive(p, info) { async derive(p: DeriveModel, info:any) {
return Buffer.from( return Buffer.from(
await crypto.hkdf(p.digest, p.secret, p.salt, info, p.keyLen)).toString('hex'); await crypto.hkdf(p.digest, p.secret, p.salt, info, p.keyLen)).toString('hex');
} }
@ -48,8 +53,8 @@ const algorithms = [
const defaultAlgId = algorithms.length - 1; const defaultAlgId = algorithms.length - 1;
// In JavaScript, the % operator is remainder, not modulus. // In JavaScript, the % operator is remainder, not modulus.
const mod = (a, n) => ((a % n) + n) % n; const mod = (a:number, n:number) => ((a % n) + n) % n;
const intervalStart = (t, interval) => t - mod(t, interval); const intervalStart = (t:number, interval:number) => t - mod(t, interval);
/** /**
* Maintains an array of secrets across one or more Etherpad instances sharing the same database, * Maintains an array of secrets across one or more Etherpad instances sharing the same database,
@ -58,7 +63,15 @@ const intervalStart = (t, interval) => t - mod(t, interval);
* The secrets are generated using a key derivation function (KDF) with input keying material coming * The secrets are generated using a key derivation function (KDF) with input keying material coming
* from a long-lived secret stored in the database (generated if missing). * from a long-lived secret stored in the database (generated if missing).
*/ */
class SecretRotator { export class SecretRotator {
readonly secrets: string[];
private readonly _dbPrefix
private readonly _interval
private readonly _legacyStaticSecret
private readonly _lifetime
private readonly _logger
private _updateTimeout:any
private readonly _t
/** /**
* @param {string} dbPrefix - Database key prefix to use for tracking secret metadata. * @param {string} dbPrefix - Database key prefix to use for tracking secret metadata.
* @param {number} interval - How often to rotate in a new secret. * @param {number} interval - How often to rotate in a new secret.
@ -68,7 +81,7 @@ class SecretRotator {
* rotation. If the oldest known secret starts after `lifetime` ago, this secret will cover * rotation. If the oldest known secret starts after `lifetime` ago, this secret will cover
* the time period starting `lifetime` ago and ending at the start of that secret. * the time period starting `lifetime` ago and ending at the start of that secret.
*/ */
constructor(dbPrefix, interval, lifetime, legacyStaticSecret = null) { constructor(dbPrefix: string, interval: number, lifetime: number, legacyStaticSecret:string|null = null) {
/** /**
* The secrets. The first secret in this array is the one that should be used to generate new * The secrets. The first secret in this array is the one that should be used to generate new
* MACs. All of the secrets in this array should be used when attempting to authenticate an * MACs. All of the secrets in this array should be used when attempting to authenticate an
@ -94,7 +107,7 @@ class SecretRotator {
this._t = {now: Date.now.bind(Date), setTimeout, clearTimeout, algorithms}; this._t = {now: Date.now.bind(Date), setTimeout, clearTimeout, algorithms};
} }
async _publish(params, id = null) { async _publish(params: LegacyParams, id:string|null = null) {
// Params are published to the db with a randomly generated key to avoid race conditions with // Params are published to the db with a randomly generated key to avoid race conditions with
// other instances. // other instances.
if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`; if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`;
@ -114,7 +127,7 @@ class SecretRotator {
this._updateTimeout = null; this._updateTimeout = null;
} }
async _deriveSecrets(p, now) { async _deriveSecrets(p: any, now: number) {
this._logger.debug('deriving secrets from', p); this._logger.debug('deriving secrets from', p);
if (!p.interval) return [await algorithms[p.algId].derive(p.algParams, null)]; if (!p.interval) return [await algorithms[p.algId].derive(p.algParams, null)];
const t0 = intervalStart(now, p.interval); const t0 = intervalStart(now, p.interval);
@ -139,7 +152,7 @@ class SecretRotator {
// Whether the derived secret for the interval starting at tN is still relevant. If there was no // Whether the derived secret for the interval starting at tN is still relevant. If there was no
// clock skew, a derived secret is relevant until p.lifetime has elapsed since the end of the // clock skew, a derived secret is relevant until p.lifetime has elapsed since the end of the
// interval. To accommodate clock skew, this end time is extended by p.interval. // interval. To accommodate clock skew, this end time is extended by p.interval.
const expired = (tN) => now >= tN + (2 * p.interval) + p.lifetime; const expired = (tN:number) => now >= tN + (2 * p.interval) + p.lifetime;
// Walk from t0 back until either the start of coverage or the derived secret is expired. t0 // Walk from t0 back until either the start of coverage or the derived secret is expired. t0
// must always be the first entry in case p is the current params. (The first derived secret is // must always be the first entry in case p is the current params. (The first derived secret is
// used for generating MACs, so the secret derived for t0 must be before the secrets derived for // used for generating MACs, so the secret derived for t0 must be before the secrets derived for
@ -160,12 +173,12 @@ class SecretRotator {
// TODO: This is racy. If two instances start up at the same time and there are no existing // TODO: This is racy. If two instances start up at the same time and there are no existing
// matching publications, each will generate and publish their own paramters. In practice this // matching publications, each will generate and publish their own paramters. In practice this
// is unlikely to happen, and if it does it can be fixed by restarting both Etherpad instances. // is unlikely to happen, and if it does it can be fixed by restarting both Etherpad instances.
const dbKeys = await db.findKeys(`${this._dbPrefix}:*`, null) || []; const dbKeys:string[] = await db.findKeys(`${this._dbPrefix}:*`, null) || [];
let currentParams = null; let currentParams:any = null;
let currentId = null; let currentId = null;
const dbWrites = []; const dbWrites:any[] = [];
const allParams = []; const allParams = [];
const legacyParams = []; const legacyParams:LegacyParams[] = [];
await Promise.all(dbKeys.map(async (dbKey) => { await Promise.all(dbKeys.map(async (dbKey) => {
const p = await db.get(dbKey); const p = await db.get(dbKey);
if (p.algId === 0 && p.algParams === this._legacyStaticSecret) legacyParams.push(p); if (p.algId === 0 && p.algParams === this._legacyStaticSecret) legacyParams.push(p);
@ -198,7 +211,7 @@ class SecretRotator {
!legacyParams.find((p) => p.end + p.lifetime >= legacyEnd + this._lifetime)) { !legacyParams.find((p) => p.end + p.lifetime >= legacyEnd + this._lifetime)) {
const d = new Date(legacyEnd).toJSON(); const d = new Date(legacyEnd).toJSON();
this._logger.debug(`adding legacy static secret for ${d} with lifetime ${this._lifetime}`); this._logger.debug(`adding legacy static secret for ${d} with lifetime ${this._lifetime}`);
const p = { const p: LegacyParams = {
algId: 0, algId: 0,
algParams: this._legacyStaticSecret, algParams: this._legacyStaticSecret,
// The start time is equal to the end time so that this legacy secret does not affect the // The start time is equal to the end time so that this legacy secret does not affect the
@ -248,4 +261,4 @@ class SecretRotator {
} }
} }
module.exports = SecretRotator; export default SecretRotator

View file

@ -24,11 +24,15 @@
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js'); import {PluginType} from "./types/Plugin";
import {ErrorCaused} from "./types/ErrorCaused";
import {PromiseHooks} from "node:v8";
import log4js from 'log4js';
const settings = require('./utils/Settings'); const settings = require('./utils/Settings');
let wtfnode; let wtfnode: any;
if (settings.dumpOnUncleanExit) { if (settings.dumpOnUncleanExit) {
// wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and // wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and
// it should be above everything else so that it can hook in before resources are used. // it should be above everything else so that it can hook in before resources are used.
@ -51,7 +55,7 @@ const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins'); const plugins = require('../static/js/pluginfw/plugins');
const installer = require('../static/js/pluginfw/installer'); const installer = require('../static/js/pluginfw/installer');
const {Gate} = require('./utils/promises'); const {Gate} = require('./utils/promises');
const stats = require('./stats'); const stats = require('./stats')
const logger = log4js.getLogger('server'); const logger = log4js.getLogger('server');
@ -68,14 +72,15 @@ const State = {
let state = State.INITIAL; let state = State.INITIAL;
const removeSignalListener = (signal, listener) => { const removeSignalListener = (signal: NodeJS.Signals, listener: NodeJS.SignalsListener) => {
logger.debug(`Removing ${signal} listener because it might interfere with shutdown tasks. ` + logger.debug(`Removing ${signal} listener because it might interfere with shutdown tasks. ` +
`Function code:\n${listener.toString()}\n` + `Function code:\n${listener.toString()}\n` +
`Current stack:\n${(new Error()).stack.split('\n').slice(1).join('\n')}`); `Current stack:\n${new Error()!.stack!.split('\n').slice(1).join('\n')}`);
process.off(signal, listener); process.off(signal, listener);
}; };
let startDoneGate;
let startDoneGate: { resolve: () => void; }
exports.start = async () => { exports.start = async () => {
switch (state) { switch (state) {
case State.INITIAL: case State.INITIAL:
@ -102,15 +107,17 @@ exports.start = async () => {
// Check if Etherpad version is up-to-date // Check if Etherpad version is up-to-date
UpdateCheck.check(); UpdateCheck.check();
// @ts-ignore
stats.gauge('memoryUsage', () => process.memoryUsage().rss); stats.gauge('memoryUsage', () => process.memoryUsage().rss);
// @ts-ignore
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err: ErrorCaused) => {
logger.debug(`uncaught exception: ${err.stack || err}`); logger.debug(`uncaught exception: ${err.stack || err}`);
// eslint-disable-next-line promise/no-promise-in-callback // eslint-disable-next-line promise/no-promise-in-callback
exports.exit(err) exports.exit(err)
.catch((err) => { .catch((err: ErrorCaused) => {
logger.error('Error in process exit', err); logger.error('Error in process exit', err);
// eslint-disable-next-line n/no-process-exit // eslint-disable-next-line n/no-process-exit
process.exit(1); process.exit(1);
@ -118,12 +125,12 @@ exports.start = async () => {
}); });
// As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an // As of v14, Node.js does not exit when there is an unhandled Promise rejection. Convert an
// unhandled rejection into an uncaught exception, which does cause Node.js to exit. // unhandled rejection into an uncaught exception, which does cause Node.js to exit.
process.on('unhandledRejection', (err) => { process.on('unhandledRejection', (err: ErrorCaused) => {
logger.debug(`unhandled rejection: ${err.stack || err}`); logger.debug(`unhandled rejection: ${err.stack || err}`);
throw err; throw err;
}); });
for (const signal of ['SIGINT', 'SIGTERM']) { for (const signal of ['SIGINT', 'SIGTERM'] as NodeJS.Signals[]) {
// Forcibly remove other signal listeners to prevent them from terminating node before we are // Forcibly remove other signal listeners to prevent them from terminating node before we are
// done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a // done cleaning up. See https://github.com/andywer/threads.js/pull/329 for an example of a
// problematic listener. This means that exports.exit is solely responsible for performing all // problematic listener. This means that exports.exit is solely responsible for performing all
@ -142,7 +149,7 @@ exports.start = async () => {
await db.init(); await db.init();
await installer.checkForMigration(); await installer.checkForMigration();
await plugins.update(); await plugins.update();
const installedPlugins = Object.values(pluginDefs.plugins) const installedPlugins = (Object.values(pluginDefs.plugins) as PluginType[])
.filter((plugin) => plugin.package.name !== 'ep_etherpad-lite') .filter((plugin) => plugin.package.name !== 'ep_etherpad-lite')
.map((plugin) => `${plugin.package.name}@${plugin.package.version}`) .map((plugin) => `${plugin.package.name}@${plugin.package.version}`)
.join(', '); .join(', ');
@ -190,7 +197,7 @@ exports.stop = async () => {
logger.info('Stopping Etherpad...'); logger.info('Stopping Etherpad...');
state = State.STOPPING; state = State.STOPPING;
try { try {
let timeout = null; let timeout: NodeJS.Timeout = null as unknown as NodeJS.Timeout;
await Promise.race([ await Promise.race([
hooks.aCallAll('shutdown'), hooks.aCallAll('shutdown'),
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@ -209,15 +216,15 @@ exports.stop = async () => {
stopDoneGate.resolve(); stopDoneGate.resolve();
}; };
let exitGate; let exitGate: any;
let exitCalled = false; let exitCalled = false;
exports.exit = async (err = null) => { exports.exit = async (err: ErrorCaused|string|null = null) => {
/* eslint-disable no-process-exit */ /* eslint-disable no-process-exit */
if (err === 'SIGTERM') { if (err === 'SIGTERM') {
// Termination from SIGTERM is not treated as an abnormal termination. // Termination from SIGTERM is not treated as an abnormal termination.
logger.info('Received SIGTERM signal'); logger.info('Received SIGTERM signal');
err = null; err = null;
} else if (err != null) { } else if (typeof err == "object" && err != null) {
logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`); logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`);
logger.error(err.stack || err.toString()); logger.error(err.stack || err.toString());
process.exitCode = 1; process.exitCode = 1;
@ -277,4 +284,6 @@ exports.exit = async (err = null) => {
}; };
if (require.main === module) exports.start(); if (require.main === module) exports.start();
// @ts-ignore
if (typeof(PhusionPassenger) !== 'undefined') exports.start(); if (typeof(PhusionPassenger) !== 'undefined') exports.start();

View file

@ -4,6 +4,7 @@ const measured = require('measured-core');
module.exports = measured.createCollection(); module.exports = measured.createCollection();
// @ts-ignore
module.exports.shutdown = async (hookName, context) => { module.exports.shutdown = async (hookName, context) => {
module.exports.end(); module.exports.end();
}; };

View file

@ -0,0 +1,5 @@
export type ArgsExpressType = {
app:any,
io: any,
server:any
}

View file

@ -0,0 +1,5 @@
export type AsyncQueueTask = {
srcFile: string,
destFile: string,
type: string
}

View file

@ -0,0 +1,6 @@
export type DeriveModel = {
digest: string,
secret: string,
salt: string,
keyLen: number
}

View file

@ -0,0 +1,14 @@
export class ErrorCaused extends Error {
cause: Error;
code: any;
constructor(message: string, cause: Error) {
super();
this.cause = cause
this.name = "ErrorCaused"
}
}
type ErrorCause = {
}

View file

@ -0,0 +1,5 @@
export type I18nPluginDefs = {
package: {
path: string
}
}

View file

@ -0,0 +1,8 @@
export type LegacyParams = {
start: number,
end: number,
lifetime: number,
algId: number,
algParams: any,
interval:number|null
}

View file

@ -0,0 +1,7 @@
export type MapType = {
[key: string|number]: string|number
}
export type MapArrayType<T> = {
[key:string]: T
}

16
src/node/types/PadType.ts Normal file
View file

@ -0,0 +1,16 @@
export type PadType = {
apool: ()=>APool,
atext: AText,
getInternalRevisionAText: (text:string)=>Promise<AText>
}
type APool = {
putAttrib: ([],flag: boolean)=>number
}
export type AText = {
text: string,
attribs: any
}

9
src/node/types/Plugin.ts Normal file
View file

@ -0,0 +1,9 @@
'use strict';
export type PluginType = {
package: {
name: string,
version: string
}
}

View file

@ -0,0 +1,8 @@
import type {Readable} from "node:stream";
import type {ChildProcess} from "node:child_process";
export type PromiseWithStd = {
stdout?: Readable|null,
stderr?: Readable|null,
child?: ChildProcess
} & Promise<any>

View file

@ -0,0 +1,3 @@
export type QueryType = {
searchTerm: string; sortBy: string; sortDir: string; offset: number; limit: number;
}

View file

@ -0,0 +1,15 @@
export type RunCMDOptions = {
cwd?: string,
stdio?: string[],
env?: NodeJS.ProcessEnv
}
export type RunCMDPromise = {
stdout?:Function,
stderr?:Function
}
export type ErrorExtended = {
code?: number|null,
signal?: NodeJS.Signals|null
}

View file

@ -0,0 +1,3 @@
export type SecretRotatorType = {
stop: ()=>void
}

View file

@ -0,0 +1,34 @@
export type SwaggerUIResource = {
[key: string]: {
[secondKey: string]: {
operationId: string,
summary?: string,
description?:string
responseSchema?: object
}
}
}
export type OpenAPISuccessResponse = {
[key: number] :{
$ref: string,
content?: {
[key: string]: {
schema: {
properties: {
data: {
type: string,
properties: object
}
}
}
}
}
}
}
export type OpenAPIOperations = {
[key:string]: any
}

View file

@ -19,6 +19,9 @@
* limitations under the License. * limitations under the License.
*/ */
import {ChildProcess} from "node:child_process";
import {AsyncQueueTask} from "../types/AsyncQueueTask";
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
const async = require('async'); const async = require('async');
const settings = require('./Settings'); const settings = require('./Settings');
@ -27,13 +30,13 @@ const os = require('os');
// on windows we have to spawn a process for each convertion, // on windows we have to spawn a process for each convertion,
// cause the plugin abicommand doesn't exist on this platform // cause the plugin abicommand doesn't exist on this platform
if (os.type().indexOf('Windows') > -1) { if (os.type().indexOf('Windows') > -1) {
exports.convertFile = async (srcFile, destFile, type) => { exports.convertFile = async (srcFile: string, destFile: string, type: string) => {
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]); const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
let stdoutBuffer = ''; let stdoutBuffer = '';
abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); abiword.stdout.on('data', (data: string) => { stdoutBuffer += data.toString(); });
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); abiword.stderr.on('data', (data: string) => { stdoutBuffer += data.toString(); });
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => {
abiword.on('exit', (code) => { abiword.on('exit', (code: number) => {
if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`)); if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
if (stdoutBuffer !== '') { if (stdoutBuffer !== '') {
console.log(stdoutBuffer); console.log(stdoutBuffer);
@ -46,13 +49,13 @@ if (os.type().indexOf('Windows') > -1) {
// communicate with it via stdin/stdout // communicate with it via stdin/stdout
// thats much faster, about factor 10 // thats much faster, about factor 10
} else { } else {
let abiword; let abiword: ChildProcess;
let stdoutCallback = null; let stdoutCallback: Function|null = null;
const spawnAbiword = () => { const spawnAbiword = () => {
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
let stdoutBuffer = ''; let stdoutBuffer = '';
let firstPrompt = true; let firstPrompt = true;
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); abiword.stderr!.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword.on('exit', (code) => { abiword.on('exit', (code) => {
spawnAbiword(); spawnAbiword();
if (stdoutCallback != null) { if (stdoutCallback != null) {
@ -60,7 +63,7 @@ if (os.type().indexOf('Windows') > -1) {
stdoutCallback = null; stdoutCallback = null;
} }
}); });
abiword.stdout.on('data', (data) => { abiword.stdout!.on('data', (data) => {
stdoutBuffer += data.toString(); stdoutBuffer += data.toString();
// we're searching for the prompt, cause this means everything we need is in the buffer // we're searching for the prompt, cause this means everything we need is in the buffer
if (stdoutBuffer.search('AbiWord:>') !== -1) { if (stdoutBuffer.search('AbiWord:>') !== -1) {
@ -76,15 +79,15 @@ if (os.type().indexOf('Windows') > -1) {
}; };
spawnAbiword(); spawnAbiword();
const queue = async.queue((task, callback) => { const queue = async.queue((task: AsyncQueueTask, callback:Function) => {
abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`); abiword.stdin!.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
stdoutCallback = (err) => { stdoutCallback = (err: string) => {
if (err != null) console.error('Abiword File failed to convert', err); if (err != null) console.error('Abiword File failed to convert', err);
callback(err); callback(err);
}; };
}, 1); }, 1);
exports.convertFile = async (srcFile, destFile, type) => { exports.convertFile = async (srcFile: string, destFile: string, type: string) => {
await queue.pushAsync({srcFile, destFile, type}); await queue.pushAsync({srcFile, destFile, type});
}; };
} }

View file

@ -18,7 +18,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const log4js = require('log4js'); const log4js = require('log4js');
const path = require('path'); const path = require('path');
const _ = require('underscore'); const _ = require('underscore');
@ -29,7 +28,7 @@ const absPathLogger = log4js.getLogger('AbsolutePaths');
* findEtherpadRoot() computes its value only on first invocation. * findEtherpadRoot() computes its value only on first invocation.
* Subsequent invocations are served from this variable. * Subsequent invocations are served from this variable.
*/ */
let etherpadRoot = null; let etherpadRoot: string|null = null;
/** /**
* If stringArray's last elements are exactly equal to lastDesiredElements, * If stringArray's last elements are exactly equal to lastDesiredElements,
@ -41,7 +40,7 @@ let etherpadRoot = null;
* @return {string[]|boolean} The shortened array, or false if there was no * @return {string[]|boolean} The shortened array, or false if there was no
* overlap. * overlap.
*/ */
const popIfEndsWith = (stringArray, lastDesiredElements) => { const popIfEndsWith = (stringArray: string[], lastDesiredElements: string[]): string[] | false => {
if (stringArray.length <= lastDesiredElements.length) { if (stringArray.length <= lastDesiredElements.length) {
absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" ` + absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" ` +
`from "${stringArray.join(path.sep)}", it should contain at least ` + `from "${stringArray.join(path.sep)}", it should contain at least ` +
@ -131,7 +130,7 @@ exports.findEtherpadRoot = () => {
* it is returned unchanged. Otherwise it is interpreted * it is returned unchanged. Otherwise it is interpreted
* relative to exports.root. * relative to exports.root.
*/ */
exports.makeAbsolute = (somePath) => { exports.makeAbsolute = (somePath: string) => {
if (path.isAbsolute(somePath)) { if (path.isAbsolute(somePath)) {
return somePath; return somePath;
} }
@ -150,10 +149,8 @@ exports.makeAbsolute = (somePath) => {
* a subdirectory of the base one * a subdirectory of the base one
* @return {boolean} * @return {boolean}
*/ */
exports.isSubdir = (parent, arbitraryDir) => { exports.isSubdir = (parent: string, arbitraryDir: string): boolean => {
// modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825 // modified from: https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js#45242825
const relative = path.relative(parent, arbitraryDir); const relative = path.relative(parent, arbitraryDir);
const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
return isSubdir;
}; };

View file

@ -21,13 +21,13 @@ const authorManager = require('../db/AuthorManager');
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
exports.getPadRaw = async (padId, readOnlyId) => { exports.getPadRaw = async (padId:string, readOnlyId:string) => {
const dstPfx = `pad:${readOnlyId || padId}`; const dstPfx = `pad:${readOnlyId || padId}`;
const [pad, customPrefixes] = await Promise.all([ const [pad, customPrefixes] = await Promise.all([
padManager.getPad(padId), padManager.getPad(padId),
hooks.aCallAll('exportEtherpadAdditionalContent'), hooks.aCallAll('exportEtherpadAdditionalContent'),
]); ]);
const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => { const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix:string) => {
const srcPfx = `${customPrefix}:${padId}`; const srcPfx = `${customPrefix}:${padId}`;
const dstPfx = `${customPrefix}:${readOnlyId || padId}`; const dstPfx = `${customPrefix}:${readOnlyId || padId}`;
assert(!srcPfx.includes('*')); assert(!srcPfx.includes('*'));

View file

@ -26,7 +26,7 @@ const { checkValidRev } = require('./checkValidRev');
/* /*
* This method seems unused in core and no plugins depend on it * This method seems unused in core and no plugins depend on it
*/ */
exports.getPadPlainText = (pad, revNum) => { exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any; atext: any; pool: any; }, revNum: undefined) => {
const _analyzeLine = exports._analyzeLine; const _analyzeLine = exports._analyzeLine;
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext); const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
@ -47,10 +47,12 @@ exports.getPadPlainText = (pad, revNum) => {
return pieces.join(''); return pieces.join('');
}; };
type LineModel = {
[id:string]:string|number|LineModel
}
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
exports._analyzeLine = (text, aline, apool) => { const line: LineModel = {};
const line = {};
// identify list // identify list
let lineMarker = 0; let lineMarker = 0;
@ -86,4 +88,4 @@ exports._analyzeLine = (text, aline, apool) => {
exports._encodeWhitespace = exports._encodeWhitespace =
(s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); (s:string) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`);

View file

@ -19,13 +19,16 @@
* limitations under the License. * limitations under the License.
*/ */
import {AText, PadType} from "../types/PadType";
import {MapType} from "../types/MapType";
const Changeset = require('../../static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes'); const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const _analyzeLine = require('./ExportHelper')._analyzeLine; const _analyzeLine = require('./ExportHelper')._analyzeLine;
// This is slightly different than the HTML method as it passes the output to getTXTFromAText // This is slightly different than the HTML method as it passes the output to getTXTFromAText
const getPadTXT = async (pad, revNum) => { const getPadTXT = async (pad: PadType, revNum: string) => {
let atext = pad.atext; let atext = pad.atext;
if (revNum !== undefined) { if (revNum !== undefined) {
@ -39,13 +42,13 @@ const getPadTXT = async (pad, revNum) => {
// This is different than the functionality provided in ExportHtml as it provides formatting // This is different than the functionality provided in ExportHtml as it provides formatting
// functionality that is designed specifically for TXT exports // functionality that is designed specifically for TXT exports
const getTXTFromAtext = (pad, atext, authorColors) => { const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
const apool = pad.apool(); const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
const anumMap = {}; const anumMap: MapType = {};
const css = ''; const css = '';
props.forEach((propName, i) => { props.forEach((propName, i) => {
@ -55,8 +58,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
} }
}); });
const getLineTXT = (text, attribs) => { const getLineTXT = (text:string, attribs:any) => {
const propVals = [false, false, false]; const propVals:(number|boolean)[] = [false, false, false];
const ENTER = 1; const ENTER = 1;
const STAY = 2; const STAY = 2;
const LEAVE = 0; const LEAVE = 0;
@ -71,7 +74,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
let idx = 0; let idx = 0;
const processNextChars = (numChars) => { const processNextChars = (numChars: number) => {
if (numChars <= 0) { if (numChars <= 0) {
return; return;
} }
@ -84,7 +87,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
for (const a of attributes.decodeAttribString(o.attribs)) { for (const a of attributes.decodeAttribString(o.attribs)) {
if (a in anumMap) { if (a in anumMap) {
const i = anumMap[a]; // i = 0 => bold, etc. const i = anumMap[a] as number; // i = 0 => bold, etc.
if (!propVals[i]) { if (!propVals[i]) {
propVals[i] = ENTER; propVals[i] = ENTER;
@ -189,7 +192,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
// want to deal gracefully with blank lines. // want to deal gracefully with blank lines.
// => keeps track of the parents level of indentation // => keeps track of the parents level of indentation
const listNumbers = {}; const listNumbers:MapType = {};
let prevListLevel; let prevListLevel;
for (let i = 0; i < textLines.length; i++) { for (let i = 0; i < textLines.length; i++) {
@ -233,6 +236,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
delete listNumbers[prevListLevel]; delete listNumbers[prevListLevel];
} }
// @ts-ignore
listNumbers[line.listLevel]++; listNumbers[line.listLevel]++;
if (line.listLevel > 1) { if (line.listLevel > 1) {
let x = 1; let x = 1;
@ -258,7 +262,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
exports.getTXTFromAtext = getTXTFromAtext; exports.getTXTFromAtext = getTXTFromAtext;
exports.getPadTXTDocument = async (padId, revNum) => { exports.getPadTXTDocument = async (padId:string, revNum:string) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
return getPadTXT(pad, revNum); return getPadTXT(pad, revNum);
}; };

View file

@ -5,17 +5,19 @@
* objects lack. * objects lack.
*/ */
class Stream { class Stream {
private _iter
private _next: any
/** /**
* @returns {Stream} A Stream that yields values in the half-open range [start, end). * @returns {Stream} A Stream that yields values in the half-open range [start, end).
*/ */
static range(start, end) { static range(start: number, end: number) {
return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })()); return new Stream((function* () { for (let i = start; i < end; ++i) yield i; })());
} }
/** /**
* @param {Iterable<any>} values - Any iterable of values. * @param {Iterable<any>} values - Any iterable of values.
*/ */
constructor(values) { constructor(values: Iterable<any>) {
this._iter = values[Symbol.iterator](); this._iter = values[Symbol.iterator]();
this._next = null; this._next = null;
} }
@ -52,10 +54,11 @@ class Stream {
* @param {number} size - The number of values to read at a time. * @param {number} size - The number of values to read at a time.
* @returns {Stream} A new Stream that gets its values from this Stream. * @returns {Stream} A new Stream that gets its values from this Stream.
*/ */
batch(size) { batch(size: number) {
return new Stream((function* () { return new Stream((function* () {
const b = []; const b = [];
try { try {
// @ts-ignore
for (const v of this) { for (const v of this) {
Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors. Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors.
b.push(v); b.push(v);
@ -100,10 +103,11 @@ class Stream {
* @param {number} capacity - The number of values to keep buffered. * @param {number} capacity - The number of values to keep buffered.
* @returns {Stream} A new Stream that gets its values from this Stream. * @returns {Stream} A new Stream that gets its values from this Stream.
*/ */
buffer(capacity) { buffer(capacity: number) {
return new Stream((function* () { return new Stream((function* () {
const b = []; const b = [];
try { try {
// @ts-ignore
for (const v of this) { for (const v of this) {
Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors. Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors.
// Note: V8 has good Array push+shift optimization. // Note: V8 has good Array push+shift optimization.
@ -123,7 +127,8 @@ class Stream {
* @param {(v: any) => any} fn - Value transformation function. * @param {(v: any) => any} fn - Value transformation function.
* @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`. * @returns {Stream} A new Stream that yields this Stream's values, transformed by `fn`.
*/ */
map(fn) { return new Stream((function* () { for (const v of this) yield fn(v); }).call(this)); } map(fn:Function) { return new Stream((function* () { // @ts-ignore
for (const v of this) yield fn(v); }).call(this)); }
/** /**
* Implements the JavaScript iterable protocol. * Implements the JavaScript iterable protocol.

View file

@ -6,9 +6,14 @@ const headers = {
'User-Agent': 'Etherpad/' + settings.getEpVersion(), 'User-Agent': 'Etherpad/' + settings.getEpVersion(),
} }
type Infos = {
latestVersion: string
}
const updateInterval = 60 * 60 * 1000; // 1 hour const updateInterval = 60 * 60 * 1000; // 1 hour
let infos; let infos: Infos;
let lastLoadingTime = null; let lastLoadingTime: number | null = null;
const loadEtherpadInformations = () => { const loadEtherpadInformations = () => {
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) { if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
@ -16,7 +21,7 @@ const loadEtherpadInformations = () => {
} }
return axios.get('https://static.etherpad.org/info.json', {headers: headers}) return axios.get('https://static.etherpad.org/info.json', {headers: headers})
.then(async resp => { .then(async (resp: any) => {
infos = await resp.data; infos = await resp.data;
if (infos === undefined || infos === null) { if (infos === undefined || infos === null) {
await Promise.reject("Could not retrieve current version") await Promise.reject("Could not retrieve current version")
@ -26,7 +31,7 @@ const loadEtherpadInformations = () => {
lastLoadingTime = Date.now(); lastLoadingTime = Date.now();
return await Promise.resolve(infos); return await Promise.resolve(infos);
}) })
.catch(async err => { .catch(async (err: Error) => {
return await Promise.reject(err); return await Promise.reject(err);
}); });
} }
@ -37,20 +42,20 @@ exports.getLatestVersion = () => {
return infos?.latestVersion; return infos?.latestVersion;
}; };
exports.needsUpdate = async (cb) => { exports.needsUpdate = async (cb: Function) => {
await loadEtherpadInformations() await loadEtherpadInformations()
.then((info) => { .then((info:Infos) => {
if (semver.gt(info.latestVersion, settings.getEpVersion())) { if (semver.gt(info.latestVersion, settings.getEpVersion())) {
if (cb) return cb(true); if (cb) return cb(true);
} }
}).catch((err) => { }).catch((err: Error) => {
console.error(`Can not perform Etherpad update check: ${err}`); console.error(`Can not perform Etherpad update check: ${err}`);
if (cb) return cb(false); if (cb) return cb(false);
}); });
}; };
exports.check = () => { exports.check = () => {
exports.needsUpdate((needsUpdate) => { exports.needsUpdate((needsUpdate: boolean) => {
if (needsUpdate) { if (needsUpdate) {
console.warn(`Update available: Download the actual version ${infos.latestVersion}`); console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
} }

View file

@ -36,32 +36,38 @@ const util = require('util');
* *
*/ */
// MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - START
let _crypto;
try { const _crypto = require('crypto');
_crypto = require('crypto');
} catch {
_crypto = undefined;
}
let CACHE_DIR = path.join(settings.root, 'var/'); let CACHE_DIR = path.join(settings.root, 'var/');
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
const responseCache = {}; type Headers = {
[id: string]: string
}
const djb2Hash = (data) => { type ResponseCache = {
[id: string]: {
statusCode: number
headers: Headers
}
}
const responseCache: ResponseCache = {};
const djb2Hash = (data: string) => {
const chars = data.split('').map((str) => str.charCodeAt(0)); const chars = data.split('').map((str) => str.charCodeAt(0));
return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`; return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`;
}; };
const generateCacheKeyWithSha256 = const generateCacheKeyWithSha256 =
(path) => _crypto.createHash('sha256').update(path).digest('hex'); (path: string) => _crypto.createHash('sha256').update(path).digest('hex');
const generateCacheKeyWithDjb2 = const generateCacheKeyWithDjb2 =
(path) => Buffer.from(djb2Hash(path)).toString('hex'); (path: string) => Buffer.from(djb2Hash(path)).toString('hex');
let generateCacheKey; let generateCacheKey: (path: string)=>string;
if (_crypto) { if (_crypto) {
generateCacheKey = generateCacheKeyWithSha256; generateCacheKey = generateCacheKeyWithSha256;
@ -79,17 +85,17 @@ if (_crypto) {
*/ */
module.exports = class CachingMiddleware { module.exports = class CachingMiddleware {
handle(req, res, next) { handle(req: any, res: any, next: any) {
this._handle(req, res, next).catch((err) => next(err || new Error(err))); this._handle(req, res, next).catch((err) => next(err || new Error(err)));
} }
async _handle(req, res, next) { async _handle(req: any, res: any, next: any) {
if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) { if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) {
return next(undefined, req, res); return next(undefined, req, res);
} }
const oldReq = {}; const oldReq:ResponseCache = {};
const oldRes = {}; const oldRes:ResponseCache = {};
const supportsGzip = const supportsGzip =
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1; (req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
@ -119,7 +125,7 @@ module.exports = class CachingMiddleware {
res.write = oldRes.write || res.write; res.write = oldRes.write || res.write;
res.end = oldRes.end || res.end; res.end = oldRes.end || res.end;
const headers = {}; const headers: Headers = {};
Object.assign(headers, (responseCache[cacheKey].headers || {})); Object.assign(headers, (responseCache[cacheKey].headers || {}));
const statusCode = responseCache[cacheKey].statusCode; const statusCode = responseCache[cacheKey].statusCode;
@ -150,18 +156,19 @@ module.exports = class CachingMiddleware {
return respond(); return respond();
} }
const _headers = {}; const _headers:Headers = {};
oldRes.setHeader = res.setHeader; oldRes.setHeader = res.setHeader;
res.setHeader = (key, value) => { res.setHeader = (key: string, value: string) => {
// Don't set cookies, see issue #707 // Don't set cookies, see issue #707
if (key.toLowerCase() === 'set-cookie') return; if (key.toLowerCase() === 'set-cookie') return;
_headers[key.toLowerCase()] = value; _headers[key.toLowerCase()] = value;
// @ts-ignore
oldRes.setHeader.call(res, key, value); oldRes.setHeader.call(res, key, value);
}; };
oldRes.writeHead = res.writeHead; oldRes.writeHead = res.writeHead;
res.writeHead = (status, headers) => { res.writeHead = (status: number, headers: Headers) => {
res.writeHead = oldRes.writeHead; res.writeHead = oldRes.writeHead;
if (status === 200) { if (status === 200) {
// Update cache // Update cache
@ -174,14 +181,14 @@ module.exports = class CachingMiddleware {
oldRes.write = res.write; oldRes.write = res.write;
oldRes.end = res.end; oldRes.end = res.end;
res.write = (data, encoding) => { res.write = (data: number, encoding: number) => {
buffer += data.toString(encoding); buffer += data.toString(encoding);
}; };
res.end = async (data, encoding) => { res.end = async (data: number, encoding: number) => {
await Promise.all([ await Promise.all([
fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}), fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}),
util.promisify(zlib.gzip)(buffer) util.promisify(zlib.gzip)(buffer)
.then((content) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content)) .then((content: string) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content))
.catch(() => {}), .catch(() => {}),
]); ]);
responseCache[cacheKey] = {statusCode: status, headers}; responseCache[cacheKey] = {statusCode: status, headers};
@ -191,8 +198,8 @@ module.exports = class CachingMiddleware {
// Nothing new changed from the cached version. // Nothing new changed from the cached version.
oldRes.write = res.write; oldRes.write = res.write;
oldRes.end = res.end; oldRes.end = res.end;
res.write = (data, encoding) => {}; res.write = (data: number, encoding: number) => {};
res.end = (data, encoding) => { respond(); }; res.end = (data: number, encoding: number) => { respond(); };
} else { } else {
res.writeHead(status, headers); res.writeHead(status, headers);
} }

View file

@ -4,7 +4,7 @@ const CustomError = require('../utils/customError');
// checks if a rev is a legal number // checks if a rev is a legal number
// pre-condition is that `rev` is not undefined // pre-condition is that `rev` is not undefined
const checkValidRev = (rev) => { const checkValidRev = (rev: number|string) => {
if (typeof rev !== 'number') { if (typeof rev !== 'number') {
rev = parseInt(rev, 10); rev = parseInt(rev, 10);
} }
@ -28,7 +28,7 @@ const checkValidRev = (rev) => {
}; };
// checks if a number is an int // checks if a number is an int
const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value); const isInt = (value:number) => (parseFloat(String(value)) === parseInt(String(value), 10)) && !isNaN(value);
exports.isInt = isInt; exports.isInt = isInt;
exports.checkValidRev = checkValidRev; exports.checkValidRev = checkValidRev;

View file

@ -10,11 +10,11 @@
class CustomError extends Error { class CustomError extends Error {
/** /**
* Creates an instance of CustomError. * Creates an instance of CustomError.
* @param {*} message * @param {string} message
* @param {string} [name='Error'] a custom name for the error object * @param {string} [name='Error'] a custom name for the error object
* @memberof CustomError * @memberof CustomError
*/ */
constructor(message, name = 'Error') { constructor(message:string, name: string = 'Error') {
super(message); super(message);
this.name = name; this.name = name;
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);

View file

@ -1,8 +1,8 @@
'use strict'; 'use strict';
const fs = require('fs'); const fs = require('fs');
const check = (path) => { const check = (path:string) => {
const existsSync = fs.statSync || fs.existsSync || path.existsSync; const existsSync = fs.statSync || fs.existsSync;
let result; let result;
try { try {

View file

@ -7,14 +7,16 @@
// `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if // `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as // `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
// the predicate. // the predicate.
exports.firstSatisfies = (promises, predicate) => { exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
if (predicate == null) predicate = (x) => x; if (predicate == null) {
predicate = (x: any) => x;
}
// Transform each original Promise into a Promise that never resolves if the original resolved // Transform each original Promise into a Promise that never resolves if the original resolved
// value does not satisfy `predicate`. These transformed Promises will be passed to Promise.race, // value does not satisfy `predicate`. These transformed Promises will be passed to Promise.race,
// yielding the first resolved value that satisfies `predicate`. // yielding the first resolved value that satisfies `predicate`.
const newPromises = promises.map( const newPromises = promises.map((p) =>
(p) => new Promise((resolve, reject) => p.then((v) => predicate(v) && resolve(v), reject))); new Promise((resolve, reject) => p.then((v) => predicate!(v) && resolve(v), reject)));
// If `promises` is an empty array or if none of them resolve to a value that satisfies // If `promises` is an empty array or if none of them resolve to a value that satisfies
// `predicate`, then `Promise.race(newPromises)` will never resolve. To handle that, add another // `predicate`, then `Promise.race(newPromises)` will never resolve. To handle that, add another
@ -42,7 +44,7 @@ exports.firstSatisfies = (promises, predicate) => {
// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away, // `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async // and each remaining Promise will be created once one of the earlier Promises resolves.) This async
// function resolves once all `total` Promises have resolved. // function resolves once all `total` Promises have resolved.
exports.timesLimit = async (total, concurrency, promiseCreator) => { exports.timesLimit = async (total: number, concurrency: number, promiseCreator: Function) => {
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive'); if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
let next = 0; let next = 0;
const addAnother = () => promiseCreator(next++).finally(() => { const addAnother = () => promiseCreator(next++).finally(() => {
@ -59,7 +61,7 @@ exports.timesLimit = async (total, concurrency, promiseCreator) => {
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as * An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
* properties. * properties.
*/ */
class Gate extends Promise { class Gate<T> extends Promise<T> {
// Coax `.then()` into returning an ordinary Promise, not a Gate. See // Coax `.then()` into returning an ordinary Promise, not a Gate. See
// https://stackoverflow.com/a/65669070 for the rationale. // https://stackoverflow.com/a/65669070 for the rationale.
static get [Symbol.species]() { return Promise; } static get [Symbol.species]() { return Promise; }
@ -68,7 +70,7 @@ class Gate extends Promise {
// `this` is assigned when `super()` returns, not when it is called, so it is not acceptable to // `this` is assigned when `super()` returns, not when it is called, so it is not acceptable to
// do the following because it will throw a ReferenceError when it dereferences `this`: // do the following because it will throw a ReferenceError when it dereferences `this`:
// super((resolve, reject) => Object.assign(this, {resolve, reject})); // super((resolve, reject) => Object.assign(this, {resolve, reject}));
let props; let props: any;
super((resolve, reject) => props = {resolve, reject}); super((resolve, reject) => props = {resolve, reject});
Object.assign(this, props); Object.assign(this, props);
} }

View file

@ -3,8 +3,8 @@
* Generates a random String with the given length. Is needed to generate the * Generates a random String with the given length. Is needed to generate the
* Author, Group, readonly, session Ids * Author, Group, readonly, session Ids
*/ */
const crypto = require('crypto'); const cryptoMod = require('crypto');
const randomString = (len) => crypto.randomBytes(len).toString('hex'); const randomString = (len: number) => cryptoMod.randomBytes(len).toString('hex');
module.exports = randomString; module.exports = randomString;

View file

@ -1,5 +1,10 @@
'use strict'; 'use strict';
import {ErrorExtended, RunCMDOptions, RunCMDPromise} from "../types/RunCMDOptions";
import {ChildProcess} from "node:child_process";
import {PromiseWithStd} from "../types/PromiseWithStd";
import {Readable} from "node:stream";
const spawn = require('cross-spawn'); const spawn = require('cross-spawn');
const log4js = require('log4js'); const log4js = require('log4js');
const path = require('path'); const path = require('path');
@ -7,12 +12,12 @@ const settings = require('./Settings');
const logger = log4js.getLogger('runCmd'); const logger = log4js.getLogger('runCmd');
const logLines = (readable, logLineFn) => { const logLines = (readable: undefined | Readable | null, logLineFn: (arg0: (string | undefined)) => void) => {
readable.setEncoding('utf8'); readable!.setEncoding('utf8');
// The process won't necessarily write full lines every time -- it might write a part of a line // The process won't necessarily write full lines every time -- it might write a part of a line
// then write the rest of the line later. // then write the rest of the line later.
let leftovers = ''; let leftovers: string| undefined = '';
readable.on('data', (chunk) => { readable!.on('data', (chunk) => {
const lines = chunk.split('\n'); const lines = chunk.split('\n');
if (lines.length === 0) return; if (lines.length === 0) return;
lines[0] = leftovers + lines[0]; lines[0] = leftovers + lines[0];
@ -21,7 +26,7 @@ const logLines = (readable, logLineFn) => {
logLineFn(line); logLineFn(line);
} }
}); });
readable.on('end', () => { readable!.on('end', () => {
if (leftovers !== '') logLineFn(leftovers); if (leftovers !== '') logLineFn(leftovers);
leftovers = ''; leftovers = '';
}); });
@ -69,7 +74,7 @@ const logLines = (readable, logLineFn) => {
* - `stderr`: Similar to `stdout` but for stderr. * - `stderr`: Similar to `stdout` but for stderr.
* - `child`: The ChildProcess object. * - `child`: The ChildProcess object.
*/ */
module.exports = exports = (args, opts = {}) => { module.exports = exports = (args: string[], opts:RunCMDOptions = {}) => {
logger.debug(`Executing command: ${args.join(' ')}`); logger.debug(`Executing command: ${args.join(' ')}`);
opts = {cwd: settings.root, ...opts}; opts = {cwd: settings.root, ...opts};
@ -82,8 +87,8 @@ module.exports = exports = (args, opts = {}) => {
: opts.stdio === 'string' ? [null, 'string', 'string'] : opts.stdio === 'string' ? [null, 'string', 'string']
: Array(3).fill(opts.stdio); : Array(3).fill(opts.stdio);
const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`); const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`);
if (stdio[1] == null) stdio[1] = (line) => cmdLogger.info(line); if (stdio[1] == null) stdio[1] = (line: string) => cmdLogger.info(line);
if (stdio[2] == null) stdio[2] = (line) => cmdLogger.error(line); if (stdio[2] == null) stdio[2] = (line: string) => cmdLogger.error(line);
const stdioLoggers = []; const stdioLoggers = [];
const stdioSaveString = []; const stdioSaveString = [];
for (const fd of [1, 2]) { for (const fd of [1, 2]) {
@ -116,13 +121,13 @@ module.exports = exports = (args, opts = {}) => {
// Create an error object to use in case the process fails. This is done here rather than in the // Create an error object to use in case the process fails. This is done here rather than in the
// process's `exit` handler so that we get a useful stack trace. // process's `exit` handler so that we get a useful stack trace.
const procFailedErr = new Error(); const procFailedErr: Error & ErrorExtended = new Error();
const proc = spawn(args[0], args.slice(1), opts); const proc: ChildProcess = spawn(args[0], args.slice(1), opts);
const streams = [undefined, proc.stdout, proc.stderr]; const streams:[undefined, Readable|null, Readable|null] = [undefined, proc.stdout, proc.stderr];
let px; let px: { reject: any; resolve: any; };
const p = new Promise((resolve, reject) => { px = {resolve, reject}; }); const p:PromiseWithStd = new Promise<string>((resolve, reject) => { px = {resolve, reject}; });
[, p.stdout, p.stderr] = streams; [, p.stdout, p.stderr] = streams;
p.child = proc; p.child = proc;
@ -132,9 +137,10 @@ module.exports = exports = (args, opts = {}) => {
if (stdioLoggers[fd] != null) { if (stdioLoggers[fd] != null) {
logLines(streams[fd], stdioLoggers[fd]); logLines(streams[fd], stdioLoggers[fd]);
} else if (stdioSaveString[fd]) { } else if (stdioSaveString[fd]) {
// @ts-ignore
p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => { p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => {
const chunks = []; const chunks = [];
for await (const chunk of streams[fd]) chunks.push(chunk); for await (const chunk of streams[fd]!) chunks.push(chunk);
return Buffer.concat(chunks).toString().replace(/\n+$/g, ''); return Buffer.concat(chunks).toString().replace(/\n+$/g, '');
})(); })();
} }

View file

@ -4,7 +4,7 @@ const path = require('path');
// Normalizes p and ensures that it is a relative path that does not reach outside. See // Normalizes p and ensures that it is a relative path that does not reach outside. See
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context. // https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
module.exports = (p, pathApi = path) => { module.exports = (p: string, pathApi = path) => {
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word // The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might // "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,

View file

@ -1,270 +0,0 @@
'use strict';
/**
* The Toolbar Module creates and renders the toolbars and buttons
*/
const _ = require('underscore');
const removeItem = (array, what) => {
let ax;
while ((ax = array.indexOf(what)) !== -1) {
array.splice(ax, 1);
}
return array;
};
const defaultButtonAttributes = (name, overrides) => ({
command: name,
localizationId: `pad.toolbar.${name}.title`,
class: `buttonicon buttonicon-${name}`,
});
const tag = (name, attributes, contents) => {
const aStr = tagAttributes(attributes);
if (_.isString(contents) && contents.length > 0) {
return `<${name}${aStr}>${contents}</${name}>`;
} else {
return `<${name}${aStr}></${name}>`;
}
};
const tagAttributes = (attributes) => {
attributes = _.reduce(attributes || {}, (o, val, name) => {
if (!_.isUndefined(val)) {
o[name] = val;
}
return o;
}, {});
return ` ${_.map(attributes, (val, name) => `${name}="${_.escape(val)}"`).join(' ')}`;
};
const ButtonsGroup = function () {
this.buttons = [];
};
ButtonsGroup.fromArray = function (array) {
const btnGroup = new this();
_.each(array, (btnName) => {
btnGroup.addButton(Button.load(btnName));
});
return btnGroup;
};
ButtonsGroup.prototype.addButton = function (button) {
this.buttons.push(button);
return this;
};
ButtonsGroup.prototype.render = function () {
if (this.buttons && this.buttons.length === 1) {
this.buttons[0].grouping = '';
} else if (this.buttons && this.buttons.length > 1) {
_.first(this.buttons).grouping = 'grouped-left';
_.last(this.buttons).grouping = 'grouped-right';
_.each(this.buttons.slice(1, -1), (btn) => {
btn.grouping = 'grouped-middle';
});
}
return _.map(this.buttons, (btn) => {
if (btn) return btn.render();
}).join('\n');
};
const Button = function (attributes) {
this.attributes = attributes;
};
Button.load = (btnName) => {
const button = module.exports.availableButtons[btnName];
try {
if (button.constructor === Button || button.constructor === SelectButton) {
return button;
} else {
return new Button(button);
}
} catch (e) {
console.warn('Error loading button', btnName);
return false;
}
};
_.extend(Button.prototype, {
grouping: '',
render() {
const liAttributes = {
'data-type': 'button',
'data-key': this.attributes.command,
};
return tag('li', liAttributes,
tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId},
tag('button', {
'class': ` ${this.attributes.class}`,
'data-l10n-id': this.attributes.localizationId,
})));
},
});
const SelectButton = function (attributes) {
this.attributes = attributes;
this.options = [];
};
_.extend(SelectButton.prototype, Button.prototype, {
addOption(value, text, attributes) {
this.options.push({
value,
text,
attributes,
});
return this;
},
select(attributes) {
const options = [];
_.each(this.options, (opt) => {
const a = _.extend({
value: opt.value,
}, opt.attributes);
options.push(tag('option', a, opt.text));
});
return tag('select', attributes, options.join(''));
},
render() {
const attributes = {
'id': this.attributes.id,
'data-key': this.attributes.command,
'data-type': 'select',
};
return tag('li', attributes, this.select({id: this.attributes.selectId}));
},
});
const Separator = function () {};
Separator.prototype.render = function () {
return tag('li', {class: 'separator'});
};
module.exports = {
availableButtons: {
bold: defaultButtonAttributes('bold'),
italic: defaultButtonAttributes('italic'),
underline: defaultButtonAttributes('underline'),
strikethrough: defaultButtonAttributes('strikethrough'),
orderedlist: {
command: 'insertorderedlist',
localizationId: 'pad.toolbar.ol.title',
class: 'buttonicon buttonicon-insertorderedlist',
},
unorderedlist: {
command: 'insertunorderedlist',
localizationId: 'pad.toolbar.ul.title',
class: 'buttonicon buttonicon-insertunorderedlist',
},
indent: defaultButtonAttributes('indent'),
outdent: {
command: 'outdent',
localizationId: 'pad.toolbar.unindent.title',
class: 'buttonicon buttonicon-outdent',
},
undo: defaultButtonAttributes('undo'),
redo: defaultButtonAttributes('redo'),
clearauthorship: {
command: 'clearauthorship',
localizationId: 'pad.toolbar.clearAuthorship.title',
class: 'buttonicon buttonicon-clearauthorship',
},
importexport: {
command: 'import_export',
localizationId: 'pad.toolbar.import_export.title',
class: 'buttonicon buttonicon-import_export',
},
timeslider: {
command: 'showTimeSlider',
localizationId: 'pad.toolbar.timeslider.title',
class: 'buttonicon buttonicon-history',
},
savedrevision: defaultButtonAttributes('savedRevision'),
settings: defaultButtonAttributes('settings'),
embed: defaultButtonAttributes('embed'),
showusers: defaultButtonAttributes('showusers'),
timeslider_export: {
command: 'import_export',
localizationId: 'timeslider.toolbar.exportlink.title',
class: 'buttonicon buttonicon-import_export',
},
timeslider_settings: {
command: 'settings',
localizationId: 'pad.toolbar.settings.title',
class: 'buttonicon buttonicon-settings',
},
timeslider_returnToPad: {
command: 'timeslider_returnToPad',
localizationId: 'timeslider.toolbar.returnbutton',
class: 'buttontext',
},
},
registerButton(buttonName, buttonInfo) {
this.availableButtons[buttonName] = buttonInfo;
},
button: (attributes) => new Button(attributes),
separator: () => (new Separator()).render(),
selectButton: (attributes) => new SelectButton(attributes),
/*
* Valid values for whichMenu: 'left' | 'right' | 'timeslider-right'
* Valid values for page: 'pad' | 'timeslider'
*/
menu(buttons, isReadOnly, whichMenu, page) {
if (isReadOnly) {
// The best way to detect if it's the left editbar is to check for a bold button
if (buttons[0].indexOf('bold') !== -1) {
// Clear all formatting buttons
buttons = [];
} else {
// Remove Save Revision from the right menu
removeItem(buttons[0], 'savedrevision');
}
} else {
/*
* This pad is not read only
*
* Add back the savedrevision button (the "star") if is not already there,
* but only on the right toolbar, and only if we are showing a pad (dont't
* do it in the timeslider).
*
* This is a quick fix for #3702 (and subsequent issue #3767): it was
* sufficient to visit a single read only pad to cause the disappearence
* of the star button from all the pads.
*/
if ((buttons[0].indexOf('savedrevision') === -1) &&
(whichMenu === 'right') && (page === 'pad')) {
buttons[0].push('savedrevision');
}
}
const groups = _.map(buttons, (group) => ButtonsGroup.fromArray(group).render());
return groups.join(this.separator());
},
};

305
src/node/utils/toolbar.ts Normal file
View file

@ -0,0 +1,305 @@
'use strict';
/**
* The Toolbar Module creates and renders the toolbars and buttons
*/
const _ = require('underscore');
const removeItem = (array: string[], what: string) => {
let ax;
while ((ax = array.indexOf(what)) !== -1) {
array.splice(ax, 1);
}
return array;
};
const defaultButtonAttributes = (name: string, overrides?: boolean) => ({
command: name,
localizationId: `pad.toolbar.${name}.title`,
class: `buttonicon buttonicon-${name}`,
});
const tag = (name: string, attributes: AttributeObj, contents?: string) => {
const aStr = tagAttributes(attributes);
if (_.isString(contents) && contents!.length > 0) {
return `<${name}${aStr}>${contents}</${name}>`;
} else {
return `<${name}${aStr}></${name}>`;
}
};
type AttributeObj = {
[id: string]: string
}
const tagAttributes = (attributes: AttributeObj) => {
attributes = _.reduce(attributes || {}, (o: AttributeObj, val: string, name: string) => {
if (!_.isUndefined(val)) {
o[name] = val;
}
return o;
}, {});
return ` ${_.map(attributes, (val: string, name: string) => `${name}="${_.escape(val)}"`).join(' ')}`;
};
type ButtonGroupType = {
grouping: string,
render: Function
}
class ButtonGroup {
private buttons: Button[]
constructor() {
this.buttons = []
}
public static fromArray = function (array: string[]) {
const btnGroup = new ButtonGroup();
_.each(array, (btnName: string) => {
const button = Button.load(btnName) as Button
btnGroup.addButton(button);
});
return btnGroup;
}
private addButton(button: Button) {
this.buttons.push(button);
return this;
}
render() {
if (this.buttons && this.buttons.length === 1) {
this.buttons[0].grouping = '';
} else if (this.buttons && this.buttons.length > 1) {
_.first(this.buttons).grouping = 'grouped-left';
_.last(this.buttons).grouping = 'grouped-right';
_.each(this.buttons.slice(1, -1), (btn: Button) => {
btn.grouping = 'grouped-middle';
});
}
return _.map(this.buttons, (btn: ButtonGroup) => {
if (btn) return btn.render();
}).join('\n');
}
}
class Button {
protected attributes: AttributeObj
grouping: string
constructor(attributes: AttributeObj) {
this.attributes = attributes
this.grouping = ""
}
public static load(btnName: string) {
const button = module.exports.availableButtons[btnName];
try {
if (button.constructor === Button || button.constructor === SelectButton) {
return button;
} else {
return new Button(button);
}
} catch (e) {
console.warn('Error loading button', btnName);
return false;
}
}
render() {
const liAttributes = {
'data-type': 'button',
'data-key': this.attributes.command,
};
return tag('li', liAttributes,
tag('a', {'class': this.grouping, 'data-l10n-id': this.attributes.localizationId},
tag('button', {
'class': ` ${this.attributes.class}`,
'data-l10n-id': this.attributes.localizationId,
})));
}
}
type SelectButtonOptions = {
value: string,
text: string,
attributes: AttributeObj
}
class SelectButton extends Button {
private readonly options: SelectButtonOptions[];
constructor(attrs: AttributeObj) {
super(attrs);
this.options = []
}
addOption(value: string, text: string, attributes: AttributeObj) {
this.options.push({
value,
text,
attributes,
})
return this;
}
select(attributes: AttributeObj) {
const options: string[] = [];
_.each(this.options, (opt: AttributeSelect) => {
const a = _.extend({
value: opt.value,
}, opt.attributes);
options.push(tag('option', a, opt.text));
});
return tag('select', attributes, options.join(''));
}
render() {
const attributes = {
'id': this.attributes.id,
'data-key': this.attributes.command,
'data-type': 'select',
};
return tag('li', attributes, this.select({id: this.attributes.selectId}));
}
}
type AttributeSelect = {
value: string,
attributes: AttributeObj,
text: string
}
class Separator {
constructor() {
}
public render() {
return tag('li', {class: 'separator'});
}
}
module.exports = {
availableButtons: {
bold: defaultButtonAttributes('bold'),
italic: defaultButtonAttributes('italic'),
underline: defaultButtonAttributes('underline'),
strikethrough: defaultButtonAttributes('strikethrough'),
orderedlist: {
command: 'insertorderedlist',
localizationId: 'pad.toolbar.ol.title',
class: 'buttonicon buttonicon-insertorderedlist',
},
unorderedlist: {
command: 'insertunorderedlist',
localizationId: 'pad.toolbar.ul.title',
class: 'buttonicon buttonicon-insertunorderedlist',
},
indent: defaultButtonAttributes('indent'),
outdent: {
command: 'outdent',
localizationId: 'pad.toolbar.unindent.title',
class: 'buttonicon buttonicon-outdent',
},
undo: defaultButtonAttributes('undo'),
redo: defaultButtonAttributes('redo'),
clearauthorship: {
command: 'clearauthorship',
localizationId: 'pad.toolbar.clearAuthorship.title',
class: 'buttonicon buttonicon-clearauthorship',
},
importexport: {
command: 'import_export',
localizationId: 'pad.toolbar.import_export.title',
class: 'buttonicon buttonicon-import_export',
},
timeslider: {
command: 'showTimeSlider',
localizationId: 'pad.toolbar.timeslider.title',
class: 'buttonicon buttonicon-history',
},
savedrevision: defaultButtonAttributes('savedRevision'),
settings: defaultButtonAttributes('settings'),
embed: defaultButtonAttributes('embed'),
showusers: defaultButtonAttributes('showusers'),
timeslider_export: {
command: 'import_export',
localizationId: 'timeslider.toolbar.exportlink.title',
class: 'buttonicon buttonicon-import_export',
},
timeslider_settings: {
command: 'settings',
localizationId: 'pad.toolbar.settings.title',
class: 'buttonicon buttonicon-settings',
},
timeslider_returnToPad: {
command: 'timeslider_returnToPad',
localizationId: 'timeslider.toolbar.returnbutton',
class: 'buttontext',
},
},
registerButton(buttonName: string, buttonInfo: any) {
this.availableButtons[buttonName] = buttonInfo;
},
button: (attributes: AttributeObj) => new Button(attributes),
separator: () => (new Separator()).render(),
selectButton: (attributes: AttributeObj) => new SelectButton(attributes),
/*
* Valid values for whichMenu: 'left' | 'right' | 'timeslider-right'
* Valid values for page: 'pad' | 'timeslider'
*/
menu(buttons: string[][], isReadOnly: boolean, whichMenu: string, page: string) {
if (isReadOnly) {
// The best way to detect if it's the left editbar is to check for a bold button
if (buttons[0].indexOf('bold') !== -1) {
// Clear all formatting buttons
buttons = [];
} else {
// Remove Save Revision from the right menu
removeItem(buttons[0], 'savedrevision');
}
} else if ((buttons[0].indexOf('savedrevision') === -1) &&
(whichMenu === 'right') && (page === 'pad')) {
/*
* This pad is not read only
*
* Add back the savedrevision button (the "star") if is not already there,
* but only on the right toolbar, and only if we are showing a pad (dont't
* do it in the timeslider).
*
* This is a quick fix for #3702 (and subsequent issue #3767): it was
* sufficient to visit a single read only pad to cause the disappearence
* of the star button from all the pads.
*/
buttons[0].push('savedrevision');
}
const groups = _.map(buttons, (group: string[]) => ButtonGroup.fromArray(group).render());
return groups.join(this.separator());
},
};

389
src/package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "ep_etherpad-lite", "name": "ep_etherpad-lite",
"version": "1.9.7", "version": "1.9.6",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -22,6 +22,154 @@
"lodash.clonedeep": "^4.5.0" "lodash.clonedeep": "^4.5.0"
} }
}, },
"@asamuzakjp/dom-selector": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz",
"integrity": "sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==",
"requires": {
"bidi-js": "^1.0.3",
"css-tree": "^2.3.1",
"is-potential-custom-element-name": "^1.0.1"
}
},
"@esbuild/aix-ppc64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
"integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
"optional": true
},
"@esbuild/android-arm": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
"integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
"optional": true
},
"@esbuild/android-arm64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
"integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
"optional": true
},
"@esbuild/android-x64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
"integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
"optional": true
},
"@esbuild/darwin-arm64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
"integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
"optional": true
},
"@esbuild/darwin-x64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
"integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
"optional": true
},
"@esbuild/freebsd-arm64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
"integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
"optional": true
},
"@esbuild/freebsd-x64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
"integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
"optional": true
},
"@esbuild/linux-arm": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
"integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
"optional": true
},
"@esbuild/linux-arm64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
"integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
"optional": true
},
"@esbuild/linux-ia32": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
"integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
"optional": true
},
"@esbuild/linux-loong64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
"integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
"optional": true
},
"@esbuild/linux-mips64el": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
"integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
"optional": true
},
"@esbuild/linux-ppc64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
"integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
"optional": true
},
"@esbuild/linux-riscv64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
"integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
"optional": true
},
"@esbuild/linux-s390x": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
"integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
"optional": true
},
"@esbuild/linux-x64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
"integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
"optional": true
},
"@esbuild/netbsd-x64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
"integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
"optional": true
},
"@esbuild/openbsd-x64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
"integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
"optional": true
},
"@esbuild/sunos-x64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
"integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
"optional": true
},
"@esbuild/win32-arm64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
"integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
"optional": true
},
"@esbuild/win32-ia32": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
"integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
"optional": true
},
"@esbuild/win32-x64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
"integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
"optional": true
},
"@eslint-community/eslint-utils": { "@eslint-community/eslint-utils": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -261,6 +409,31 @@
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
"dev": true "dev": true
}, },
"@types/async": {
"version": "3.2.24",
"resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.24.tgz",
"integrity": "sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==",
"dev": true
},
"@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/debug": { "@types/debug": {
"version": "4.1.12", "version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -269,6 +442,30 @@
"@types/ms": "*" "@types/ms": "*"
} }
}, },
"@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.43",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
"integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"@types/fs-extra": { "@types/fs-extra": {
"version": "9.0.13", "version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
@ -285,6 +482,12 @@
"@types/unist": "*" "@types/unist": "*"
} }
}, },
"@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true
},
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.13", "version": "7.0.13",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
@ -302,14 +505,14 @@
"integrity": "sha512-Q8oFIHJHr+htLrTXN2FuZfg+WXVHQRwU/hC2GpUu+Q8e3FUM9EDkS2pE3R2AO1ZGu56f479ybdMCNF1DAu8cAQ==" "integrity": "sha512-Q8oFIHJHr+htLrTXN2FuZfg+WXVHQRwU/hC2GpUu+Q8e3FUM9EDkS2pE3R2AO1ZGu56f479ybdMCNF1DAu8cAQ=="
}, },
"@types/lodash": { "@types/lodash": {
"version": "4.14.202", "version": "4.14.199",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz",
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg=="
}, },
"@types/lodash.clonedeep": { "@types/lodash.clonedeep": {
"version": "4.5.9", "version": "4.5.7",
"resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz",
"integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", "integrity": "sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==",
"requires": { "requires": {
"@types/lodash": "*" "@types/lodash": "*"
} }
@ -322,15 +525,21 @@
"@types/unist": "*" "@types/unist": "*"
} }
}, },
"@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true
},
"@types/ms": { "@types/ms": {
"version": "0.7.34", "version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
}, },
"@types/node": { "@types/node": {
"version": "20.10.6", "version": "20.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
"integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==",
"requires": { "requires": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@ -344,11 +553,44 @@
"form-data": "^4.0.0" "form-data": "^4.0.0"
} }
}, },
"@types/qs": {
"version": "6.9.11",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
"integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true
},
"@types/semver": { "@types/semver": {
"version": "7.5.3", "version": "7.5.3",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz",
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==" "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw=="
}, },
"@types/send": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"requires": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"@types/serve-static": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
"integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
"dev": true,
"requires": {
"@types/http-errors": "*",
"@types/mime": "*",
"@types/node": "*"
}
},
"@types/tar": { "@types/tar": {
"version": "6.1.10", "version": "6.1.10",
"resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.10.tgz", "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.10.tgz",
@ -358,6 +600,12 @@
"minipass": "^4.0.0" "minipass": "^4.0.0"
} }
}, },
"@types/underscore": {
"version": "1.11.15",
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz",
"integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==",
"dev": true
},
"@types/unist": { "@types/unist": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
@ -767,9 +1015,9 @@
"dev": true "dev": true
}, },
"axios": { "axios": {
"version": "1.6.7", "version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"requires": { "requires": {
"follow-redirects": "^1.15.4", "follow-redirects": "^1.15.4",
"form-data": "^4.0.0", "form-data": "^4.0.0",
@ -806,6 +1054,14 @@
"resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz", "resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz",
"integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==" "integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg=="
}, },
"bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"requires": {
"require-from-string": "^2.0.2"
}
},
"binary-extensions": { "binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -1082,6 +1338,15 @@
"which": "^2.0.1" "which": "^2.0.1"
} }
}, },
"css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"requires": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
}
},
"cssstyle": { "cssstyle": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
@ -1412,6 +1677,36 @@
"is-symbol": "^1.0.2" "is-symbol": "^1.0.2"
} }
}, },
"esbuild": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
"integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
"requires": {
"@esbuild/aix-ppc64": "0.19.11",
"@esbuild/android-arm": "0.19.11",
"@esbuild/android-arm64": "0.19.11",
"@esbuild/android-x64": "0.19.11",
"@esbuild/darwin-arm64": "0.19.11",
"@esbuild/darwin-x64": "0.19.11",
"@esbuild/freebsd-arm64": "0.19.11",
"@esbuild/freebsd-x64": "0.19.11",
"@esbuild/linux-arm": "0.19.11",
"@esbuild/linux-arm64": "0.19.11",
"@esbuild/linux-ia32": "0.19.11",
"@esbuild/linux-loong64": "0.19.11",
"@esbuild/linux-mips64el": "0.19.11",
"@esbuild/linux-ppc64": "0.19.11",
"@esbuild/linux-riscv64": "0.19.11",
"@esbuild/linux-s390x": "0.19.11",
"@esbuild/linux-x64": "0.19.11",
"@esbuild/netbsd-x64": "0.19.11",
"@esbuild/openbsd-x64": "0.19.11",
"@esbuild/sunos-x64": "0.19.11",
"@esbuild/win32-arm64": "0.19.11",
"@esbuild/win32-ia32": "0.19.11",
"@esbuild/win32-x64": "0.19.11"
}
},
"escalade": { "escalade": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@ -2249,7 +2544,6 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"optional": true "optional": true
}, },
"function-bind": { "function-bind": {
@ -2306,7 +2600,6 @@
"version": "4.7.2", "version": "4.7.2",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz",
"integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==",
"dev": true,
"requires": { "requires": {
"resolve-pkg-maps": "^1.0.0" "resolve-pkg-maps": "^1.0.0"
} }
@ -2963,10 +3256,11 @@
} }
}, },
"jsdom": { "jsdom": {
"version": "24.0.0", "version": "23.2.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz",
"integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==",
"requires": { "requires": {
"@asamuzakjp/dom-selector": "^2.0.1",
"cssstyle": "^4.0.1", "cssstyle": "^4.0.1",
"data-urls": "^5.0.0", "data-urls": "^5.0.0",
"decimal.js": "^10.4.3", "decimal.js": "^10.4.3",
@ -2975,7 +3269,6 @@
"http-proxy-agent": "^7.0.0", "http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.2",
"is-potential-custom-element-name": "^1.0.1", "is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.7",
"parse5": "^7.1.2", "parse5": "^7.1.2",
"rrweb-cssom": "^0.6.0", "rrweb-cssom": "^0.6.0",
"saxes": "^6.0.0", "saxes": "^6.0.0",
@ -3263,6 +3556,11 @@
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0"
} }
}, },
"mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
},
"measured-core": { "measured-core": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/measured-core/-/measured-core-2.0.0.tgz", "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-2.0.0.tgz",
@ -6895,11 +7193,6 @@
} }
} }
}, },
"nwsapi": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
"integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ=="
},
"object-inspect": { "object-inspect": {
"version": "1.12.3", "version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
@ -6984,9 +7277,9 @@
} }
}, },
"openapi-backend": { "openapi-backend": {
"version": "5.10.6", "version": "5.10.5",
"resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-5.10.6.tgz", "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-5.10.5.tgz",
"integrity": "sha512-vTjBRys/O4JIHdlRHUKZ7pxS+gwIJreAAU9dvYRFrImtPzQ5qxm5a6B8BTVT9m6I8RGGsShJv35MAc3Tu2/y/A==", "integrity": "sha512-ivZfL0Lwj7rRctCqxAquGy4j/VcdUXUvDsEVM3NG/2jDuvYT2dS+sf9ntGo5vv4hkOnkWgPnR6HxHp7NPexqAA==",
"requires": { "requires": {
"@apidevtools/json-schema-ref-parser": "^11.1.0", "@apidevtools/json-schema-ref-parser": "^11.1.0",
"ajv": "^8.6.2", "ajv": "^8.6.2",
@ -7243,9 +7536,9 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
}, },
"rate-limiter-flexible": { "rate-limiter-flexible": {
"version": "4.0.1", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-4.0.1.tgz", "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-4.0.0.tgz",
"integrity": "sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==" "integrity": "sha512-SkA18LEPqJJKHixi6E7tzBKTXbj9gu5wPyfTykPVRZR5JGSw0dMCjtZsjlfuabVY940pu28Wu87NZN4FhztnyQ=="
}, },
"raw-body": { "raw-body": {
"version": "2.5.1", "version": "2.5.1",
@ -7385,8 +7678,7 @@
"resolve-pkg-maps": { "resolve-pkg-maps": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="
"dev": true
}, },
"reusify": { "reusify": {
"version": "1.0.4", "version": "1.0.4",
@ -7477,9 +7769,9 @@
"integrity": "sha512-5qfoAgfRWS1sUn+fUJtdbbqM1BD/LoQGa+smPTDjf9OqHyuJqi6ewtbYL0+V1S1RaU6OCOCMWGZocIfz2YK4uw==" "integrity": "sha512-5qfoAgfRWS1sUn+fUJtdbbqM1BD/LoQGa+smPTDjf9OqHyuJqi6ewtbYL0+V1S1RaU6OCOCMWGZocIfz2YK4uw=="
}, },
"selenium-webdriver": { "selenium-webdriver": {
"version": "4.17.0", "version": "4.16.0",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.17.0.tgz", "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.16.0.tgz",
"integrity": "sha512-e2E+2XBlGepzwgFbyQfSwo9Cbj6G5fFfs9MzAS00nC99EewmcS2rwn2MwtgfP7I5p1e7DYv4HQJXtWedsu6DvA==", "integrity": "sha512-IbqpRpfGE7JDGgXHJeWuCqT/tUqnLvZ14csSwt+S8o4nJo3RtQoE9VR4jB47tP/A8ArkYsh/THuMY6kyRP6kuA==",
"dev": true, "dev": true,
"requires": { "requires": {
"jszip": "^3.10.1", "jszip": "^3.10.1",
@ -7742,6 +8034,11 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}, },
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
},
"source-map-support": { "source-map-support": {
"version": "0.5.21", "version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@ -8148,6 +8445,16 @@
"tslib": "^1.8.1" "tslib": "^1.8.1"
} }
}, },
"tsx": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.0.tgz",
"integrity": "sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==",
"requires": {
"esbuild": "~0.19.10",
"fsevents": "~2.3.3",
"get-tsconfig": "^4.7.2"
}
},
"type-check": { "type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -8232,9 +8539,9 @@
"dev": true "dev": true
}, },
"ueberdb2": { "ueberdb2": {
"version": "4.2.50", "version": "4.2.48",
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-4.2.50.tgz", "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-4.2.48.tgz",
"integrity": "sha512-XOiWxmDHhoCNLSrepIJa+kdRBXABuI5ZXQhXpmT2Z8qK3EbipcrAeQBWDQXfcnnHftmzaR3NY7zil76biCN/GQ==" "integrity": "sha512-kazgi/wodwsPZGo7EBfAjm17HvzXb4RDau1NocRoWCwkjK7hz5t+5QkoCgeTPMSr7uEpRSmVVmci3dYsLsaB5g=="
}, },
"uid-safe": { "uid-safe": {
"version": "2.1.5", "version": "2.1.5",
@ -8511,9 +8818,9 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}, },
"ws": { "ws": {
"version": "8.16.0", "version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"dev": true "dev": true
}, },
"wtfnode": { "wtfnode": {

View file

@ -31,7 +31,7 @@
], ],
"dependencies": { "dependencies": {
"async": "^3.2.5", "async": "^3.2.5",
"axios": "^1.6.7", "axios": "^1.6.5",
"clean-css": "^5.3.3", "clean-css": "^5.3.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
@ -46,7 +46,7 @@
"formidable": "^3.5.1", "formidable": "^3.5.1",
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsdom": "^24.0.0", "jsdom": "^23.2.0",
"jsonminify": "0.4.2", "jsonminify": "0.4.2",
"languages4translatewiki": "0.1.3", "languages4translatewiki": "0.1.3",
"live-plugin-manager": "^0.18.1", "live-plugin-manager": "^0.18.1",
@ -55,9 +55,9 @@
"measured-core": "^2.0.0", "measured-core": "^2.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"npm": "^6.14.18", "npm": "^6.14.18",
"openapi-backend": "^5.10.6", "openapi-backend": "^5.10.5",
"proxy-addr": "^2.0.7", "proxy-addr": "^2.0.7",
"rate-limiter-flexible": "^4.0.1", "rate-limiter-flexible": "^4.0.0",
"rehype": "^13.0.1", "rehype": "^13.0.1",
"rehype-minify-whitespace": "^6.0.0", "rehype-minify-whitespace": "^6.0.0",
"resolve": "1.22.8", "resolve": "1.22.8",
@ -68,17 +68,21 @@
"terser": "^5.27.0", "terser": "^5.27.0",
"threads": "^1.7.0", "threads": "^1.7.0",
"tinycon": "0.6.8", "tinycon": "0.6.8",
"ueberdb2": "^4.2.50", "tsx": "^4.7.0",
"ueberdb2": "^4.2.48",
"underscore": "1.13.6", "underscore": "1.13.6",
"unorm": "1.6.0", "unorm": "1.6.0",
"wtfnode": "^0.9.1" "wtfnode": "^0.9.1"
}, },
"bin": { "bin": {
"etherpad": "node/server.js",
"etherpad-healthcheck": "bin/etherpad-healthcheck", "etherpad-healthcheck": "bin/etherpad-healthcheck",
"etherpad-lite": "node/server.js" "etherpad-lite": "node/server.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/async": "^3.2.24",
"@types/express": "^4.17.21",
"@types/node": "^20.11.5",
"@types/underscore": "^1.11.15",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-etherpad": "^3.0.22", "eslint-config-etherpad": "^3.0.22",
"etherpad-cli-client": "^2.0.2", "etherpad-cli-client": "^2.0.2",
@ -86,7 +90,7 @@
"mocha-froth": "^0.2.10", "mocha-froth": "^0.2.10",
"nodeify": "^1.0.1", "nodeify": "^1.0.1",
"openapi-schema-validation": "^0.4.2", "openapi-schema-validation": "^0.4.2",
"selenium-webdriver": "^4.17.0", "selenium-webdriver": "^4.16.0",
"set-cookie-parser": "^2.6.0", "set-cookie-parser": "^2.6.0",
"sinon": "^17.0.1", "sinon": "^17.0.1",
"split-grid": "^1.0.11", "split-grid": "^1.0.11",
@ -103,9 +107,13 @@
}, },
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", "test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
"test-container": "mocha --timeout 5000 tests/container/specs/api" "test-container": "mocha --import=tsx --timeout 5000 tests/container/specs/api",
"dev": "node --import tsx node/server.ts",
"prod": "node --import tsx node/server.ts",
"ts-check": "tsc --noEmit",
"ts-check:watch": "tsc --noEmit --watch"
}, },
"version": "1.9.7", "version": "1.9.6",
"license": "Apache-2.0" "license": "Apache-2.0"
} }

View file

@ -1,10 +1,10 @@
'use strict'; 'use strict';
const SecretRotator = require('../../../node/security/SecretRotator');
const assert = require('assert').strict; const assert = require('assert').strict;
const common = require('../common'); const common = require('../common');
const crypto = require('../../../node/security/crypto'); const crypto = require('../../../node/security/crypto');
const db = require('../../../node/db/DB'); const db = require('../../../node/db/DB');
const SecretRotator = require("../../../node/security/SecretRotator").SecretRotator;
const logger = common.logger; const logger = common.logger;

View file

@ -4,6 +4,6 @@ module.exports = defineConfig({
e2e: { e2e: {
baseUrl: "http://127.0.0.1:9001", baseUrl: "http://127.0.0.1:9001",
supportFile: false, supportFile: false,
specPattern: 'src/tests/frontend/cypress/integration/**/*.js' specPattern: 'tests/frontend/cypress/integration/**/*.js'
} }
}) })

View file

@ -11,8 +11,8 @@ MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1
try cd "${MY_DIR}/../../../.." try cd "${MY_DIR}/../../../.."
log "Assuming src/bin/installDeps.sh has already been run" log "Assuming src/bin/installDeps.sh has already been run"
node src/node/server.js --experimental-worker "${@}" & ( cd src && npm run dev --experimental-worker "${@}" &
ep_pid=$! ep_pid=$!)
log "Waiting for Etherpad to accept connections (http://localhost:9001)..." log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
connected=false connected=false

View file

@ -11,8 +11,8 @@ MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1
try cd "${MY_DIR}/../../../.." try cd "${MY_DIR}/../../../.."
log "Assuming src/bin/installDeps.sh has already been run" log "Assuming src/bin/installDeps.sh has already been run"
node src/node/server.js --experimental-worker "${@}" & (cd src && npm run dev --experimental-worker "${@}" &
ep_pid=$! ep_pid=$!)
log "Waiting for Etherpad to accept connections (http://localhost:9001)..." log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
connected=false connected=false

View file

@ -19,8 +19,8 @@ s!"points":[^,]*!"points": 1000!
log "Deprecation notice: runnerBackend.sh - Please use: cd src && npm test" log "Deprecation notice: runnerBackend.sh - Please use: cd src && npm test"
log "Assuming src/bin/installDeps.sh has already been run" log "Assuming src/bin/installDeps.sh has already been run"
node src/node/server.js "${@}" & (cd src && npm run dev "${@}" &
ep_pid=$! ep_pid=$!)
log "Waiting for Etherpad to accept connections (http://localhost:9001)..." log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
connected=false connected=false

View file

@ -24,8 +24,8 @@ s!"points":[^,]*!"points": 1000!
' settings.json.template >settings.json ' settings.json.template >settings.json
log "Assuming src/bin/installDeps.sh has already been run" log "Assuming src/bin/installDeps.sh has already been run"
node src/node/server.js & (cd src && npm run dev &
ep_pid=$! ep_pid=$!)
log "Waiting for Etherpad to accept connections (http://localhost:9001)..." log "Waiting for Etherpad to accept connections (http://localhost:9001)..."
connected=false connected=false

View file

@ -1,4 +1,4 @@
FROM node:alpine3.12 FROM node:latest
WORKDIR /tmp WORKDIR /tmp
RUN npm i etherpad-cli-client RUN npm i etherpad-cli-client
COPY ./src/tests/ratelimit/send_changesets.js /tmp/send_changesets.js COPY ./src/tests/ratelimit/send_changesets.js /tmp/send_changesets.js

17
src/tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
"moduleDetection": "force",
"lib": ["es6"],
/* Language and Environment */
"target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View file

@ -2,7 +2,7 @@
<system.webServer> <system.webServer>
<handlers> <handlers>
<add name="iisnode" path="src/node/server.js" verb="*" modules="iisnode" /> <add name="iisnode" path="src/node/server.ts" verb="*" modules="iisnode" />
</handlers> </handlers>
<rewrite> <rewrite>
@ -13,7 +13,7 @@
<action type="Rewrite" url="src/node/iisnode" /> <action type="Rewrite" url="src/node/iisnode" />
</rule> </rule>
<rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true"> <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
<match url="^server.js\/debug[\/]?" /> <match url="^server.ts\/debug[\/]?" />
</rule> </rule>
--> -->
<rule name="StaticContent"> <rule name="StaticContent">
@ -23,7 +23,7 @@
<conditions> <conditions>
<add input="{{REQUEST_FILENAME}}" matchType="IsFile" negate="True"/> <add input="{{REQUEST_FILENAME}}" matchType="IsFile" negate="True"/>
</conditions> </conditions>
<action type="Rewrite" url="src/node/server.js" /> <action type="Rewrite" url="src/node/server.ts" />
</rule> </rule>
</rules> </rules>
</rewrite> </rewrite>

View file

@ -8,4 +8,4 @@ REM around this, everything must consistently use either `src` or
REM `node_modules\ep_etherpad-lite` on Windows. Because some plugins access REM `node_modules\ep_etherpad-lite` on Windows. Because some plugins access
REM Etherpad internals via `require('ep_etherpad-lite/foo')`, REM Etherpad internals via `require('ep_etherpad-lite/foo')`,
REM `node_modules\ep_etherpad-lite` is used here. REM `node_modules\ep_etherpad-lite` is used here.
node node_modules\ep_etherpad-lite\node\server.js node run dev