refactor(node/hooks): partial CJS->ESM (express, i18n, 7 of 14 express/* files)

Done: express.ts (incl. https/http hoisted to top-level imports, exports.server
+ exports.sessionMiddleware -> export let), i18n.ts, admin.ts, webaccess.ts
(authnFailureDelayMs preserved as export let + setter), padurlsanitize.ts,
pwa.ts, errorhandling.ts (exports.app preserved as export let), tokenTransfer.ts,
adminplugins.ts.

Still CJS in hooks/express/: adminsettings, apicalls, importexport, openapi,
socketio, specialpages, static.
This commit is contained in:
SamTV12345 2026-04-26 13:41:33 +02:00
parent 18a290a83f
commit 63de3c6e30
9 changed files with 77 additions and 72 deletions

View file

@ -1,7 +1,7 @@
'use strict';
import {Socket} from "node:net";
import type {MapArrayType} from "../types/MapType";
import type {MapArrayType} from "../types/MapType.js";
import _ from 'underscore';
import cookieParser from 'cookie-parser';
@ -9,15 +9,17 @@ import events from 'events';
import express from 'express';
import expressSession, {Store} from 'express-session';
import fs from 'fs';
const hooks = require('../../static/js/pluginfw/hooks');
import hooks from '../../static/js/pluginfw/hooks.js';
import log4js from 'log4js';
const SessionStore = require('../db/SessionStore');
import settings, {getEpVersion, getGitCommit} from '../utils/Settings';
const stats = require('../stats')
import SessionStore from '../db/SessionStore.js';
import settings, {getEpVersion, getGitCommit} from '../utils/Settings.js';
import stats from '../stats.js';
import util from 'util';
const webaccess = require('./express/webaccess');
import * as webaccess from './express/webaccess.js';
import https from 'https';
import http from 'http';
import SecretRotator from '../security/SecretRotator';
import SecretRotator from '../security/SecretRotator.js';
let secretRotator: SecretRotator|null = null;
const logger = log4js.getLogger('http');
@ -27,14 +29,15 @@ const sockets:Set<Socket> = new Set();
const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime');
exports.server = null;
export let server: any = null;
export let sessionMiddleware: any = null;
const closeServer = async () => {
if (exports.server != null) {
if (server != null) {
logger.info('Closing HTTP server...');
// Call exports.server.close() to reject new connections but don't await just yet because the
// Call server.close() to reject new connections but don't await just yet because the
// Promise won't resolve until all preexisting connections are closed.
const p = util.promisify(exports.server.close.bind(exports.server))();
const p = util.promisify(server.close.bind(server))();
await hooks.aCallAll('expressCloseServer');
// Give existing connections some time to close on their own before forcibly terminating. The
// time should be long enough to avoid interrupting most preexisting transmissions but short
@ -53,7 +56,7 @@ const closeServer = async () => {
}
await p;
clearTimeout(timeout);
exports.server = null;
server = null;
startTime.setValue(0);
logger.info('HTTP server closed');
}
@ -64,14 +67,14 @@ const closeServer = async () => {
secretRotator = null;
};
exports.createServer = async () => {
export const createServer = async () => {
console.log('Report bugs at https://github.com/ether/etherpad/issues');
serverName = `Etherpad ${getGitCommit()} (https://etherpad.org)`;
console.log(`Your Etherpad version is ${getEpVersion()} (${getGitCommit()})`);
await exports.restartServer();
await restartServer();
if (settings.ip === '') {
// using Unix socket for connectivity
@ -96,7 +99,7 @@ exports.createServer = async () => {
}
};
exports.restartServer = async () => {
export const restartServer = async () => {
await closeServer();
const app = express(); // New syntax for express v3
@ -119,11 +122,9 @@ exports.restartServer = async () => {
}
}
const https = require('https');
exports.server = https.createServer(options, app);
server = https.createServer(options, app);
} else {
const http = require('http');
exports.server = http.createServer(app);
server = http.createServer(app);
}
app.use((req, res, next) => {
@ -205,7 +206,7 @@ exports.restartServer = async () => {
store.startCleanup();
}
sessionStore = store;
exports.sessionMiddleware = expressSession({
sessionMiddleware = expressSession({
rolling: true,
secret,
store: sessionStore ?? undefined,
@ -242,15 +243,15 @@ exports.restartServer = async () => {
// middleware. This allows plugins to avoid creating an express-session record in the database
// when it is not needed (e.g., public static content).
await hooks.aCallAll('expressPreSession', {app, settings});
app.use(exports.sessionMiddleware);
app.use(sessionMiddleware);
app.use(webaccess.checkAccess);
await Promise.all([
hooks.aCallAll('expressConfigure', {app}),
hooks.aCallAll('expressCreateServer', {app, server: exports.server}),
hooks.aCallAll('expressCreateServer', {app, server: server}),
]);
exports.server.on('connection', (socket:Socket) => {
server.on('connection', (socket:Socket) => {
sockets.add(socket);
socketsEvents.emit('updated');
socket.on('close', () => {
@ -258,11 +259,11 @@ exports.restartServer = async () => {
socketsEvents.emit('updated');
});
});
await util.promisify(exports.server.listen).bind(exports.server)(settings.port, settings.ip);
await util.promisify(server.listen).bind(server)(settings.port, settings.ip);
startTime.setValue(Date.now());
logger.info('HTTP server listening for connections');
};
exports.shutdown = async (hookName:string, context: any) => {
export const shutdown = async (hookName:string, context: any) => {
await closeServer();
};

View file

@ -1,10 +1,10 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import {ArgsExpressType} from "../../types/ArgsExpressType.js";
import path from "path";
import fs from "fs";
import {MapArrayType} from "../../types/MapType";
import {MapArrayType} from "../../types/MapType.js";
import settings from 'ep_etherpad-lite/node/utils/Settings';
import settings from '../../utils/Settings.js';
const ADMIN_PATH = path.join(settings.root, 'src', 'templates');
const PROXY_HEADER = "x-proxy-path"
@ -15,7 +15,7 @@ const PROXY_HEADER = "x-proxy-path"
* @param {Function} cb the callback function
* @return {*}
*/
exports.expressCreateServer = (hookName: string, args: ArgsExpressType, cb: Function): any => {
export const expressCreateServer = (hookName: string, args: ArgsExpressType, cb: Function): any => {
if (!fs.existsSync(ADMIN_PATH)) {
console.error('admin template not found, skipping admin interface. You need to rebuild it in /admin with pnpm run build-copy')

View file

@ -1,20 +1,21 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import {ErrorCaused} from "../../types/ErrorCaused";
import {QueryType} from "../../types/QueryType";
import {ArgsExpressType} from "../../types/ArgsExpressType.js";
import {ErrorCaused} from "../../types/ErrorCaused.js";
import {QueryType} from "../../types/QueryType.js";
import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer";
import {PackageData, PackageInfo} from "../../types/PackageInfo";
import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer.js";
import {PackageData, PackageInfo} from "../../types/PackageInfo.js";
import semver from 'semver';
import log4js from 'log4js';
import {MapArrayType} from "../../types/MapType";
import {MapArrayType} from "../../types/MapType.js";
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
import pluginDefs from '../../../static/js/pluginfw/plugin_defs.js';
import stats from '../../stats.js';
const logger = log4js.getLogger('adminPlugins');
exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
export const socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
const io = args.io.of('/pluginfw/installer');
io.on('connection', (socket:any) => {
// @ts-ignore
@ -41,7 +42,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
socket.on('getStats', ()=>{
console.log("Getting stats for admin plugins");
socket.emit('results:stats', require('../../stats').toJSON());
socket.emit('results:stats', stats.toJSON());
})
socket.on('getInstalled', async (query: string) => {

View file

@ -1,12 +1,13 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import {ErrorCaused} from "../../types/ErrorCaused";
import {ArgsExpressType} from "../../types/ArgsExpressType.js";
import {ErrorCaused} from "../../types/ErrorCaused.js";
const stats = require('../../stats')
import stats from '../../stats.js';
exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => {
exports.app = args.app;
export let app: any = null;
export const expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => {
app = args.app;
// Handle errors
args.app.use((err:ErrorCaused, req:any, res:any, next:Function) => {

View file

@ -1,10 +1,10 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import {ArgsExpressType} from "../../types/ArgsExpressType.js";
const padManager = require('../../db/PadManager');
import * as padManager from '../../db/PadManager.js';
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
export const 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:any, res:any, next:Function, padId:string) => {
(async () => {

View file

@ -1,5 +1,5 @@
import {ArgsExpressType} from "../../types/ArgsExpressType";
import settings from '../../utils/Settings';
import {ArgsExpressType} from "../../types/ArgsExpressType.js";
import settings from '../../utils/Settings.js';
const pwa = {
name: settings.title || "Etherpad",
@ -23,7 +23,7 @@ const pwa = {
background_color: "#0f775b"
}
exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
export const expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => {
args.app.get('/manifest.json', (req:any, res:any) => {
res.json(pwa);
});

View file

@ -1,7 +1,7 @@
import {ArgsExpressType} from "../../types/ArgsExpressType";
const db = require('../../db/DB');
import {ArgsExpressType} from "../../types/ArgsExpressType.js";
import db from '../../db/DB.js';
import crypto from 'crypto'
import settings from '../../utils/Settings';
import settings from '../../utils/Settings.js';
type TokenTransferRequest = {

View file

@ -2,13 +2,13 @@
import {strict as assert} from "assert";
import log4js from 'log4js';
import {SocketClientRequest} from "../../types/SocketClientRequest";
import {WebAccessTypes} from "../../types/WebAccessTypes";
import {SettingsUser} from "../../types/SettingsUser";
import {SocketClientRequest} from "../../types/SocketClientRequest.js";
import {WebAccessTypes} from "../../types/WebAccessTypes.js";
import {SettingsUser} from "../../types/SettingsUser.js";
const httpLogger = log4js.getLogger('http');
import settings from '../../utils/Settings';
const hooks = require('../../../static/js/pluginfw/hooks');
import readOnlyManager from '../../db/ReadOnlyManager';
import settings from '../../utils/Settings.js';
import hooks from '../../../static/js/pluginfw/hooks.js';
import readOnlyManager from '../../db/ReadOnlyManager.js';
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
@ -21,7 +21,7 @@ const aCallFirst0 =
// @ts-ignore
async (hookName: string, context:any, pred = null) => (await aCallFirst(hookName, context, pred))[0];
exports.normalizeAuthzLevel = (level: string|boolean) => {
export const normalizeAuthzLevel = (level: string|boolean) => {
if (!level) return false;
switch (level) {
case true:
@ -36,18 +36,19 @@ exports.normalizeAuthzLevel = (level: string|boolean) => {
return false;
};
exports.userCanModify = (padId: string, req: SocketClientRequest) => {
export const userCanModify = (padId: string, req: SocketClientRequest) => {
if (readOnlyManager.isReadOnlyId(padId)) return false;
if (!settings.requireAuthentication) return true;
const {session: {user} = {}} = req;
if (!user || user.readOnly) return false;
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
const level = normalizeAuthzLevel(user.padAuthorizations[padId]);
return level && level !== 'readOnly';
};
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
exports.authnFailureDelayMs = 1000;
export let authnFailureDelayMs = 1000;
export const setAuthnFailureDelayMs = (v: number) => { authnFailureDelayMs = v; };
const staticResources = [
/^\/padbootstrap-[a-zA-Z0-9]+\.min\.js$/,
@ -106,7 +107,7 @@ const checkAccess = async (req:any, res:any, next: Function) => {
// authentication is checked and once after (if settings.requireAuthorization is true).
const authorize = async () => {
const grant = async (level: string|false) => {
level = exports.normalizeAuthzLevel(level);
level = normalizeAuthzLevel(level);
if (!level) return false;
const user = req.session.user;
if (user == null) return true; // This will happen if authentication is not required.
@ -186,7 +187,7 @@ const checkAccess = async (req:any, res:any, next: Function) => {
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
}
// Delay the error response for 1s to slow down brute force attacks.
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
await new Promise((resolve) => setTimeout(resolve, authnFailureDelayMs));
res.status(401).send('Authentication Required');
return;
}
@ -230,6 +231,7 @@ const checkAccess = async (req:any, res:any, next: Function) => {
* Express middleware to authenticate the user and check authorization. Must be installed after the
* express-session middleware.
*/
exports.checkAccess = (req:any, res:any, next:Function) => {
export const checkAccessMiddleware = (req:any, res:any, next:Function) => {
checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
};
export { checkAccessMiddleware as checkAccess };

View file

@ -1,15 +1,15 @@
'use strict';
import type {MapArrayType} from "../types/MapType";
import {I18nPluginDefs} from "../types/I18nPluginDefs";
import type {MapArrayType} from "../types/MapType.js";
import {I18nPluginDefs} from "../types/I18nPluginDefs.js";
const languages = require('languages4translatewiki');
import languages from 'languages4translatewiki';
import fs from 'fs';
import path from 'path';
import _ from 'underscore';
const pluginDefs = require('../../static/js/pluginfw/plugin_defs');
import existsSync from '../utils/path_exists';
import settings from '../utils/Settings';
import pluginDefs from '../../static/js/pluginfw/plugin_defs.js';
import existsSync from '../utils/path_exists.js';
import settings from '../utils/Settings.js';
// returns all existing messages merged together and grouped by langcode
// {es: {"foo": "string"}, en:...}
@ -131,7 +131,7 @@ const generateLocaleIndex = (locales:MapArrayType<string>) => {
};
exports.expressPreSession = async (hookName:string, {app}:any) => {
export const expressPreSession = async (hookName:string, {app}:any) => {
// regenerate locales on server restart
const locales = getAllLocales();
const localeIndex = generateLocaleIndex(locales);