From ead3c0ea381b8cb5b7ac39004a7f319200504a4d Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+SamTV12345@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:13:02 +0100 Subject: [PATCH] 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. --- .github/workflows/perform-type-check.yml | 33 ++ .github/workflows/rate-limit.yml | 2 +- .../workflows/upgrade-from-latest-release.yml | 11 +- .github/workflows/windows.yml | 6 +- Dockerfile | 13 +- README.md | 2 +- doc/api/hooks_server-side.adoc | 4 +- .../deb-src/sysroot/etc/init/etherpad.conf | 2 +- src/bin/push-after-release.sh | 0 src/bin/run.sh | 2 +- src/node/README.md | 2 +- src/node/db/{DB.js => DB.ts} | 6 +- .../db/{SessionStore.js => SessionStore.ts} | 13 +- src/node/handler/PadMessageHandler.js | 4 +- src/node/handler/SocketIORouter.js | 2 +- src/node/hooks/{express.js => express.ts} | 44 +- src/node/hooks/express/{admin.js => admin.ts} | 7 +- .../{adminplugins.js => adminplugins.ts} | 38 +- .../{adminsettings.js => adminsettings.ts} | 13 +- .../express/{apicalls.js => apicalls.ts} | 12 +- .../{errorhandling.js => errorhandling.ts} | 9 +- .../{importexport.js => importexport.ts} | 11 +- .../hooks/express/{openapi.js => openapi.ts} | 65 +-- .../{padurlsanitize.js => padurlsanitize.ts} | 6 +- .../express/{socketio.js => socketio.ts} | 14 +- src/node/hooks/{i18n.js => i18n.ts} | 33 +- src/node/{padaccess.js => padaccess.ts} | 2 +- .../{SecretRotator.js => SecretRotator.ts} | 53 ++- src/node/security/{crypto.js => crypto.ts} | 0 src/node/{server.js => server.ts} | 39 +- src/node/{stats.js => stats.ts} | 3 +- src/node/types/ArgsExpressType.ts | 5 + src/node/types/AsyncQueueTask.ts | 5 + src/node/types/DeriveModel.ts | 6 + src/node/types/ErrorCaused.ts | 14 + src/node/types/I18nPluginDefs.ts | 5 + src/node/types/LegacyParams.ts | 8 + src/node/types/MapType.ts | 7 + src/node/types/PadType.ts | 16 + src/node/types/Plugin.ts | 9 + src/node/types/PromiseWithStd.ts | 8 + src/node/types/QueryType.ts | 3 + src/node/types/RunCMDOptions.ts | 15 + src/node/types/SecretRotatorType.ts | 3 + src/node/types/SwaggerUIResource.ts | 34 ++ src/node/utils/{Abiword.js => Abiword.ts} | 29 +- .../{AbsolutePaths.js => AbsolutePaths.ts} | 13 +- .../{ExportEtherpad.js => ExportEtherpad.ts} | 4 +- .../{ExportHelper.js => ExportHelper.ts} | 12 +- src/node/utils/{ExportTxt.js => ExportTxt.ts} | 22 +- src/node/utils/{Stream.js => Stream.ts} | 15 +- .../utils/{UpdateCheck.js => UpdateCheck.ts} | 21 +- ...ng_middleware.js => caching_middleware.ts} | 57 +-- .../{checkValidRev.js => checkValidRev.ts} | 4 +- .../utils/{customError.js => customError.ts} | 4 +- .../utils/{path_exists.js => path_exists.ts} | 4 +- src/node/utils/{promises.js => promises.ts} | 16 +- .../{randomstring.js => randomstring.ts} | 4 +- src/node/utils/{run_cmd.js => run_cmd.ts} | 34 +- ...anitizePathname.js => sanitizePathname.ts} | 2 +- src/node/utils/toolbar.js | 270 ------------ src/node/utils/toolbar.ts | 305 ++++++++++++++ src/package-lock.json | 389 ++++++++++++++++-- src/package.json | 30 +- src/tests/backend/specs/SecretRotator.js | 2 +- src/tests/frontend/cypress/cypress.config.js | 2 +- src/tests/frontend/travis/adminrunner.sh | 4 +- src/tests/frontend/travis/runner.sh | 4 +- src/tests/frontend/travis/runnerBackend.sh | 4 +- src/tests/frontend/travis/runnerLoadTest.sh | 4 +- src/tests/ratelimit/Dockerfile.anotherip | 2 +- src/tsconfig.json | 17 + src/web.config | 6 +- start.bat | 2 +- 74 files changed, 1259 insertions(+), 612 deletions(-) create mode 100644 .github/workflows/perform-type-check.yml mode change 100644 => 100755 src/bin/push-after-release.sh rename src/node/db/{DB.js => DB.ts} (91%) rename src/node/db/{SessionStore.js => SessionStore.ts} (94%) rename src/node/hooks/{express.js => express.ts} (90%) rename src/node/hooks/express/{admin.js => admin.ts} (68%) rename src/node/hooks/express/{adminplugins.js => adminplugins.ts} (76%) rename src/node/hooks/express/{adminsettings.js => adminsettings.ts} (79%) rename src/node/hooks/express/{apicalls.js => apicalls.ts} (78%) rename src/node/hooks/express/{errorhandling.js => errorhandling.ts} (56%) rename src/node/hooks/express/{importexport.js => importexport.ts} (90%) rename src/node/hooks/express/{openapi.js => openapi.ts} (90%) rename src/node/hooks/express/{padurlsanitize.js => padurlsanitize.ts} (81%) rename src/node/hooks/express/{socketio.js => socketio.ts} (94%) rename src/node/hooks/{i18n.js => i18n.ts} (81%) rename src/node/{padaccess.js => padaccess.ts} (70%) rename src/node/security/{SecretRotator.js => SecretRotator.ts} (87%) rename src/node/security/{crypto.js => crypto.ts} (100%) rename src/node/{server.js => server.ts} (90%) rename src/node/{stats.js => stats.ts} (92%) create mode 100644 src/node/types/ArgsExpressType.ts create mode 100644 src/node/types/AsyncQueueTask.ts create mode 100644 src/node/types/DeriveModel.ts create mode 100644 src/node/types/ErrorCaused.ts create mode 100644 src/node/types/I18nPluginDefs.ts create mode 100644 src/node/types/LegacyParams.ts create mode 100644 src/node/types/MapType.ts create mode 100644 src/node/types/PadType.ts create mode 100644 src/node/types/Plugin.ts create mode 100644 src/node/types/PromiseWithStd.ts create mode 100644 src/node/types/QueryType.ts create mode 100644 src/node/types/RunCMDOptions.ts create mode 100644 src/node/types/SecretRotatorType.ts create mode 100644 src/node/types/SwaggerUIResource.ts rename src/node/utils/{Abiword.js => Abiword.ts} (72%) rename src/node/utils/{AbsolutePaths.js => AbsolutePaths.ts} (94%) rename src/node/utils/{ExportEtherpad.js => ExportEtherpad.ts} (96%) rename src/node/utils/{ExportHelper.js => ExportHelper.ts} (86%) rename src/node/utils/{ExportTxt.js => ExportTxt.ts} (92%) rename src/node/utils/{Stream.js => Stream.ts} (93%) rename src/node/utils/{UpdateCheck.js => UpdateCheck.ts} (79%) rename src/node/utils/{caching_middleware.js => caching_middleware.ts} (83%) rename src/node/utils/{checkValidRev.js => checkValidRev.ts} (81%) rename src/node/utils/{customError.js => customError.ts} (85%) rename src/node/utils/{path_exists.js => path_exists.ts} (65%) rename src/node/utils/{promises.js => promises.ts} (89%) rename src/node/utils/{randomstring.js => randomstring.ts} (59%) rename src/node/utils/{run_cmd.js => run_cmd.ts} (83%) rename src/node/utils/{sanitizePathname.js => sanitizePathname.ts} (96%) delete mode 100644 src/node/utils/toolbar.js create mode 100644 src/node/utils/toolbar.ts create mode 100644 src/tsconfig.json diff --git a/.github/workflows/perform-type-check.yml b/.github/workflows/perform-type-check.yml new file mode 100644 index 000000000..66f57672e --- /dev/null +++ b/.github/workflows/perform-type-check.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/rate-limit.yml b/.github/workflows/rate-limit.yml index 1e878dc44..736091f63 100644 --- a/.github/workflows/rate-limit.yml +++ b/.github/workflows/rate-limit.yml @@ -33,7 +33,7 @@ jobs: - name: build docker image 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.anotherip -t anotherip . - diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index cdd6d20b3..cc5e7736c 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -87,13 +87,16 @@ jobs: run: src/bin/installDeps.sh - name: Run the backend tests - run: cd src && npm test + working-directory: ./src + run: npm test - 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 + working-directory: ./src 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 - ./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 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 8dbd03339..e95bbeae5 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -113,14 +113,16 @@ jobs: cache-dependency-path: | etherpad/src/package-lock.json etherpad/src/bin/doc/package-lock.json + - name: Install npm@6 + run: npm install npm@6 -g - 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 run: | 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 src\node_modules\cypress\bin\cypress run --config-file src\tests\frontendcypress\cypress.config.js # On release, upload windows zip to GitHub release tab diff --git a/Dockerfile b/Dockerfile index 0f42ab832..4c2903a46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # # Author: muxator -FROM node:lts-alpine +FROM node:alpine AS builder LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite" ARG TIMEZONE= @@ -63,7 +63,8 @@ ARG EP_UID=5001 ARG EP_GID=0 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 && \ 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 RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}" +USER root # the mkdir is needed for configuration of openjdk-11-jre-headless, see # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199 RUN \ @@ -108,13 +110,16 @@ RUN { [ -z "${ETHERPAD_PLUGINS}" ] || \ COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json # Fix group permissions -RUN chmod -R g=u . +#RUN chmod -R g=u . USER root RUN cd src && npm link + USER etherpad +WORKDIR /opt/etherpad-lite + HEALTHCHECK --interval=20s --timeout=3s CMD ["etherpad-healthcheck"] EXPOSE 9001 -CMD ["etherpad"] +CMD ["npm", "run", "prod", "--prefix", "./src"] \ No newline at end of file diff --git a/README.md b/README.md index 41da517af..2c9b80586 100644 --- a/README.md +++ b/README.md @@ -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 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 the file (e.g. `application/lib/etherpad-lite/var/dirty.db`) 3. Add auto-generated files to the main project `.gitignore` diff --git a/doc/api/hooks_server-side.adoc b/doc/api/hooks_server-side.adoc index a5e70ec02..d95d4ec6b 100644 --- a/doc/api/hooks_server-side.adoc +++ b/doc/api/hooks_server-side.adoc @@ -2,7 +2,7 @@ These hooks are called on server-side. === loadSettings -Called from: src/node/server.js +Called from: src/node/server.ts Things in context: @@ -11,7 +11,7 @@ Things in context: Use this hook to receive the global settings in your plugin. === shutdown -Called from: src/node/server.js +Called from: src/node/server.ts Things in context: None diff --git a/src/bin/deb-src/sysroot/etc/init/etherpad.conf b/src/bin/deb-src/sysroot/etc/init/etherpad.conf index 82706654d..0d017df1e 100644 --- a/src/bin/deb-src/sysroot/etc/init/etherpad.conf +++ b/src/bin/deb-src/sysroot/etc/init/etherpad.conf @@ -20,7 +20,7 @@ end script script 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 \ 2>> $EPLOGS/error.log echo "Etherpad is running on http://localhost:9001 - To change settings edit /opt/etherpad/settings.json" diff --git a/src/bin/push-after-release.sh b/src/bin/push-after-release.sh old mode 100644 new mode 100755 diff --git a/src/bin/run.sh b/src/bin/run.sh index b655e9635..235105a02 100755 --- a/src/bin/run.sh +++ b/src/bin/run.sh @@ -32,4 +32,4 @@ src/bin/installDeps.sh "$@" || exit 1 # Move to the node folder and start log "Starting Etherpad..." -exec node src/node/server.js "$@" +exec npm run dev --prefix ./src "$@" diff --git a/src/node/README.md b/src/node/README.md index d0a61287a..88a53bfd1 100644 --- a/src/node/README.md +++ b/src/node/README.md @@ -10,4 +10,4 @@ Module file names start with a capital letter and uses camelCase # Where does it start? -server.js is started directly +server.ts is started directly diff --git a/src/node/db/DB.js b/src/node/db/DB.ts similarity index 91% rename from src/node/db/DB.js rename to src/node/db/DB.ts index 02e83f85d..1daab8dd3 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.ts @@ -24,7 +24,7 @@ const ueberDB = require('ueberdb2'); const settings = require('../utils/Settings'); const log4js = require('log4js'); -const stats = require('../stats'); +const stats = require('../stats') const logger = log4js.getLogger('ueberDB'); @@ -47,13 +47,13 @@ exports.init = async () => { } for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) { 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.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(); exports.db = null; logger.log('Database closed'); diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.ts similarity index 94% rename from src/node/db/SessionStore.js rename to src/node/db/SessionStore.ts index 40e5e90d0..5dca5e201 100644 --- a/src/node/db/SessionStore.js +++ b/src/node/db/SessionStore.ts @@ -34,9 +34,10 @@ class SessionStore extends Store { 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) || {}; clearTimeout(exp.timeout); + // @ts-ignore const {cookie: {expires} = {}} = sess || {}; if (expires) { const sessExp = new Date(expires).getTime(); @@ -63,23 +64,23 @@ class SessionStore extends Store { return sess; } - async _write(sid, sess) { + async _write(sid: string, sess: any) { await DB.set(`sessionstorage:${sid}`, sess); } - async _get(sid) { + async _get(sid: string) { logger.debug(`GET ${sid}`); const s = await DB.get(`sessionstorage:${sid}`); return await this._updateExpirations(sid, s); } - async _set(sid, sess) { + async _set(sid: string, sess:any) { logger.debug(`SET ${sid}`); sess = await this._updateExpirations(sid, sess); if (sess != null) await this._write(sid, sess); } - async _destroy(sid) { + async _destroy(sid:string) { logger.debug(`DESTROY ${sid}`); clearTimeout((this._expirations.get(sid) || {}).timeout); 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 // would behave like set() in that case but it's OK if it doesn't -- express-session will call // set() soon enough. - async _touch(sid, sess) { + async _touch(sid: string, sess:any) { logger.debug(`TOUCH ${sid}`); sess = await this._updateExpirations(sid, sess, false); if (sess == null) return; // Already expired. diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 2cfdcfc1a..55894017a 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -35,7 +35,7 @@ const log4js = require('log4js'); const messageLogger = log4js.getLogger('message'); const accessLogger = log4js.getLogger('access'); const hooks = require('../../static/js/pluginfw/hooks.js'); -const stats = require('../stats'); +const stats = require('../stats') const assert = require('assert').strict; const {RateLimiterMemory} = require('rate-limiter-flexible'); const webaccess = require('../hooks/express/webaccess'); @@ -133,7 +133,7 @@ class Channels { 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 */ exports.setSocketIO = (socket_io) => { diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index d9e76158d..e34a16603 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -22,7 +22,7 @@ const log4js = require('log4js'); const settings = require('../utils/Settings'); -const stats = require('../stats'); +const stats = require('../../node/stats') const logger = log4js.getLogger('socket.io'); diff --git a/src/node/hooks/express.js b/src/node/hooks/express.ts similarity index 90% rename from src/node/hooks/express.js rename to src/node/hooks/express.ts index 98be763c2..b273887d6 100644 --- a/src/node/hooks/express.js +++ b/src/node/hooks/express.ts @@ -1,25 +1,31 @@ 'use strict'; -const _ = require('underscore'); -const SecretRotator = require('../security/SecretRotator'); -const cookieParser = require('cookie-parser'); -const events = require('events'); -const express = require('express'); -const expressSession = require('express-session'); -const fs = require('fs'); +import {Socket} from "node:net"; +import type {MapArrayType} from "../types/MapType"; + +import _ from 'underscore'; +// @ts-ignore +import cookieParser from 'cookie-parser'; +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 log4js = require('log4js'); +import log4js from 'log4js'; const SessionStore = require('../db/SessionStore'); const settings = require('../utils/Settings'); -const stats = require('../stats'); -const util = require('util'); +const stats = require('../stats') +import util from 'util'; const webaccess = require('./express/webaccess'); -let secretRotator = null; +import SecretRotator from '../security/SecretRotator'; + +let secretRotator: SecretRotator|null = null; const logger = log4js.getLogger('http'); -let serverName; -let sessionStore; -const sockets = new Set(); +let serverName:string; +let sessionStore: { shutdown: () => void; } | null; +const sockets:Set = new Set(); const socketsEvents = new events.EventEmitter(); const startTime = stats.settableGauge('httpStartTime'); @@ -101,7 +107,7 @@ exports.restartServer = async () => { console.log(`SSL -- server key file: ${settings.ssl.key}`); console.log(`SSL -- Certificate Authority's certificate file: ${settings.ssl.cert}`); - const options = { + const options: MapArrayType = { key: fs.readFileSync(settings.ssl.key), cert: fs.readFileSync(settings.ssl.cert), }; @@ -163,7 +169,7 @@ exports.restartServer = async () => { app.use((req, res, next) => { const stopWatch = stats.timer('httpRequests').start(); const sendFn = res.send.bind(res); - res.send = (...args) => { stopWatch.end(); sendFn(...args); }; + res.send = (...args) => { stopWatch.end(); return sendFn(...args); }; next(); }); @@ -173,7 +179,7 @@ exports.restartServer = async () => { // anyway. if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { app.use(log4js.connectLogger(logger, { - level: log4js.levels.DEBUG, + level: log4js.levels.DEBUG.levelStr, format: ':status, :method :url', })); } @@ -237,7 +243,7 @@ exports.restartServer = async () => { hooks.aCallAll('expressConfigure', {app}), hooks.aCallAll('expressCreateServer', {app, server: exports.server}), ]); - exports.server.on('connection', (socket) => { + exports.server.on('connection', (socket:Socket) => { sockets.add(socket); socketsEvents.emit('updated'); socket.on('close', () => { @@ -250,6 +256,6 @@ exports.restartServer = async () => { logger.info('HTTP server listening for connections'); }; -exports.shutdown = async (hookName, context) => { +exports.shutdown = async (hookName:string, context: any) => { await closeServer(); }; diff --git a/src/node/hooks/express/admin.js b/src/node/hooks/express/admin.ts similarity index 68% rename from src/node/hooks/express/admin.js rename to src/node/hooks/express/admin.ts index ed272efd8..90e491396 100644 --- a/src/node/hooks/express/admin.js +++ b/src/node/hooks/express/admin.ts @@ -1,6 +1,7 @@ 'use strict'; -const eejs = require('../../eejs'); +import {ArgsExpressType} from "../../types/ArgsExpressType"; +const eejs = require('../../eejs'); /** * Add the admin navigation link @@ -9,8 +10,8 @@ const eejs = require('../../eejs'); * @param {Function} cb the callback function * @return {*} */ -exports.expressCreateServer = (hookName, args, cb) => { - args.app.get('/admin', (req, res) => { +exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function): any => { + args.app.get('/admin', (req:any, res:any) => { if ('/' !== req.path[req.path.length - 1]) return res.redirect('./admin/'); res.send(eejs.require('ep_etherpad-lite/templates/admin/index.html', {req})); }); diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.ts similarity index 76% rename from src/node/hooks/express/adminplugins.js rename to src/node/hooks/express/adminplugins.ts index 19908cf59..ad1795e17 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.ts @@ -1,5 +1,11 @@ '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 settings = require('../../utils/Settings'); const installer = require('../../../static/js/pluginfw/installer'); @@ -8,8 +14,8 @@ const plugins = require('../../../static/js/pluginfw/plugins'); const semver = require('semver'); const UpdateCheck = require('../../utils/UpdateCheck'); -exports.expressCreateServer = (hookName, args, cb) => { - args.app.get('/admin/plugins', (req, res) => { +exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Function) => { + args.app.get('/admin/plugins', (req:any, res:any) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins.html', { plugins: pluginDefs.plugins, 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 epVersion = settings.getEpVersion(); @@ -36,13 +42,14 @@ exports.expressCreateServer = (hookName, args, cb) => { return cb(); }; -exports.socketio = (hookName, args, cb) => { +exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => { 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; if (!isAdmin) return; - socket.on('getInstalled', (query) => { + socket.on('getInstalled', (query:string) => { // send currently installed plugins const installed = Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package); @@ -66,13 +73,14 @@ exports.socketio = (hookName, args, cb) => { socket.emit('results:updatable', {updatable}); } 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.on('getAvailable', async (query) => { + socket.on('getAvailable', async (query:string) => { try { const results = await installer.getAvailablePlugins(/* maxCacheAge:*/ false); 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 { const results = await installer.search(query.searchTerm, /* maxCacheAge:*/ 60 * 10); let res = Object.keys(results) @@ -98,8 +106,8 @@ exports.socketio = (hookName, args, cb) => { } }); - socket.on('install', (pluginName) => { - installer.install(pluginName, (err) => { + socket.on('install', (pluginName: string) => { + installer.install(pluginName, (err: ErrorCaused) => { if (err) console.warn(err.stack || err.toString()); socket.emit('finished:install', { @@ -110,8 +118,8 @@ exports.socketio = (hookName, args, cb) => { }); }); - socket.on('uninstall', (pluginName) => { - installer.uninstall(pluginName, (err) => { + socket.on('uninstall', (pluginName:string) => { + installer.uninstall(pluginName, (err:ErrorCaused) => { if (err) console.warn(err.stack || err.toString()); 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 * @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]) { return dir ? -1 : 1; } + // @ts-ignore if (a[property] > b[property]) { return dir ? 1 : -1; } diff --git a/src/node/hooks/express/adminsettings.js b/src/node/hooks/express/adminsettings.ts similarity index 79% rename from src/node/hooks/express/adminsettings.js rename to src/node/hooks/express/adminsettings.ts index 792801dc7..900bfd479 100644 --- a/src/node/hooks/express/adminsettings.js +++ b/src/node/hooks/express/adminsettings.ts @@ -6,8 +6,8 @@ const hooks = require('../../../static/js/pluginfw/hooks'); const plugins = require('../../../static/js/pluginfw/plugins'); const settings = require('../../utils/Settings'); -exports.expressCreateServer = (hookName, {app}) => { - app.get('/admin/settings', (req, res) => { +exports.expressCreateServer = (hookName:string, {app}:any) => { + app.get('/admin/settings', (req:any, res:any) => { res.send(eejs.require('ep_etherpad-lite/templates/admin/settings.html', { req, settings: '', @@ -16,12 +16,13 @@ exports.expressCreateServer = (hookName, {app}) => { }); }; -exports.socketio = (hookName, {io}) => { - io.of('/settings').on('connection', (socket) => { +exports.socketio = (hookName:string, {io}:any) => { + io.of('/settings').on('connection', (socket: any ) => { + // @ts-ignore const {session: {user: {is_admin: isAdmin} = {}} = {}} = socket.conn.request; if (!isAdmin) return; - socket.on('load', async (query) => { + socket.on('load', async (query:string):Promise => { let data; try { 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); socket.emit('saveprogress', 'saved'); }); diff --git a/src/node/hooks/express/apicalls.js b/src/node/hooks/express/apicalls.ts similarity index 78% rename from src/node/hooks/express/apicalls.js rename to src/node/hooks/express/apicalls.ts index 5dbb57e16..91c44e389 100644 --- a/src/node/hooks/express/apicalls.js +++ b/src/node/hooks/express/apicalls.ts @@ -6,15 +6,15 @@ const {Formidable} = require('formidable'); const apiHandler = require('../../handler/APIHandler'); 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 - 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); clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`); res.end('OK'); }); - const parseJserrorForm = async (req) => { + const parseJserrorForm = async (req:any) => { const form = new Formidable({ 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 - app.post('/jserror', (req, res, next) => { + app.post('/jserror', (req:any, res:any, next:Function) => { (async () => { const data = JSON.parse(await parseJserrorForm(req)); 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. options = Object.assign({}, options, {depth: Infinity, colors: true}); return util.inspect(data, options); @@ -38,7 +38,7 @@ exports.expressPreSession = async (hookName, {app}) => { }); // 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}); }); }; diff --git a/src/node/hooks/express/errorhandling.js b/src/node/hooks/express/errorhandling.ts similarity index 56% rename from src/node/hooks/express/errorhandling.js rename to src/node/hooks/express/errorhandling.ts index 884ca9be0..2de819b0e 100644 --- a/src/node/hooks/express/errorhandling.js +++ b/src/node/hooks/express/errorhandling.ts @@ -1,12 +1,15 @@ '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; // 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 // through these "error-handling" middleware // allowing you to respond however you like diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.ts similarity index 90% rename from src/node/hooks/express/importexport.js rename to src/node/hooks/express/importexport.ts index e1adefc4c..898606e49 100644 --- a/src/node/hooks/express/importexport.js +++ b/src/node/hooks/express/importexport.ts @@ -1,5 +1,7 @@ 'use strict'; +import {ArgsExpressType} from "../../types/ArgsExpressType"; + const hasPadAccess = require('../../padaccess'); const settings = require('../../utils/Settings'); const exportHandler = require('../../handler/ExportHandler'); @@ -10,10 +12,10 @@ const rateLimit = require('express-rate-limit'); const securityManager = require('../../db/SecurityManager'); const webaccess = require('./webaccess'); -exports.expressCreateServer = (hookName, args, cb) => { +exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => { const limiter = rateLimit({ ...settings.importExportRateLimiting, - handler: (request, response, next, options) => { + handler: (request:any) => { if (request.rateLimit.current === request.rateLimit.limit + 1) { // when the rate limiter triggers, write a warning in the logs console.warn('Import/Export rate limiter triggered on ' + @@ -24,7 +26,7 @@ exports.expressCreateServer = (hookName, args, cb) => { // handle export requests 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 () => { const types = ['pdf', 'doc', 'txt', 'html', 'odt', 'etherpad']; // send a 404 if we don't support this filetype @@ -70,8 +72,9 @@ exports.expressCreateServer = (hookName, args, cb) => { // handle import requests 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 () => { + // @ts-ignore const {session: {user} = {}} = req; const {accessStatus, authorID: authorId} = await securityManager.checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.ts similarity index 90% rename from src/node/hooks/express/openapi.js rename to src/node/hooks/express/openapi.ts index 69f268256..aa2f1e483 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.ts @@ -1,5 +1,9 @@ 'use strict'; +import {OpenAPIOperations, OpenAPISuccessResponse, SwaggerUIResource} from "../../types/SwaggerUIResource"; +import {MapArrayType} from "../../types/MapType"; +import {ErrorCaused} from "../../types/ErrorCaused"; + /** * node/hooks/express/openapi.js * @@ -52,8 +56,9 @@ const APIPathStyle = { REST: 'rest', // restful paths e.g. /rest/group/create }; + // API resources - describe your API endpoints here -const resources = { +const resources:SwaggerUIResource = { // Group group: { create: { @@ -372,7 +377,7 @@ const defaultResponses = { }, }; -const defaultResponseRefs = { +const defaultResponseRefs:OpenAPISuccessResponse = { 200: { $ref: '#/components/responses/Success', }, @@ -388,16 +393,16 @@ const defaultResponseRefs = { }; // convert to a dictionary of operation objects -const operations = {}; +const operations: OpenAPIOperations = {}; for (const [resource, actions] of Object.entries(resources)) { for (const [action, spec] of Object.entries(actions)) { - const {operationId, responseSchema, ...operation} = spec; + const {operationId,responseSchema, ...operation} = spec; // add response objects - const responses = {...defaultResponseRefs}; + const responses:OpenAPISuccessResponse = {...defaultResponseRefs}; if (responseSchema) { responses[200] = cloneDeep(defaultResponses.Success); - responses[200].content['application/json'].schema.properties.data = { + responses[200].content!['application/json'].schema.properties.data = { type: 'object', 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 = { openapi: OPENAPI_VERSION, info, @@ -490,7 +495,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { // build operations for (const funcName of Object.keys(apiHandler.version[version])) { - let operation = {}; + let operation:OpenAPIOperations = {}; if (operations[funcName]) { operation = {...operations[funcName]}; } else { @@ -505,7 +510,9 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { operation.parameters = operation.parameters || []; for (const paramName of apiHandler.version[version][funcName]) { operation.parameters.push({$ref: `#/components/parameters/${paramName}`}); + // @ts-ignore if (!definition.components.parameters[paramName]) { + // @ts-ignore definition.components.parameters[paramName] = { name: paramName, in: 'query', @@ -525,6 +532,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { // add to definition // NOTE: It may be confusing that every operation can be called with both GET and POST + // @ts-ignore definition.paths[path] = { get: { ...operation, @@ -539,7 +547,7 @@ const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { 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}/* for (const version of Object.keys(apiHandler.version)) { // we support two different styles of api: flat + rest @@ -552,7 +560,7 @@ exports.expressPreSession = async (hookName, {app}) => { const definition = generateDefinitionForVersion(version, style); // 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 res.header('Access-Control-Allow-Origin', '*'); 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 const isLatestAPIVersion = version === apiHandler.latestApiVersion; 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.json({...definition, servers: [generateServerForApiVersion(apiRoot, req)]}); }); @@ -588,12 +596,12 @@ exports.expressPreSession = async (hookName, {app}) => { // register operation handlers 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 const {header, params, query} = c.request; // read form data if method was POST - let formData = {}; + let formData:MapArrayType = {}; if (c.request.method === 'post') { const form = new IncomingForm(); formData = (await form.parse(req))[0]; @@ -615,18 +623,19 @@ exports.expressPreSession = async (hookName, {app}) => { try { data = await apiHandler.handle(version, funcName, fields, req, res); } catch (err) { + const errCaused = err as ErrorCaused // convert all errors to http errors if (createHTTPError.isHttpError(err)) { // pass http errors thrown by handler forward throw err; - } else if (err.name === 'apierror') { + } else if (errCaused.name === 'apierror') { // parameters were wrong and the api stopped execution, pass the error // convert to http error - throw new createHTTPError.BadRequest(err.message); + throw new createHTTPError.BadRequest(errCaused.message); } else { // an unknown error happened // log it and throw internal error - logger.error(err.stack || err.toString()); + logger.error(errCaused.stack || errCaused.toString()); throw new createHTTPError.InternalError('internal error'); } } @@ -649,7 +658,7 @@ exports.expressPreSession = async (hookName, {app}) => { // start and bind to express api.init(); - app.use(apiRoot, async (req, res) => { + app.use(apiRoot, async (req:any, res:any) => { let response = null; try { if (style === APIPathStyle.REST) { @@ -660,31 +669,33 @@ exports.expressPreSession = async (hookName, {app}) => { // pass to openapi-backend handler response = await api.handleRequest(req, req, res); } catch (err) { + const errCaused = err as ErrorCaused // handle http errors - res.statusCode = err.statusCode || 500; + // @ts-ignore + res.statusCode = errCaused.statusCode || 500; // convert to our json response format // https://github.com/ether/etherpad-lite/tree/master/doc/api/http_api.md#response-format switch (res.statusCode) { case 403: // forbidden - response = {code: 4, message: err.message, data: null}; + response = {code: 4, message: errCaused.message, data: null}; break; 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; case 404: // not found (no such function) - response = {code: 3, message: err.message, data: null}; + response = {code: 3, message: errCaused.message, data: null}; break; case 500: // server error (internal error) - response = {code: 2, message: err.message, data: null}; + response = {code: 2, message: errCaused.message, data: null}; break; case 400: // bad request (wrong parameters) // respond with 200 OK to keep old behavior and pass tests 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; default: - response = {code: 1, message: err.message, data: null}; + response = {code: 1, message: errCaused.message, data: null}; break; } } @@ -702,7 +713,7 @@ exports.expressPreSession = async (hookName, {app}) => { * @param {APIPathStyle} style The style of the API path * @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 @@ -710,6 +721,8 @@ const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/ * @param {Request} req The express request object * @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}`, }); diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.ts similarity index 81% rename from src/node/hooks/express/padurlsanitize.js rename to src/node/hooks/express/padurlsanitize.ts index ff1afa477..8679bcfe3 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.ts @@ -1,10 +1,12 @@ 'use strict'; +import {ArgsExpressType} from "../../types/ArgsExpressType"; + 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 - args.app.param('pad', (req, res, next, padId) => { + args.app.param('pad', (req:any, res:any, next:Function, padId:string) => { (async () => { // ensure the padname is valid and the url doesn't end with a / if (!padManager.isValidPadId(padId) || /\/$/.test(req.url)) { diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.ts similarity index 94% rename from src/node/hooks/express/socketio.js rename to src/node/hooks/express/socketio.ts index edb679940..ba1a6a313 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.ts @@ -1,5 +1,7 @@ 'use strict'; +import {ArgsExpressType} from "../../types/ArgsExpressType"; + const events = require('events'); const express = require('../express'); const log4js = require('log4js'); @@ -10,7 +12,7 @@ const socketIORouter = require('../../handler/SocketIORouter'); const hooks = require('../../../static/js/pluginfw/hooks'); const padMessageHandler = require('../../handler/PadMessageHandler'); -let io; +let io:any; const logger = log4js.getLogger('socket.io'); const sockets = new Set(); const socketsEvents = new events.EventEmitter(); @@ -46,7 +48,7 @@ exports.expressCloseServer = async () => { 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 // there shouldn't be a browser that isn't compatible to all // transports in this list at once @@ -77,7 +79,7 @@ exports.expressCreateServer = (hookName, args, cb) => { maxHttpBufferSize: settings.socketIo.maxHttpBufferSize, }); - io.on('connect', (socket) => { + io.on('connect', (socket:any) => { sockets.add(socket); socketsEvents.emit('updated'); 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; // Express sets req.ip but socket.io does not. Replicate Express's behavior here. if (req.ip == null) { @@ -105,8 +107,8 @@ exports.expressCreateServer = (hookName, args, cb) => { express.sessionMiddleware(req, {}, next); }); - io.use((socket, next) => { - socket.conn.on('packet', (packet) => { + io.use((socket:any, next:Function) => { + socket.conn.on('packet', (packet:string) => { // 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 // rolling=true the cookie's expiration time will be renewed. (Note that WebSockets does not diff --git a/src/node/hooks/i18n.js b/src/node/hooks/i18n.ts similarity index 81% rename from src/node/hooks/i18n.js rename to src/node/hooks/i18n.ts index fc9f00c72..500f1f887 100644 --- a/src/node/hooks/i18n.js +++ b/src/node/hooks/i18n.ts @@ -1,5 +1,8 @@ 'use strict'; +import type {MapArrayType} from "../types/MapType"; +import {I18nPluginDefs} from "../types/I18nPluginDefs"; + const languages = require('languages4translatewiki'); const fs = require('fs'); const path = require('path'); @@ -11,17 +14,17 @@ const settings = require('../utils/Settings'); // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} const getAllLocales = () => { - const locales2paths = {}; + const locales2paths:MapArrayType = {}; // Puts the paths of all locale files contained in a given directory // into `locales2paths` (files from various dirs are grouped by lang code) // (only json files with valid language code as name) - const extractLangs = (dir) => { + const extractLangs = (dir: string) => { if (!existsSync(dir)) return; let stat = fs.lstatSync(dir); if (!stat.isDirectory() || stat.isSymbolicLink()) return; - fs.readdirSync(dir).forEach((file) => { + fs.readdirSync(dir).forEach((file:string) => { file = path.resolve(dir, file); stat = fs.lstatSync(file); if (stat.isDirectory() || stat.isSymbolicLink()) return; @@ -40,15 +43,15 @@ const getAllLocales = () => { extractLangs(path.join(settings.root, 'src/locales')); // add plugins languages (if any) - for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) { + for (const {package: {path: pluginPath}} of Object.values(pluginDefs.plugins)) { // 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')); } // Build a locale index (merge all locale data other than user-supplied overrides) - const locales = {}; - _.each(locales2paths, (files, langcode) => { + const locales:MapArrayType = {}; + _.each(locales2paths, (files: string[], langcode: string) => { locales[langcode] = {}; files.forEach((file) => { @@ -70,9 +73,9 @@ const getAllLocales = () => { 'for Customization for Administrators, under Localization.'); if (settings.customLocaleStrings) { if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr; - _.each(settings.customLocaleStrings, (overrides, langcode) => { + _.each(settings.customLocaleStrings, (overrides:MapArrayType , langcode:string) => { if (typeof overrides !== 'object') throw wrongFormatErr; - _.each(overrides, (localeString, key) => { + _.each(overrides, (localeString:string|object, key:string) => { if (typeof localeString !== 'string') throw wrongFormatErr; const locale = locales[langcode]; @@ -102,8 +105,8 @@ const getAllLocales = () => { // returns a hash of all available languages availables with nativeName and direction // e.g. { es: {nativeName: "espaƱol", direction: "ltr"}, ... } -const getAvailableLangs = (locales) => { - const result = {}; +const getAvailableLangs = (locales:MapArrayType) => { + const result:MapArrayType = {}; for (const langcode of Object.keys(locales)) { result[langcode] = languages.getLanguageInfo(langcode); } @@ -111,7 +114,7 @@ const getAvailableLangs = (locales) => { }; // returns locale index that will be served in /locales.json -const generateLocaleIndex = (locales) => { +const generateLocaleIndex = (locales:MapArrayType) => { const result = _.clone(locales); // keep English strings for (const langcode of Object.keys(locales)) { 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 const locales = getAllLocales(); const localeIndex = generateLocaleIndex(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 const locale = req.params.locale.split('.')[0]; 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('Content-Type', 'application/json; charset=utf-8'); res.send(localeIndex); diff --git a/src/node/padaccess.js b/src/node/padaccess.ts similarity index 70% rename from src/node/padaccess.js rename to src/node/padaccess.ts index e9cc7cde5..ce3cf9ddd 100644 --- a/src/node/padaccess.js +++ b/src/node/padaccess.ts @@ -2,7 +2,7 @@ const securityManager = require('./db/SecurityManager'); // 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 accessObj = await securityManager.checkAccess( req.params.pad, req.cookies.sessionID, req.cookies.token, user); diff --git a/src/node/security/SecretRotator.js b/src/node/security/SecretRotator.ts similarity index 87% rename from src/node/security/SecretRotator.js rename to src/node/security/SecretRotator.ts index 3cc08a01c..ee5bec772 100644 --- a/src/node/security/SecretRotator.js +++ b/src/node/security/SecretRotator.ts @@ -1,4 +1,7 @@ -'use strict'; + + +import {DeriveModel} from "../types/DeriveModel"; +import {LegacyParams} from "../types/LegacyParams"; const {Buffer} = require('buffer'); const crypto = require('./crypto'); @@ -6,22 +9,24 @@ const db = require('../db/DB'); const log4js = require('log4js'); class Kdf { - async generateParams() { throw new Error('not implemented'); } - async derive(params, info) { throw new Error('not implemented'); } + async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> { throw new Error('not implemented'); } + async derive(params: DeriveModel, info: any) { throw new Error('not implemented'); } } class LegacyStaticSecret extends Kdf { - async derive(params, info) { return params; } + async derive(params:any, info:any) { return params; } } class Hkdf extends Kdf { - constructor(digest, keyLen) { + private readonly _digest: string + private readonly _keyLen: number + constructor(digest:string, keyLen:number) { super(); this._digest = digest; this._keyLen = keyLen; } - async generateParams() { + async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> { const [secret, salt] = (await Promise.all([ 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}; } - async derive(p, info) { + async derive(p: DeriveModel, info:any) { return Buffer.from( 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; // In JavaScript, the % operator is remainder, not modulus. -const mod = (a, n) => ((a % n) + n) % n; -const intervalStart = (t, interval) => t - mod(t, interval); +const mod = (a:number, n:number) => ((a % n) + n) % n; +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, @@ -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 * 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 {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 * 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 * 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}; } - 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 // other instances. if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`; @@ -114,7 +127,7 @@ class SecretRotator { this._updateTimeout = null; } - async _deriveSecrets(p, now) { + async _deriveSecrets(p: any, now: number) { this._logger.debug('deriving secrets from', p); if (!p.interval) return [await algorithms[p.algId].derive(p.algParams, null)]; 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 // 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. - 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 // 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 @@ -160,12 +173,12 @@ class SecretRotator { // 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 // 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) || []; - let currentParams = null; + const dbKeys:string[] = await db.findKeys(`${this._dbPrefix}:*`, null) || []; + let currentParams:any = null; let currentId = null; - const dbWrites = []; + const dbWrites:any[] = []; const allParams = []; - const legacyParams = []; + const legacyParams:LegacyParams[] = []; await Promise.all(dbKeys.map(async (dbKey) => { const p = await db.get(dbKey); 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)) { const d = new Date(legacyEnd).toJSON(); this._logger.debug(`adding legacy static secret for ${d} with lifetime ${this._lifetime}`); - const p = { + const p: LegacyParams = { algId: 0, algParams: this._legacyStaticSecret, // 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 diff --git a/src/node/security/crypto.js b/src/node/security/crypto.ts similarity index 100% rename from src/node/security/crypto.js rename to src/node/security/crypto.ts diff --git a/src/node/server.js b/src/node/server.ts similarity index 90% rename from src/node/server.js rename to src/node/server.ts index 44f0a1e3d..76ffd3a6a 100755 --- a/src/node/server.js +++ b/src/node/server.ts @@ -24,11 +24,15 @@ * 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'); -let wtfnode; +let wtfnode: any; if (settings.dumpOnUncleanExit) { // 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. @@ -51,7 +55,7 @@ const pluginDefs = require('../static/js/pluginfw/plugin_defs'); const plugins = require('../static/js/pluginfw/plugins'); const installer = require('../static/js/pluginfw/installer'); const {Gate} = require('./utils/promises'); -const stats = require('./stats'); +const stats = require('./stats') const logger = log4js.getLogger('server'); @@ -68,14 +72,15 @@ const State = { 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. ` + `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); }; -let startDoneGate; + +let startDoneGate: { resolve: () => void; } exports.start = async () => { switch (state) { case State.INITIAL: @@ -102,15 +107,17 @@ exports.start = async () => { // Check if Etherpad version is up-to-date UpdateCheck.check(); + // @ts-ignore stats.gauge('memoryUsage', () => process.memoryUsage().rss); + // @ts-ignore stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed); - process.on('uncaughtException', (err) => { + process.on('uncaughtException', (err: ErrorCaused) => { logger.debug(`uncaught exception: ${err.stack || err}`); // eslint-disable-next-line promise/no-promise-in-callback exports.exit(err) - .catch((err) => { + .catch((err: ErrorCaused) => { logger.error('Error in process exit', err); // eslint-disable-next-line n/no-process-exit 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 // 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}`); 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 // 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 @@ -142,7 +149,7 @@ exports.start = async () => { await db.init(); await installer.checkForMigration(); 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') .map((plugin) => `${plugin.package.name}@${plugin.package.version}`) .join(', '); @@ -190,7 +197,7 @@ exports.stop = async () => { logger.info('Stopping Etherpad...'); state = State.STOPPING; try { - let timeout = null; + let timeout: NodeJS.Timeout = null as unknown as NodeJS.Timeout; await Promise.race([ hooks.aCallAll('shutdown'), new Promise((resolve, reject) => { @@ -209,15 +216,15 @@ exports.stop = async () => { stopDoneGate.resolve(); }; -let exitGate; +let exitGate: any; let exitCalled = false; -exports.exit = async (err = null) => { +exports.exit = async (err: ErrorCaused|string|null = null) => { /* eslint-disable no-process-exit */ if (err === 'SIGTERM') { // Termination from SIGTERM is not treated as an abnormal termination. logger.info('Received SIGTERM signal'); 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(err.stack || err.toString()); process.exitCode = 1; @@ -277,4 +284,6 @@ exports.exit = async (err = null) => { }; if (require.main === module) exports.start(); + +// @ts-ignore if (typeof(PhusionPassenger) !== 'undefined') exports.start(); diff --git a/src/node/stats.js b/src/node/stats.ts similarity index 92% rename from src/node/stats.js rename to src/node/stats.ts index cecaca20d..f1fc0cccf 100644 --- a/src/node/stats.js +++ b/src/node/stats.ts @@ -4,6 +4,7 @@ const measured = require('measured-core'); module.exports = measured.createCollection(); +// @ts-ignore module.exports.shutdown = async (hookName, context) => { module.exports.end(); -}; +}; \ No newline at end of file diff --git a/src/node/types/ArgsExpressType.ts b/src/node/types/ArgsExpressType.ts new file mode 100644 index 000000000..5c0675b97 --- /dev/null +++ b/src/node/types/ArgsExpressType.ts @@ -0,0 +1,5 @@ +export type ArgsExpressType = { + app:any, + io: any, + server:any +} \ No newline at end of file diff --git a/src/node/types/AsyncQueueTask.ts b/src/node/types/AsyncQueueTask.ts new file mode 100644 index 000000000..03a915ac7 --- /dev/null +++ b/src/node/types/AsyncQueueTask.ts @@ -0,0 +1,5 @@ +export type AsyncQueueTask = { + srcFile: string, + destFile: string, + type: string +} \ No newline at end of file diff --git a/src/node/types/DeriveModel.ts b/src/node/types/DeriveModel.ts new file mode 100644 index 000000000..b6297f3ce --- /dev/null +++ b/src/node/types/DeriveModel.ts @@ -0,0 +1,6 @@ +export type DeriveModel = { + digest: string, + secret: string, + salt: string, + keyLen: number +} \ No newline at end of file diff --git a/src/node/types/ErrorCaused.ts b/src/node/types/ErrorCaused.ts new file mode 100644 index 000000000..63cc677b5 --- /dev/null +++ b/src/node/types/ErrorCaused.ts @@ -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 = { + +} \ No newline at end of file diff --git a/src/node/types/I18nPluginDefs.ts b/src/node/types/I18nPluginDefs.ts new file mode 100644 index 000000000..feb9a593d --- /dev/null +++ b/src/node/types/I18nPluginDefs.ts @@ -0,0 +1,5 @@ +export type I18nPluginDefs = { + package: { + path: string + } +} \ No newline at end of file diff --git a/src/node/types/LegacyParams.ts b/src/node/types/LegacyParams.ts new file mode 100644 index 000000000..ea03c5618 --- /dev/null +++ b/src/node/types/LegacyParams.ts @@ -0,0 +1,8 @@ +export type LegacyParams = { + start: number, + end: number, + lifetime: number, + algId: number, + algParams: any, + interval:number|null +} \ No newline at end of file diff --git a/src/node/types/MapType.ts b/src/node/types/MapType.ts new file mode 100644 index 000000000..709ca0348 --- /dev/null +++ b/src/node/types/MapType.ts @@ -0,0 +1,7 @@ +export type MapType = { + [key: string|number]: string|number +} + +export type MapArrayType = { + [key:string]: T +} \ No newline at end of file diff --git a/src/node/types/PadType.ts b/src/node/types/PadType.ts new file mode 100644 index 000000000..eab10d905 --- /dev/null +++ b/src/node/types/PadType.ts @@ -0,0 +1,16 @@ +export type PadType = { + apool: ()=>APool, + atext: AText, + getInternalRevisionAText: (text:string)=>Promise +} + + +type APool = { + putAttrib: ([],flag: boolean)=>number +} + + +export type AText = { + text: string, + attribs: any +} \ No newline at end of file diff --git a/src/node/types/Plugin.ts b/src/node/types/Plugin.ts new file mode 100644 index 000000000..44b97922c --- /dev/null +++ b/src/node/types/Plugin.ts @@ -0,0 +1,9 @@ +'use strict'; + + +export type PluginType = { + package: { + name: string, + version: string + } +} \ No newline at end of file diff --git a/src/node/types/PromiseWithStd.ts b/src/node/types/PromiseWithStd.ts new file mode 100644 index 000000000..426fcbe54 --- /dev/null +++ b/src/node/types/PromiseWithStd.ts @@ -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 \ No newline at end of file diff --git a/src/node/types/QueryType.ts b/src/node/types/QueryType.ts new file mode 100644 index 000000000..f851c6534 --- /dev/null +++ b/src/node/types/QueryType.ts @@ -0,0 +1,3 @@ +export type QueryType = { +searchTerm: string; sortBy: string; sortDir: string; offset: number; limit: number; +} \ No newline at end of file diff --git a/src/node/types/RunCMDOptions.ts b/src/node/types/RunCMDOptions.ts new file mode 100644 index 000000000..74298f221 --- /dev/null +++ b/src/node/types/RunCMDOptions.ts @@ -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 +} \ No newline at end of file diff --git a/src/node/types/SecretRotatorType.ts b/src/node/types/SecretRotatorType.ts new file mode 100644 index 000000000..2c0f05f15 --- /dev/null +++ b/src/node/types/SecretRotatorType.ts @@ -0,0 +1,3 @@ +export type SecretRotatorType = { + stop: ()=>void +} \ No newline at end of file diff --git a/src/node/types/SwaggerUIResource.ts b/src/node/types/SwaggerUIResource.ts new file mode 100644 index 000000000..3f61f9ba8 --- /dev/null +++ b/src/node/types/SwaggerUIResource.ts @@ -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 +} \ No newline at end of file diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.ts similarity index 72% rename from src/node/utils/Abiword.js rename to src/node/utils/Abiword.ts index 1ed487ae1..c0937fcd9 100644 --- a/src/node/utils/Abiword.js +++ b/src/node/utils/Abiword.ts @@ -19,6 +19,9 @@ * limitations under the License. */ +import {ChildProcess} from "node:child_process"; +import {AsyncQueueTask} from "../types/AsyncQueueTask"; + const spawn = require('child_process').spawn; const async = require('async'); const settings = require('./Settings'); @@ -27,13 +30,13 @@ const os = require('os'); // on windows we have to spawn a process for each convertion, // cause the plugin abicommand doesn't exist on this platform 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]); let stdoutBuffer = ''; - abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); }); - abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); - await new Promise((resolve, reject) => { - abiword.on('exit', (code) => { + abiword.stdout.on('data', (data: string) => { stdoutBuffer += data.toString(); }); + abiword.stderr.on('data', (data: string) => { stdoutBuffer += data.toString(); }); + await new Promise((resolve, reject) => { + abiword.on('exit', (code: number) => { if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`)); if (stdoutBuffer !== '') { console.log(stdoutBuffer); @@ -46,13 +49,13 @@ if (os.type().indexOf('Windows') > -1) { // communicate with it via stdin/stdout // thats much faster, about factor 10 } else { - let abiword; - let stdoutCallback = null; + let abiword: ChildProcess; + let stdoutCallback: Function|null = null; const spawnAbiword = () => { abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); let stdoutBuffer = ''; let firstPrompt = true; - abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); }); + abiword.stderr!.on('data', (data) => { stdoutBuffer += data.toString(); }); abiword.on('exit', (code) => { spawnAbiword(); if (stdoutCallback != null) { @@ -60,7 +63,7 @@ if (os.type().indexOf('Windows') > -1) { stdoutCallback = null; } }); - abiword.stdout.on('data', (data) => { + abiword.stdout!.on('data', (data) => { stdoutBuffer += data.toString(); // we're searching for the prompt, cause this means everything we need is in the buffer if (stdoutBuffer.search('AbiWord:>') !== -1) { @@ -76,15 +79,15 @@ if (os.type().indexOf('Windows') > -1) { }; spawnAbiword(); - const queue = async.queue((task, callback) => { - abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`); - stdoutCallback = (err) => { + const queue = async.queue((task: AsyncQueueTask, callback:Function) => { + abiword.stdin!.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`); + stdoutCallback = (err: string) => { if (err != null) console.error('Abiword File failed to convert', err); callback(err); }; }, 1); - exports.convertFile = async (srcFile, destFile, type) => { + exports.convertFile = async (srcFile: string, destFile: string, type: string) => { await queue.pushAsync({srcFile, destFile, type}); }; } diff --git a/src/node/utils/AbsolutePaths.js b/src/node/utils/AbsolutePaths.ts similarity index 94% rename from src/node/utils/AbsolutePaths.js rename to src/node/utils/AbsolutePaths.ts index 73a96bb67..c257440a1 100644 --- a/src/node/utils/AbsolutePaths.js +++ b/src/node/utils/AbsolutePaths.ts @@ -18,7 +18,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - const log4js = require('log4js'); const path = require('path'); const _ = require('underscore'); @@ -29,7 +28,7 @@ const absPathLogger = log4js.getLogger('AbsolutePaths'); * findEtherpadRoot() computes its value only on first invocation. * 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, @@ -41,7 +40,7 @@ let etherpadRoot = null; * @return {string[]|boolean} The shortened array, or false if there was no * overlap. */ -const popIfEndsWith = (stringArray, lastDesiredElements) => { +const popIfEndsWith = (stringArray: string[], lastDesiredElements: string[]): string[] | false => { if (stringArray.length <= lastDesiredElements.length) { absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" ` + `from "${stringArray.join(path.sep)}", it should contain at least ` + @@ -131,7 +130,7 @@ exports.findEtherpadRoot = () => { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = (somePath) => { +exports.makeAbsolute = (somePath: string) => { if (path.isAbsolute(somePath)) { return somePath; } @@ -150,10 +149,8 @@ exports.makeAbsolute = (somePath) => { * a subdirectory of the base one * @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 const relative = path.relative(parent, arbitraryDir); - const isSubdir = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); - - return isSubdir; + return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); }; diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.ts similarity index 96% rename from src/node/utils/ExportEtherpad.js rename to src/node/utils/ExportEtherpad.ts index e20739ad3..292fbcec4 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.ts @@ -21,13 +21,13 @@ const authorManager = require('../db/AuthorManager'); const hooks = require('../../static/js/pluginfw/hooks'); const padManager = require('../db/PadManager'); -exports.getPadRaw = async (padId, readOnlyId) => { +exports.getPadRaw = async (padId:string, readOnlyId:string) => { const dstPfx = `pad:${readOnlyId || padId}`; const [pad, customPrefixes] = await Promise.all([ padManager.getPad(padId), 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 dstPfx = `${customPrefix}:${readOnlyId || padId}`; assert(!srcPfx.includes('*')); diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.ts similarity index 86% rename from src/node/utils/ExportHelper.js rename to src/node/utils/ExportHelper.ts index 48054e7f4..f3a438e86 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.ts @@ -26,7 +26,7 @@ const { checkValidRev } = require('./checkValidRev'); /* * 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 atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext); const textLines = atext.text.slice(0, -1).split('\n'); @@ -47,10 +47,12 @@ exports.getPadPlainText = (pad, revNum) => { return pieces.join(''); }; +type LineModel = { + [id:string]:string|number|LineModel +} - -exports._analyzeLine = (text, aline, apool) => { - const line = {}; +exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => { + const line: LineModel = {}; // identify list let lineMarker = 0; @@ -86,4 +88,4 @@ exports._analyzeLine = (text, aline, apool) => { 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)};`); diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.ts similarity index 92% rename from src/node/utils/ExportTxt.js rename to src/node/utils/ExportTxt.ts index 9511dd0e7..95e8b0456 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.ts @@ -19,13 +19,16 @@ * limitations under the License. */ +import {AText, PadType} from "../types/PadType"; +import {MapType} from "../types/MapType"; + const Changeset = require('../../static/js/Changeset'); const attributes = require('../../static/js/attributes'); const padManager = require('../db/PadManager'); const _analyzeLine = require('./ExportHelper')._analyzeLine; // 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; 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 // 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 textLines = atext.text.slice(0, -1).split('\n'); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; - const anumMap = {}; + const anumMap: MapType = {}; const css = ''; props.forEach((propName, i) => { @@ -55,8 +58,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => { } }); - const getLineTXT = (text, attribs) => { - const propVals = [false, false, false]; + const getLineTXT = (text:string, attribs:any) => { + const propVals:(number|boolean)[] = [false, false, false]; const ENTER = 1; const STAY = 2; const LEAVE = 0; @@ -71,7 +74,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { let idx = 0; - const processNextChars = (numChars) => { + const processNextChars = (numChars: number) => { if (numChars <= 0) { return; } @@ -84,7 +87,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { for (const a of attributes.decodeAttribString(o.attribs)) { 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]) { propVals[i] = ENTER; @@ -189,7 +192,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation - const listNumbers = {}; + const listNumbers:MapType = {}; let prevListLevel; for (let i = 0; i < textLines.length; i++) { @@ -233,6 +236,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { delete listNumbers[prevListLevel]; } + // @ts-ignore listNumbers[line.listLevel]++; if (line.listLevel > 1) { let x = 1; @@ -258,7 +262,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { exports.getTXTFromAtext = getTXTFromAtext; -exports.getPadTXTDocument = async (padId, revNum) => { +exports.getPadTXTDocument = async (padId:string, revNum:string) => { const pad = await padManager.getPad(padId); return getPadTXT(pad, revNum); }; diff --git a/src/node/utils/Stream.js b/src/node/utils/Stream.ts similarity index 93% rename from src/node/utils/Stream.js rename to src/node/utils/Stream.ts index 611b83b33..36fde1ac7 100644 --- a/src/node/utils/Stream.js +++ b/src/node/utils/Stream.ts @@ -5,17 +5,19 @@ * objects lack. */ class Stream { + private _iter + private _next: any /** * @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; })()); } /** * @param {Iterable} values - Any iterable of values. */ - constructor(values) { + constructor(values: Iterable) { this._iter = values[Symbol.iterator](); this._next = null; } @@ -52,10 +54,11 @@ class Stream { * @param {number} size - The number of values to read at a time. * @returns {Stream} A new Stream that gets its values from this Stream. */ - batch(size) { + batch(size: number) { return new Stream((function* () { const b = []; try { + // @ts-ignore for (const v of this) { Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors. b.push(v); @@ -100,10 +103,11 @@ class Stream { * @param {number} capacity - The number of values to keep buffered. * @returns {Stream} A new Stream that gets its values from this Stream. */ - buffer(capacity) { + buffer(capacity: number) { return new Stream((function* () { const b = []; try { + // @ts-ignore for (const v of this) { Promise.resolve(v).catch(() => {}); // Suppress unhandled rejection errors. // Note: V8 has good Array push+shift optimization. @@ -123,7 +127,8 @@ class Stream { * @param {(v: any) => any} fn - Value transformation function. * @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. diff --git a/src/node/utils/UpdateCheck.js b/src/node/utils/UpdateCheck.ts similarity index 79% rename from src/node/utils/UpdateCheck.js rename to src/node/utils/UpdateCheck.ts index 9290380d8..193a40a98 100644 --- a/src/node/utils/UpdateCheck.js +++ b/src/node/utils/UpdateCheck.ts @@ -6,9 +6,14 @@ const headers = { 'User-Agent': 'Etherpad/' + settings.getEpVersion(), } +type Infos = { + latestVersion: string +} + + const updateInterval = 60 * 60 * 1000; // 1 hour -let infos; -let lastLoadingTime = null; +let infos: Infos; +let lastLoadingTime: number | null = null; const loadEtherpadInformations = () => { if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) { @@ -16,7 +21,7 @@ const loadEtherpadInformations = () => { } return axios.get('https://static.etherpad.org/info.json', {headers: headers}) - .then(async resp => { + .then(async (resp: any) => { infos = await resp.data; if (infos === undefined || infos === null) { await Promise.reject("Could not retrieve current version") @@ -26,7 +31,7 @@ const loadEtherpadInformations = () => { lastLoadingTime = Date.now(); return await Promise.resolve(infos); }) - .catch(async err => { + .catch(async (err: Error) => { return await Promise.reject(err); }); } @@ -37,20 +42,20 @@ exports.getLatestVersion = () => { return infos?.latestVersion; }; -exports.needsUpdate = async (cb) => { +exports.needsUpdate = async (cb: Function) => { await loadEtherpadInformations() - .then((info) => { + .then((info:Infos) => { if (semver.gt(info.latestVersion, settings.getEpVersion())) { if (cb) return cb(true); } - }).catch((err) => { + }).catch((err: Error) => { console.error(`Can not perform Etherpad update check: ${err}`); if (cb) return cb(false); }); }; exports.check = () => { - exports.needsUpdate((needsUpdate) => { + exports.needsUpdate((needsUpdate: boolean) => { if (needsUpdate) { console.warn(`Update available: Download the actual version ${infos.latestVersion}`); } diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.ts similarity index 83% rename from src/node/utils/caching_middleware.js rename to src/node/utils/caching_middleware.ts index 3cc4daf27..d5866b019 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.ts @@ -36,32 +36,38 @@ const util = require('util'); * */ -// MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - START -let _crypto; -try { - _crypto = require('crypto'); -} catch { - _crypto = undefined; -} +const _crypto = require('crypto'); + let CACHE_DIR = path.join(settings.root, 'var/'); 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)); return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`; }; const generateCacheKeyWithSha256 = - (path) => _crypto.createHash('sha256').update(path).digest('hex'); + (path: string) => _crypto.createHash('sha256').update(path).digest('hex'); 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) { generateCacheKey = generateCacheKeyWithSha256; @@ -79,17 +85,17 @@ if (_crypto) { */ 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))); } - async _handle(req, res, next) { + async _handle(req: any, res: any, next: any) { if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) { return next(undefined, req, res); } - const oldReq = {}; - const oldRes = {}; + const oldReq:ResponseCache = {}; + const oldRes:ResponseCache = {}; const supportsGzip = (req.get('Accept-Encoding') || '').indexOf('gzip') !== -1; @@ -119,7 +125,7 @@ module.exports = class CachingMiddleware { res.write = oldRes.write || res.write; res.end = oldRes.end || res.end; - const headers = {}; + const headers: Headers = {}; Object.assign(headers, (responseCache[cacheKey].headers || {})); const statusCode = responseCache[cacheKey].statusCode; @@ -150,18 +156,19 @@ module.exports = class CachingMiddleware { return respond(); } - const _headers = {}; + const _headers:Headers = {}; oldRes.setHeader = res.setHeader; - res.setHeader = (key, value) => { + res.setHeader = (key: string, value: string) => { // Don't set cookies, see issue #707 if (key.toLowerCase() === 'set-cookie') return; _headers[key.toLowerCase()] = value; + // @ts-ignore oldRes.setHeader.call(res, key, value); }; oldRes.writeHead = res.writeHead; - res.writeHead = (status, headers) => { + res.writeHead = (status: number, headers: Headers) => { res.writeHead = oldRes.writeHead; if (status === 200) { // Update cache @@ -174,14 +181,14 @@ module.exports = class CachingMiddleware { oldRes.write = res.write; oldRes.end = res.end; - res.write = (data, encoding) => { + res.write = (data: number, encoding: number) => { buffer += data.toString(encoding); }; - res.end = async (data, encoding) => { + res.end = async (data: number, encoding: number) => { await Promise.all([ fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}), 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(() => {}), ]); responseCache[cacheKey] = {statusCode: status, headers}; @@ -191,8 +198,8 @@ module.exports = class CachingMiddleware { // Nothing new changed from the cached version. oldRes.write = res.write; oldRes.end = res.end; - res.write = (data, encoding) => {}; - res.end = (data, encoding) => { respond(); }; + res.write = (data: number, encoding: number) => {}; + res.end = (data: number, encoding: number) => { respond(); }; } else { res.writeHead(status, headers); } diff --git a/src/node/utils/checkValidRev.js b/src/node/utils/checkValidRev.ts similarity index 81% rename from src/node/utils/checkValidRev.js rename to src/node/utils/checkValidRev.ts index 862c6a2bd..5367ddf99 100644 --- a/src/node/utils/checkValidRev.js +++ b/src/node/utils/checkValidRev.ts @@ -4,7 +4,7 @@ const CustomError = require('../utils/customError'); // checks if a rev is a legal number // pre-condition is that `rev` is not undefined -const checkValidRev = (rev) => { +const checkValidRev = (rev: number|string) => { if (typeof rev !== 'number') { rev = parseInt(rev, 10); } @@ -28,7 +28,7 @@ const checkValidRev = (rev) => { }; // 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.checkValidRev = checkValidRev; diff --git a/src/node/utils/customError.js b/src/node/utils/customError.ts similarity index 85% rename from src/node/utils/customError.js rename to src/node/utils/customError.ts index 24ad181e6..c58360269 100644 --- a/src/node/utils/customError.js +++ b/src/node/utils/customError.ts @@ -10,11 +10,11 @@ class CustomError extends Error { /** * Creates an instance of CustomError. - * @param {*} message + * @param {string} message * @param {string} [name='Error'] a custom name for the error object * @memberof CustomError */ - constructor(message, name = 'Error') { + constructor(message:string, name: string = 'Error') { super(message); this.name = name; Error.captureStackTrace(this, this.constructor); diff --git a/src/node/utils/path_exists.js b/src/node/utils/path_exists.ts similarity index 65% rename from src/node/utils/path_exists.js rename to src/node/utils/path_exists.ts index 0b4c8fe94..354cd3cc7 100644 --- a/src/node/utils/path_exists.js +++ b/src/node/utils/path_exists.ts @@ -1,8 +1,8 @@ 'use strict'; const fs = require('fs'); -const check = (path) => { - const existsSync = fs.statSync || fs.existsSync || path.existsSync; +const check = (path:string) => { + const existsSync = fs.statSync || fs.existsSync; let result; try { diff --git a/src/node/utils/promises.js b/src/node/utils/promises.ts similarity index 89% rename from src/node/utils/promises.js rename to src/node/utils/promises.ts index bc9f8c2dc..701c5da89 100644 --- a/src/node/utils/promises.js +++ b/src/node/utils/promises.ts @@ -7,14 +7,16 @@ // `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 // the predicate. -exports.firstSatisfies = (promises, predicate) => { - if (predicate == null) predicate = (x) => x; +exports.firstSatisfies = (promises: Promise[], predicate: null|Function) => { + if (predicate == null) { + predicate = (x: any) => x; + } // 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, // yielding the first resolved value that satisfies `predicate`. - const newPromises = promises.map( - (p) => new Promise((resolve, reject) => p.then((v) => predicate(v) && resolve(v), reject))); + const newPromises = promises.map((p) => + 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 // `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, // and each remaining Promise will be created once one of the earlier Promises resolves.) This async // 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'); let next = 0; 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 * properties. */ -class Gate extends Promise { +class Gate extends Promise { // Coax `.then()` into returning an ordinary Promise, not a Gate. See // https://stackoverflow.com/a/65669070 for the rationale. 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 // do the following because it will throw a ReferenceError when it dereferences `this`: // super((resolve, reject) => Object.assign(this, {resolve, reject})); - let props; + let props: any; super((resolve, reject) => props = {resolve, reject}); Object.assign(this, props); } diff --git a/src/node/utils/randomstring.js b/src/node/utils/randomstring.ts similarity index 59% rename from src/node/utils/randomstring.js rename to src/node/utils/randomstring.ts index 4ffd3e8ae..a86d28566 100644 --- a/src/node/utils/randomstring.js +++ b/src/node/utils/randomstring.ts @@ -3,8 +3,8 @@ * Generates a random String with the given length. Is needed to generate the * 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; diff --git a/src/node/utils/run_cmd.js b/src/node/utils/run_cmd.ts similarity index 83% rename from src/node/utils/run_cmd.js rename to src/node/utils/run_cmd.ts index bf5515c84..463b0f076 100644 --- a/src/node/utils/run_cmd.js +++ b/src/node/utils/run_cmd.ts @@ -1,5 +1,10 @@ '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 log4js = require('log4js'); const path = require('path'); @@ -7,12 +12,12 @@ const settings = require('./Settings'); const logger = log4js.getLogger('runCmd'); -const logLines = (readable, logLineFn) => { - readable.setEncoding('utf8'); +const logLines = (readable: undefined | Readable | null, logLineFn: (arg0: (string | undefined)) => void) => { + readable!.setEncoding('utf8'); // 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. - let leftovers = ''; - readable.on('data', (chunk) => { + let leftovers: string| undefined = ''; + readable!.on('data', (chunk) => { const lines = chunk.split('\n'); if (lines.length === 0) return; lines[0] = leftovers + lines[0]; @@ -21,7 +26,7 @@ const logLines = (readable, logLineFn) => { logLineFn(line); } }); - readable.on('end', () => { + readable!.on('end', () => { if (leftovers !== '') logLineFn(leftovers); leftovers = ''; }); @@ -69,7 +74,7 @@ const logLines = (readable, logLineFn) => { * - `stderr`: Similar to `stdout` but for stderr. * - `child`: The ChildProcess object. */ -module.exports = exports = (args, opts = {}) => { +module.exports = exports = (args: string[], opts:RunCMDOptions = {}) => { logger.debug(`Executing command: ${args.join(' ')}`); opts = {cwd: settings.root, ...opts}; @@ -82,8 +87,8 @@ module.exports = exports = (args, opts = {}) => { : opts.stdio === 'string' ? [null, 'string', 'string'] : Array(3).fill(opts.stdio); const cmdLogger = log4js.getLogger(`runCmd|${args[0]}`); - if (stdio[1] == null) stdio[1] = (line) => cmdLogger.info(line); - if (stdio[2] == null) stdio[2] = (line) => cmdLogger.error(line); + if (stdio[1] == null) stdio[1] = (line: string) => cmdLogger.info(line); + if (stdio[2] == null) stdio[2] = (line: string) => cmdLogger.error(line); const stdioLoggers = []; const stdioSaveString = []; 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 // 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 streams = [undefined, proc.stdout, proc.stderr]; + const proc: ChildProcess = spawn(args[0], args.slice(1), opts); + const streams:[undefined, Readable|null, Readable|null] = [undefined, proc.stdout, proc.stderr]; - let px; - const p = new Promise((resolve, reject) => { px = {resolve, reject}; }); + let px: { reject: any; resolve: any; }; + const p:PromiseWithStd = new Promise((resolve, reject) => { px = {resolve, reject}; }); [, p.stdout, p.stderr] = streams; p.child = proc; @@ -132,9 +137,10 @@ module.exports = exports = (args, opts = {}) => { if (stdioLoggers[fd] != null) { logLines(streams[fd], stdioLoggers[fd]); } else if (stdioSaveString[fd]) { + // @ts-ignore p[[null, 'stdout', 'stderr'][fd]] = stdioStringPromises[fd] = (async () => { 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, ''); })(); } diff --git a/src/node/utils/sanitizePathname.js b/src/node/utils/sanitizePathname.ts similarity index 96% rename from src/node/utils/sanitizePathname.js rename to src/node/utils/sanitizePathname.ts index 61b611166..2932b913d 100644 --- a/src/node/utils/sanitizePathname.js +++ b/src/node/utils/sanitizePathname.ts @@ -4,7 +4,7 @@ const path = require('path'); // 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. -module.exports = (p, pathApi = path) => { +module.exports = (p: string, pathApi = path) => { // 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 // not be the same thing as 'b'. Most path normalization functions from other libraries (e.g., diff --git a/src/node/utils/toolbar.js b/src/node/utils/toolbar.js deleted file mode 100644 index 40a476878..000000000 --- a/src/node/utils/toolbar.js +++ /dev/null @@ -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}`; - } else { - return `<${name}${aStr}>`; - } -}; - -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()); - }, -}; diff --git a/src/node/utils/toolbar.ts b/src/node/utils/toolbar.ts new file mode 100644 index 000000000..aac3fb3d3 --- /dev/null +++ b/src/node/utils/toolbar.ts @@ -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}`; + } else { + return `<${name}${aStr}>`; + } +}; + + +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()); + }, +}; diff --git a/src/package-lock.json b/src/package-lock.json index 1b2e08da4..9f1e624f8 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,6 +1,6 @@ { "name": "ep_etherpad-lite", - "version": "1.9.7", + "version": "1.9.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -22,6 +22,154 @@ "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": { "version": "4.4.0", "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==", "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": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -269,6 +442,30 @@ "@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": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -285,6 +482,12 @@ "@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": { "version": "7.0.13", "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==" }, "@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" + "version": "4.14.199", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", + "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==" }, "@types/lodash.clonedeep": { - "version": "4.5.9", - "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", - "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.7.tgz", + "integrity": "sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==", "requires": { "@types/lodash": "*" } @@ -322,15 +525,21 @@ "@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": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", "requires": { "undici-types": "~5.26.4" } @@ -344,11 +553,44 @@ "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": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", "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": { "version": "6.1.10", "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.10.tgz", @@ -358,6 +600,12 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz", @@ -767,9 +1015,9 @@ "dev": true }, "axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "requires": { "follow-redirects": "^1.15.4", "form-data": "^4.0.0", @@ -806,6 +1054,14 @@ "resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz", "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": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1082,6 +1338,15 @@ "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": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", @@ -1412,6 +1677,36 @@ "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": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2249,7 +2544,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "optional": true }, "function-bind": { @@ -2306,7 +2600,6 @@ "version": "4.7.2", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", - "dev": true, "requires": { "resolve-pkg-maps": "^1.0.0" } @@ -2963,10 +3256,11 @@ } }, "jsdom": { - "version": "24.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", - "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", + "version": "23.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz", + "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==", "requires": { + "@asamuzakjp/dom-selector": "^2.0.1", "cssstyle": "^4.0.1", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", @@ -2975,7 +3269,6 @@ "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.7", "parse5": "^7.1.2", "rrweb-cssom": "^0.6.0", "saxes": "^6.0.0", @@ -3263,6 +3556,11 @@ "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": { "version": "2.0.0", "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": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -6984,9 +7277,9 @@ } }, "openapi-backend": { - "version": "5.10.6", - "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-5.10.6.tgz", - "integrity": "sha512-vTjBRys/O4JIHdlRHUKZ7pxS+gwIJreAAU9dvYRFrImtPzQ5qxm5a6B8BTVT9m6I8RGGsShJv35MAc3Tu2/y/A==", + "version": "5.10.5", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-5.10.5.tgz", + "integrity": "sha512-ivZfL0Lwj7rRctCqxAquGy4j/VcdUXUvDsEVM3NG/2jDuvYT2dS+sf9ntGo5vv4hkOnkWgPnR6HxHp7NPexqAA==", "requires": { "@apidevtools/json-schema-ref-parser": "^11.1.0", "ajv": "^8.6.2", @@ -7243,9 +7536,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "rate-limiter-flexible": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-4.0.1.tgz", - "integrity": "sha512-2/dGHpDFpeA0+755oUkW+EKyklqLS9lu0go9pDsbhqQjZcxfRyJ6LA4JI0+HAdZ2bemD/oOjUeZQB2lCZqXQfQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-4.0.0.tgz", + "integrity": "sha512-SkA18LEPqJJKHixi6E7tzBKTXbj9gu5wPyfTykPVRZR5JGSw0dMCjtZsjlfuabVY940pu28Wu87NZN4FhztnyQ==" }, "raw-body": { "version": "2.5.1", @@ -7385,8 +7678,7 @@ "resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==" }, "reusify": { "version": "1.0.4", @@ -7477,9 +7769,9 @@ "integrity": "sha512-5qfoAgfRWS1sUn+fUJtdbbqM1BD/LoQGa+smPTDjf9OqHyuJqi6ewtbYL0+V1S1RaU6OCOCMWGZocIfz2YK4uw==" }, "selenium-webdriver": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.17.0.tgz", - "integrity": "sha512-e2E+2XBlGepzwgFbyQfSwo9Cbj6G5fFfs9MzAS00nC99EewmcS2rwn2MwtgfP7I5p1e7DYv4HQJXtWedsu6DvA==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.16.0.tgz", + "integrity": "sha512-IbqpRpfGE7JDGgXHJeWuCqT/tUqnLvZ14csSwt+S8o4nJo3RtQoE9VR4jB47tP/A8ArkYsh/THuMY6kyRP6kuA==", "dev": true, "requires": { "jszip": "^3.10.1", @@ -7742,6 +8034,11 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "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": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -8148,6 +8445,16 @@ "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": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8232,9 +8539,9 @@ "dev": true }, "ueberdb2": { - "version": "4.2.50", - "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-4.2.50.tgz", - "integrity": "sha512-XOiWxmDHhoCNLSrepIJa+kdRBXABuI5ZXQhXpmT2Z8qK3EbipcrAeQBWDQXfcnnHftmzaR3NY7zil76biCN/GQ==" + "version": "4.2.48", + "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-4.2.48.tgz", + "integrity": "sha512-kazgi/wodwsPZGo7EBfAjm17HvzXb4RDau1NocRoWCwkjK7hz5t+5QkoCgeTPMSr7uEpRSmVVmci3dYsLsaB5g==" }, "uid-safe": { "version": "2.1.5", @@ -8511,9 +8818,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "dev": true }, "wtfnode": { diff --git a/src/package.json b/src/package.json index 77b313f2b..1edbaa502 100644 --- a/src/package.json +++ b/src/package.json @@ -31,7 +31,7 @@ ], "dependencies": { "async": "^3.2.5", - "axios": "^1.6.7", + "axios": "^1.6.5", "clean-css": "^5.3.3", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", @@ -46,7 +46,7 @@ "formidable": "^3.5.1", "http-errors": "^2.0.0", "js-cookie": "^3.0.5", - "jsdom": "^24.0.0", + "jsdom": "^23.2.0", "jsonminify": "0.4.2", "languages4translatewiki": "0.1.3", "live-plugin-manager": "^0.18.1", @@ -55,9 +55,9 @@ "measured-core": "^2.0.0", "mime-types": "^2.1.35", "npm": "^6.14.18", - "openapi-backend": "^5.10.6", + "openapi-backend": "^5.10.5", "proxy-addr": "^2.0.7", - "rate-limiter-flexible": "^4.0.1", + "rate-limiter-flexible": "^4.0.0", "rehype": "^13.0.1", "rehype-minify-whitespace": "^6.0.0", "resolve": "1.22.8", @@ -68,17 +68,21 @@ "terser": "^5.27.0", "threads": "^1.7.0", "tinycon": "0.6.8", - "ueberdb2": "^4.2.50", + "tsx": "^4.7.0", + "ueberdb2": "^4.2.48", "underscore": "1.13.6", "unorm": "1.6.0", "wtfnode": "^0.9.1" }, "bin": { - "etherpad": "node/server.js", "etherpad-healthcheck": "bin/etherpad-healthcheck", - "etherpad-lite": "node/server.js" + "etherpad-lite": "node/server.ts" }, "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-config-etherpad": "^3.0.22", "etherpad-cli-client": "^2.0.2", @@ -86,7 +90,7 @@ "mocha-froth": "^0.2.10", "nodeify": "^1.0.1", "openapi-schema-validation": "^0.4.2", - "selenium-webdriver": "^4.17.0", + "selenium-webdriver": "^4.16.0", "set-cookie-parser": "^2.6.0", "sinon": "^17.0.1", "split-grid": "^1.0.11", @@ -103,9 +107,13 @@ }, "scripts": { "lint": "eslint .", - "test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", - "test-container": "mocha --timeout 5000 tests/container/specs/api" + "test": "mocha --import=tsx --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs", + "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" } diff --git a/src/tests/backend/specs/SecretRotator.js b/src/tests/backend/specs/SecretRotator.js index 1831ef71f..9436e2876 100644 --- a/src/tests/backend/specs/SecretRotator.js +++ b/src/tests/backend/specs/SecretRotator.js @@ -1,10 +1,10 @@ 'use strict'; -const SecretRotator = require('../../../node/security/SecretRotator'); const assert = require('assert').strict; const common = require('../common'); const crypto = require('../../../node/security/crypto'); const db = require('../../../node/db/DB'); +const SecretRotator = require("../../../node/security/SecretRotator").SecretRotator; const logger = common.logger; diff --git a/src/tests/frontend/cypress/cypress.config.js b/src/tests/frontend/cypress/cypress.config.js index c49b90857..3754350de 100644 --- a/src/tests/frontend/cypress/cypress.config.js +++ b/src/tests/frontend/cypress/cypress.config.js @@ -4,6 +4,6 @@ module.exports = defineConfig({ e2e: { baseUrl: "http://127.0.0.1:9001", supportFile: false, - specPattern: 'src/tests/frontend/cypress/integration/**/*.js' + specPattern: 'tests/frontend/cypress/integration/**/*.js' } }) diff --git a/src/tests/frontend/travis/adminrunner.sh b/src/tests/frontend/travis/adminrunner.sh index 9ed6d5e74..32fd12a63 100755 --- a/src/tests/frontend/travis/adminrunner.sh +++ b/src/tests/frontend/travis/adminrunner.sh @@ -11,8 +11,8 @@ MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 try cd "${MY_DIR}/../../../.." log "Assuming src/bin/installDeps.sh has already been run" -node src/node/server.js --experimental-worker "${@}" & -ep_pid=$! +( cd src && npm run dev --experimental-worker "${@}" & +ep_pid=$!) log "Waiting for Etherpad to accept connections (http://localhost:9001)..." connected=false diff --git a/src/tests/frontend/travis/runner.sh b/src/tests/frontend/travis/runner.sh index 23bc13ed7..7796bc2ef 100755 --- a/src/tests/frontend/travis/runner.sh +++ b/src/tests/frontend/travis/runner.sh @@ -11,8 +11,8 @@ MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1 try cd "${MY_DIR}/../../../.." log "Assuming src/bin/installDeps.sh has already been run" -node src/node/server.js --experimental-worker "${@}" & -ep_pid=$! +(cd src && npm run dev --experimental-worker "${@}" & +ep_pid=$!) log "Waiting for Etherpad to accept connections (http://localhost:9001)..." connected=false diff --git a/src/tests/frontend/travis/runnerBackend.sh b/src/tests/frontend/travis/runnerBackend.sh index f12ff25c1..518e77872 100755 --- a/src/tests/frontend/travis/runnerBackend.sh +++ b/src/tests/frontend/travis/runnerBackend.sh @@ -19,8 +19,8 @@ s!"points":[^,]*!"points": 1000! log "Deprecation notice: runnerBackend.sh - Please use: cd src && npm test" log "Assuming src/bin/installDeps.sh has already been run" -node src/node/server.js "${@}" & -ep_pid=$! +(cd src && npm run dev "${@}" & +ep_pid=$!) log "Waiting for Etherpad to accept connections (http://localhost:9001)..." connected=false diff --git a/src/tests/frontend/travis/runnerLoadTest.sh b/src/tests/frontend/travis/runnerLoadTest.sh index 377d8e3c9..6582b4b51 100755 --- a/src/tests/frontend/travis/runnerLoadTest.sh +++ b/src/tests/frontend/travis/runnerLoadTest.sh @@ -24,8 +24,8 @@ s!"points":[^,]*!"points": 1000! ' settings.json.template >settings.json log "Assuming src/bin/installDeps.sh has already been run" -node src/node/server.js & -ep_pid=$! +(cd src && npm run dev & +ep_pid=$!) log "Waiting for Etherpad to accept connections (http://localhost:9001)..." connected=false diff --git a/src/tests/ratelimit/Dockerfile.anotherip b/src/tests/ratelimit/Dockerfile.anotherip index 57f02f628..c352b4af1 100644 --- a/src/tests/ratelimit/Dockerfile.anotherip +++ b/src/tests/ratelimit/Dockerfile.anotherip @@ -1,4 +1,4 @@ -FROM node:alpine3.12 +FROM node:latest WORKDIR /tmp RUN npm i etherpad-cli-client COPY ./src/tests/ratelimit/send_changesets.js /tmp/send_changesets.js diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 000000000..eea5f16fa --- /dev/null +++ b/src/tsconfig.json @@ -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. */ + } +} diff --git a/src/web.config b/src/web.config index bd50a60c5..65f2cf03f 100644 --- a/src/web.config +++ b/src/web.config @@ -2,7 +2,7 @@ - + @@ -13,7 +13,7 @@ - + --> @@ -23,7 +23,7 @@ - + diff --git a/start.bat b/start.bat index 7e9264ee3..5fce6ca69 100644 --- a/start.bat +++ b/start.bat @@ -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 Etherpad internals via `require('ep_etherpad-lite/foo')`, REM `node_modules\ep_etherpad-lite` is used here. -node node_modules\ep_etherpad-lite\node\server.js +node run dev