chore: continued with backend migration

This commit is contained in:
SamTV12345 2026-04-26 15:55:11 +02:00
parent a8d7a3c5ad
commit 42907a7cc4
33 changed files with 225 additions and 108 deletions

View File

@ -24,11 +24,13 @@
import ejs from 'ejs';
import fs from 'fs';
import hooks from '../../static/js/pluginfw/hooks.js';
import * as i18n from '../hooks/i18n.js';
import path from 'node:path';
// @ts-ignore
import resolve from 'resolve';
import settings from '../utils/Settings.js';
import { pluginInstallPath } from '../../static/js/pluginfw/installer.js';
import pluginUtils from '../../static/js/pluginfw/shared.js';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { createRequire } from 'node:module';
@ -36,6 +38,10 @@ import { createRequire } from 'node:module';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const requireFromHere = createRequire(import.meta.url);
const templateModules = new Map([
['ep_etherpad-lite/node/hooks/i18n', i18n],
['ep_etherpad-lite/static/js/pluginfw/shared', pluginUtils],
]);
const templateCache = new Map();
@ -111,7 +117,7 @@ eejs.require = (
const ejspath = resolve.sync(name, { paths, basedir, extensions: ['.html', '.ejs'] });
args.e = eejs;
args.require = requireFromHere;
args.require = (name: string) => templateModules.get(name) ?? requireFromHere(name);
const cache = settings.maxAge !== 0;
const template =

View File

@ -75,7 +75,7 @@ const tmpDirectory = os.tmpdir();
* @param {String} padId the pad id to export
* @param {String} authorId the author id to use for the import
*/
const doImport = async (req:any, res:any, padId:string, authorId:string) => {
const performImport = async (req:any, res:any, padId:string, authorId:string) => {
// pipe to a file
// convert file to html via soffice
// set html in the pad
@ -248,7 +248,7 @@ export const doImport = async (req:any, res:any, padId:string, authorId:string =
let message = 'ok';
let directDatabaseAccess;
try {
directDatabaseAccess = await doImport(req, res, padId, authorId);
directDatabaseAccess = await performImport(req, res, padId, authorId);
} catch (err:any) {
const known = err instanceof ImportError && err.status;
if (!known) logger.error(`Internal error during import: ${err.stack || err}`);

View File

@ -1,7 +1,7 @@
import {ArgsExpressType} from "../types/ArgsExpressType.js";
import {MapArrayType} from "../types/MapType.js";
import type {ArgsExpressType} from "../types/ArgsExpressType.js";
import type {MapArrayType} from "../types/MapType.js";
import {IncomingForm} from "formidable";
import {ErrorCaused} from "../types/ErrorCaused.js";
import type {ErrorCaused} from "../types/ErrorCaused.js";
import createHTTPError from "http-errors";
import * as apiHandler from './APIHandler.js';

View File

@ -1,8 +1,8 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType.js";
import type {ArgsExpressType} from "../../types/ArgsExpressType.js";
import path from "path";
import fs from "fs";
import {MapArrayType} from "../../types/MapType.js";
import type {MapArrayType} from "../../types/MapType.js";
import settings from '../../utils/Settings.js';

View File

@ -1,14 +1,14 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType.js";
import {ErrorCaused} from "../../types/ErrorCaused.js";
import {QueryType} from "../../types/QueryType.js";
import type {ArgsExpressType} from "../../types/ArgsExpressType.js";
import type {ErrorCaused} from "../../types/ErrorCaused.js";
import type {QueryType} from "../../types/QueryType.js";
import {getAvailablePlugins, install, search, uninstall} from "../../../static/js/pluginfw/installer.js";
import {PackageData, PackageInfo} from "../../types/PackageInfo.js";
import type {PackageData, PackageInfo} from "../../types/PackageInfo.js";
import semver from 'semver';
import log4js from 'log4js';
import {MapArrayType} from "../../types/MapType.js";
import type {MapArrayType} from "../../types/MapType.js";
import pluginDefs from '../../../static/js/pluginfw/plugin_defs.js';
import stats from '../../stats.js';

View File

@ -1,6 +1,6 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import type {ArgsExpressType} from "../../types/ArgsExpressType.js";
import hasPadAccess from '../../padaccess.js';
import settings, {exportAvailable} from '../../utils/Settings.js';

View File

@ -1,8 +1,8 @@
'use strict';
import {OpenAPIOperations, OpenAPISuccessResponse, SwaggerUIResource} from "../../types/SwaggerUIResource";
import {MapArrayType} from "../../types/MapType";
import {ErrorCaused} from "../../types/ErrorCaused";
import type {OpenAPIOperations, OpenAPISuccessResponse, SwaggerUIResource} from "../../types/SwaggerUIResource.ts";
import type {MapArrayType} from "../../types/MapType.js";
import type {ErrorCaused} from "../../types/ErrorCaused.js";
/**
* node/hooks/express/openapi.js

View File

@ -1,6 +1,6 @@
'use strict';
import {ArgsExpressType} from "../../types/ArgsExpressType";
import type {ArgsExpressType} from "../../types/ArgsExpressType.js";
import events from 'events';
import * as express from '../express.js';

View File

@ -6,7 +6,7 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
import {MapArrayType} from "../../node/types/MapType";
import type {MapArrayType} from "../../node/types/MapType.js";
/**
* Copyright 2009 Google Inc.
@ -63,3 +63,12 @@ export const binarySearchInfinite = (expectedLength: number, func: (num: number)
};
export const noop = () => {};
export default {
isNodeText,
getAssoc,
setAssoc,
binarySearch,
binarySearchInfinite,
noop,
};

View File

@ -3736,3 +3736,7 @@ export const init = async (editorInfo, cssManagers) => {
const editor = new Ace2Inner(editorInfo, cssManagers);
await editor.init();
};
export default {
init,
};

View File

@ -956,5 +956,4 @@ export {settings};
export {randomString};
export {getParams};
export {pad};
export {init};
export {baseURL};

View File

@ -194,3 +194,7 @@ CountDownTimer.parse = (seconds) => ({
minutes: (seconds / 60) | 0,
seconds: (seconds % 60) | 0,
});
export default {
showCountDownTimerToReconnectOnModal,
};

View File

@ -37,3 +37,8 @@ export const saveNow = () => {
export const init = (_pad) => {
pad = _pad;
};
export default {
saveNow,
init,
};

View File

@ -1,7 +1,6 @@
// @ts-nocheck
'use strict';
import {createRequire} from 'node:module';
import {pathToFileURL} from 'node:url';
import {promises as fs} from 'fs';
import log4js from 'log4js';
import path from 'path';
@ -14,11 +13,6 @@ import settings, {
getEpVersion,
} from '../../../node/utils/Settings.js';
// `installer.ts` is loaded lazily inside `getPackages()` to avoid an import cycle. Use a
// `createRequire`-backed `require` so the existing CommonJS-style lazy access keeps working in
// ESM.
const requireFromHere = createRequire(import.meta.url);
const logger = log4js.getLogger('plugins');
// Log the version of npm at startup.
@ -102,11 +96,88 @@ export const pathNormalization = (part, hookFnName, hookName) => {
// If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hookName;
const moduleName = tmp.join(':') || part.plugin;
const packageDir = path.dirname(defs.plugins[part.plugin].package.path);
const fileName = path.join(packageDir, moduleName);
const pkg = defs.plugins[part.plugin].package;
const packageRoot = pkg.realPath || pkg.path;
const pluginPrefix = `${part.plugin}/`;
const relativeModuleName = moduleName.startsWith(pluginPrefix)
? moduleName.slice(pluginPrefix.length)
: moduleName;
const fileName = path.isAbsolute(relativeModuleName)
? relativeModuleName
: path.join(packageRoot, relativeModuleName);
return `${fileName}:${functionName}`;
};
const loadServerHook = async (hookFnName, hookName) => {
const parts = hookFnName.split(':');
let functionName;
let modulePath;
if (parts[0].length === 1) {
if (parts.length === 3) functionName = parts.pop();
modulePath = parts.join(':');
} else {
modulePath = parts[0];
functionName = parts[1];
}
functionName = functionName || hookName;
const candidates = path.extname(modulePath) === ''
? [`${modulePath}.ts`, `${modulePath}.js`, modulePath]
: [modulePath];
let mod;
let lastErr;
for (const candidate of candidates) {
try {
mod = await import(pathToFileURL(candidate).href);
break;
} catch (err) {
lastErr = err;
}
}
if (mod == null) throw lastErr;
for (const namespace of [mod, mod.default].filter((ns) => ns != null)) {
let hookFn = namespace;
let missing = false;
for (const name of functionName.split('.')) {
if (hookFn == null || !(name in hookFn)) {
missing = true;
break;
}
hookFn = hookFn[name];
}
if (!missing) return hookFn;
}
return undefined;
};
const extractServerHooks = async (parts) => {
const hooksByName = {};
for (const part of parts) {
for (const [hookName, regHookFnName] of Object.entries(part.hooks || {})) {
const hookFnName = pathNormalization(part, regHookFnName, hookName);
try {
const hookFn = await loadServerHook(hookFnName, hookName);
if (!hookFn) throw new Error('Not a function');
if (hooksByName[hookName] == null) hooksByName[hookName] = [];
hooksByName[hookName].push({
hook_name: hookName,
hook_fn: hookFn,
hook_fn_name: hookFnName,
part,
});
} catch (err) {
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
`part "${part.name}" hook set "hooks" hook "${hookName}": ` +
`${err.stack || err}`);
}
}
}
return hooksByName;
};
export const update = async () => {
const packages = await getPackages();
const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array.
@ -121,7 +192,7 @@ export const update = async () => {
defs.plugins = plugins;
defs.parts = sortParts(parts);
defs.hooks = pluginUtils.extractHooks(defs.parts, 'hooks', pathNormalization);
defs.hooks = await extractServerHooks(defs.parts);
defs.loaded = true;
await Promise.all(Object.keys(defs.plugins).map(async (p) => {
const logger = log4js.getLogger(`plugin:${p}`);
@ -130,9 +201,8 @@ export const update = async () => {
};
export const getPackages = async () => {
// Lazily resolved via `createRequire` to avoid a circular ESM import between
// `plugins.ts` and `installer.ts`.
const {linkInstaller} = requireFromHere('./installer');
// Lazily import to avoid a circular dependency between `plugins.ts` and `installer.ts`.
const {linkInstaller} = await import('./installer.js');
const plugins = await linkInstaller.listPlugins();
const newDependencies = {};

View File

@ -1,14 +1,8 @@
// @ts-nocheck
'use strict';
import {createRequire} from 'node:module';
import defs from './plugin_defs.js';
// `createRequire` gives us a synchronous CommonJS-style `require` even though this file is now
// ESM. This is needed to keep the existing plugin contract (CJS plugins via `module.exports`)
// working when `loadFn` loads a plugin entry path at runtime. See `doc/plugins.md`.
const requireFromHere = createRequire(import.meta.url);
const disabledHookReasons = {
hooks: {
indexCustomInlineScripts: 'The hook makes it impossible to use a Content Security Policy ' +
@ -16,6 +10,29 @@ const disabledHookReasons = {
},
};
const loadModule = (path, modules) => {
if (modules !== undefined && 'get' in modules) return modules.get(path);
if (typeof require !== 'function') throw new Error('dynamic hook loading unavailable');
return require(path);
};
const getHookFunction = (fn, functionName) => {
const namespaces = [fn, fn?.default].filter((ns) => ns != null);
for (const namespace of namespaces) {
let hookFn = namespace;
let missing = false;
for (const name of functionName.split('.')) {
if (hookFn == null || !(name in hookFn)) {
missing = true;
break;
}
hookFn = hookFn[name];
}
if (!missing) return hookFn;
}
return undefined;
};
const loadFn = (path, hookName, modules) => {
let functionName;
const parts = path.split(':');
@ -31,18 +48,8 @@ const loadFn = (path, hookName, modules) => {
functionName = parts[1];
}
let fn
if (modules === undefined || !("get" in modules)) {
fn = requireFromHere(/* webpackIgnore: true */ path);
} else {
fn = modules.get(path);
}
functionName = functionName ? functionName : hookName;
for (const name of functionName.split('.')) {
fn = fn[name];
}
let fn = getHookFunction(loadModule(path, modules), functionName);
return fn;
};

View File

@ -6,3 +6,4 @@ window.$ = $;
const jq = window.$.noConflict(true);
export {jq as jQuery, jq as $};
export default jq;

View File

@ -79,3 +79,10 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
}
export {isDarkMode, setDarkModeInLocalStorage, isWhiteModeEnabledInLocalStorage, isDarkModeEnabledInLocalStorage, updateSkinVariantsClasses};
export default {
isDarkMode,
setDarkModeInLocalStorage,
isWhiteModeEnabledInLocalStorage,
isDarkModeEnabledInLocalStorage,
updateSkinVariantsClasses,
};

View File

@ -42,8 +42,11 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
return socket;
};
if (typeof exports === 'object') {
exports.connect = connect;
} else {
window.socketio = {connect};
const socketio = {connect};
if (typeof window !== 'undefined') {
window.socketio = socketio;
}
export {connect};
export default socketio;

View File

@ -9,18 +9,13 @@
* MIT License | (c) Dustin Diaz 2015
*/
!function (name, definition) {
if (typeof module != 'undefined' && module.exports) module.exports = definition()
else if (typeof define == 'function' && define.amd) define(definition)
else this[name] = definition()
}('bowser', function () {
/**
* See useragents.js for examples of navigator.userAgent
*/
/**
* See useragents.js for examples of navigator.userAgent
*/
var t = true
const t = true;
function detect(ua) {
function detect(ua) {
function getFirstMatch(regex) {
var match = ua.match(regex);
@ -284,28 +279,28 @@
} else result.x = t
return result
}
}
var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '')
const bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '');
bowser.test = function (browserList) {
for (var i = 0; i < browserList.length; ++i) {
var browserItem = browserList[i];
if (typeof browserItem=== 'string') {
if (browserItem in bowser) {
return true;
}
bowser.test = function (browserList) {
for (let i = 0; i < browserList.length; ++i) {
const browserItem = browserList[i];
if (typeof browserItem=== 'string') {
if (browserItem in bowser) {
return true;
}
}
return false;
}
return false;
};
/*
* Set our detect method to the main bowser object so we can
* reuse it to test other user agents.
* This is needed to implement future tests.
*/
bowser._detect = detect;
/*
* Set our detect method to the main bowser object so we can
* reuse it to test other user agents.
* This is needed to implement future tests.
*/
bowser._detect = detect;
return bowser
});
export {detect};
export default bowser;

View File

@ -1,6 +1,7 @@
'use strict';
import {MapArrayType} from "../../node/types/MapType.js";
import {afterAll, beforeAll} from 'vitest';
import AttributePool from '../../static/js/AttributePool.js';
import {strict as assert} from 'assert';
@ -32,10 +33,9 @@ const logLevel = logger.level;
// https://github.com/mochajs/mocha/issues/2640
process.on('unhandledRejection', (reason: string) => { throw reason; });
before(async function () {
this.timeout(60000);
beforeAll(async () => {
await init();
});
}, 60000);
export const generateJWTToken = () => {
@ -82,6 +82,7 @@ export const init = async function () {
settings.importExportRateLimiting = {max: 999999};
settings.commitRateLimiting = {duration: 0.001, points: 1e6};
httpServer = await server.start();
if (httpServer == null) throw new Error('server.start() did not return an HTTP server');
// @ts-ignore
baseUrl = `http://localhost:${httpServer!.address()!.port}`;
logger.debug(`HTTP server at ${baseUrl}`);
@ -92,7 +93,7 @@ export const init = async function () {
backups.authnFailureDelayMs = webaccess.authnFailureDelayMs;
webaccess.setAuthnFailureDelayMs(0);
after(async function () {
afterAll(async () => {
webaccess.setAuthnFailureDelayMs(backups.authnFailureDelayMs);
// Note: This does not unset settings that were added.
Object.assign(settings, backups.settings);

View File

@ -47,7 +47,6 @@ describe(__filename, function () {
});
it('can obtain valid openapi definition document', async function () {
this.timeout(15000);
await agent.get('/api/openapi.json')
.expect(200)
.expect((res:any) => {
@ -106,7 +105,6 @@ describe(__filename, function () {
});
it('/api/openapi.json exposes apiKey security in apikey mode', async function () {
this.timeout(15000);
const res = await agent.get('/api/openapi.json').expect(200);
const schemes = res.body.components.securitySchemes;
const hasApiKey = Object.values(schemes).some((s: any) => s.type === 'apiKey');

View File

@ -230,7 +230,6 @@ const testImports:MapArrayType<any> = {
};
describe(__filename, function () {
this.timeout(1000);
before(async function () { agent = await common.init(); });

View File

@ -41,7 +41,6 @@ const deleteTestPad = async () => {
};
describe(__filename, function () {
this.timeout(45000);
before(async function () { agent = await common.init(); });
describe('Connectivity', function () {
@ -319,7 +318,6 @@ describe(__filename, function () {
}); // End of LibreOffice tests.
it('Tries to import .etherpad', async function () {
this.timeout(3000);
await agent.post(`/p/${testPadId}/import`)
.set("authorization", await common.generateJWTToken())
.attach('file', etherpadDoc, {
@ -336,7 +334,6 @@ describe(__filename, function () {
});
it('exports Etherpad', async function () {
this.timeout(3000);
await agent.get(`/p/${testPadId}/export/etherpad`)
.set("authorization", await common.generateJWTToken())
.buffer(true).parse(superagent.parse.text)
@ -345,7 +342,6 @@ describe(__filename, function () {
});
it('exports HTML for this Etherpad file', async function () {
this.timeout(3000);
await agent.get(`/p/${testPadId}/export/html`)
.set("authorization", await common.generateJWTToken())
.expect(200)
@ -354,7 +350,6 @@ describe(__filename, function () {
});
it('Tries to import unsupported file type', async function () {
this.timeout(3000);
settings.allowUnknownFileEnds = false;
await agent.post(`/p/${testPadId}/import`)
.set("authorization", await common.generateJWTToken())
@ -685,7 +680,6 @@ describe(__filename, function () {
return pad;
};
this.timeout(1000);
beforeEach(async function () {
await deleteTestPad();

View File

@ -8,7 +8,6 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
describe(__filename, function () {
this.timeout(30000);
let agent: any;
before(async function () { agent = await common.init(); });

View File

@ -49,7 +49,6 @@ describe(__filename, function () {
});
it('CLIENT_VARS rev matches initialAttributedText state at that exact rev', async function () {
this.timeout(30000);
const padId = randomString(10);
// Create a pad with initial text
@ -97,7 +96,6 @@ describe(__filename, function () {
// (b) lands several edits during that delay.
// The bug also applied at higher load — to also reproduce the load
// scenario, we pre-populate the pad with many revisions before connecting.
this.timeout(60000);
const padId = randomString(10);
const pad = await padManager.getPad(padId, 'rev0\n');
@ -173,7 +171,6 @@ describe(__filename, function () {
});
it('client receives revisions created during clientVars hook await window', async function () {
this.timeout(30000);
const padId = randomString(10);
const pad = await padManager.getPad(padId, 'start\n');

View File

@ -22,7 +22,6 @@ describe(__filename, function () {
});
it('can set and retrieve 50,000 characters of text on a pad', async function () {
this.timeout(30000);
const padId = `largePasteTest${Date.now()}`;
const largeText = 'A'.repeat(50000);

View File

@ -18,7 +18,6 @@ const __dirname = dirname(__filename);
const plugins = pluginDefs;
describe(__filename, function () {
this.timeout(30000);
let agent: any;
let authorize:Function;
const backups:MapArrayType<any> = {};

View File

@ -14,7 +14,6 @@ const __dirname = dirname(__filename);
describe(__filename, function () {
this.timeout(30000);
let agent:any;
const backups:MapArrayType<any> = {};
before(async function () { agent = await common.init(); });

View File

@ -133,7 +133,6 @@ describe(__filename, function () {
describe('undo of clear authorship colors (bug #2802)', function () {
it('should not disconnect when undoing clear authorship with multiple authors', async function () {
this.timeout(30000);
// Step 1: Connect User A
const userA = await connectUser();

View File

@ -17,7 +17,6 @@ const __dirname = dirname(__filename);
const plugins = pluginDefs;
describe(__filename, function () {
this.timeout(30000);
let agent:any;
const backups:MapArrayType<any> = {};
const authHookNames = ['preAuthorize', 'authenticate', 'authorize'];

View File

@ -0,0 +1,13 @@
import {afterAll, beforeAll, describe, it} from 'vitest';
process.env.NODE_ENV = 'production';
process.env.AUTHENTICATION_METHOD = 'sso';
Object.assign(globalThis, {
after: afterAll,
before: beforeAll,
context: describe,
specify: it,
xdescribe: describe.skip,
xit: it.skip,
});

View File

@ -15,7 +15,7 @@
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"resolveJsonModule": true,
"types": ["node", "jquery", "mocha"]
"types": ["node", "jquery", "mocha", "vitest/globals"]
},
"exclude": ["../plugin_packages", "node_modules"]
}

View File

@ -1,7 +1,18 @@
import { defineConfig } from 'vitest/config'
import {defineConfig} from 'vitest/config';
export default defineConfig({
test: {
include: ["tests/backend-new/specs/**/*.ts"],
globals: true,
setupFiles: ['./tests/backend/vitest.setup.ts'],
include: [
'tests/backend-new/specs/**/*.ts',
'tests/backend/specs/**/*.ts',
'tests/container/specs/**/*.ts',
],
exclude: [
'tests/backend/specs/api/fuzzImportTest.ts',
],
hookTimeout: 60000,
testTimeout: 120000,
},
})
});