diff --git a/bin/createUserSession.ts b/bin/createUserSession.ts index 165cf287a..095cebb0e 100644 --- a/bin/createUserSession.ts +++ b/bin/createUserSession.ts @@ -18,7 +18,7 @@ import process from "node:process"; process.on('unhandledRejection', (err) => { throw err; }); -const settings = require('ep_etherpad-lite/node/utils/Settings'); +import settings from 'ep_etherpad-lite/node/utils/Settings'; (async () => { axios.defaults.baseURL = `http://${settings.ip}:${settings.port}`; const api = axios; diff --git a/bin/importSqlFile.ts b/bin/importSqlFile.ts index 7660fa407..6c501fc72 100644 --- a/bin/importSqlFile.ts +++ b/bin/importSqlFile.ts @@ -3,13 +3,13 @@ // 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. import util from "node:util"; -const fs = require('fs'); +import fs from 'node:fs'; import log4js from 'log4js'; import readline from 'readline'; -import {Database} from "ueberdb2"; +import {Database, DatabaseType} from "ueberdb2"; import process from "node:process"; -const settings = require('ep_etherpad-lite/node/utils/Settings'); +import settings from 'ep_etherpad-lite/node/utils/Settings'; process.on('unhandledRejection', (err) => { throw err; }); const startTime = Date.now(); @@ -58,7 +58,7 @@ const unescape = (val: string) => { json: false, // data is already json encoded }; const db = new Database( // eslint-disable-line new-cap - settings.dbType, + settings.dbType as DatabaseType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB')); diff --git a/bin/migrateDB.ts b/bin/migrateDB.ts index 37f1cda23..d1b373289 100644 --- a/bin/migrateDB.ts +++ b/bin/migrateDB.ts @@ -2,7 +2,7 @@ import {readFileSync} from 'node:fs' import {Database, DatabaseType} from "ueberdb2"; import path from "node:path"; -const settings = require('ep_etherpad-lite/node/utils/Settings'); +import settings from 'ep_etherpad-lite/node/utils/Settings'; // file1 = source, file2 = target diff --git a/bin/migrateDirtyDBtoRealDB.ts b/bin/migrateDirtyDBtoRealDB.ts index 3cd32a85a..8bb095d0b 100644 --- a/bin/migrateDirtyDBtoRealDB.ts +++ b/bin/migrateDirtyDBtoRealDB.ts @@ -1,10 +1,9 @@ 'use strict'; import process from 'node:process'; -import {Database} from "ueberdb2"; +import {Database, DatabaseType} from "ueberdb2"; import log4js from 'log4js'; -import util from 'util'; -const settings = require('ep_etherpad-lite/node/utils/Settings'); +import settings from 'ep_etherpad-lite/node/utils/Settings'; // 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. @@ -24,7 +23,7 @@ process.on('unhandledRejection', (err) => { throw err; }); writeInterval: 0, // Write directly to the database, don't buffer }; const db = new Database( // eslint-disable-line new-cap - settings.dbType, + settings.dbType as DatabaseType, settings.dbSettings, dbWrapperSettings, log4js.getLogger('ueberDB')); diff --git a/bin/rebuildPad.ts b/bin/rebuildPad.ts index 8bb63ed84..16787e2c1 100644 --- a/bin/rebuildPad.ts +++ b/bin/rebuildPad.ts @@ -1,5 +1,3 @@ -'use strict'; - /* This is a repair tool. It rebuilds an old pad at a new pad location up to a known "good" revision. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fbf055df..89bfbdcbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,7 +105,7 @@ importers: dependencies: axios: specifier: ^1.10.0 - version: 1.10.0 + version: 1.11.0 ep_etherpad-lite: specifier: workspace:../src version: link:../src @@ -288,6 +288,12 @@ importers: '@types/cookie-parser': specifier: ^1.4.9 version: 1.4.9(@types/express@5.0.3) + '@types/cross-spawn': + specifier: ^6.0.6 + version: 6.0.6 + '@types/ejs': + specifier: ^3.1.5 + version: 3.1.5 '@types/express': specifier: ^5.0.0 version: 5.0.3 @@ -1564,12 +1570,18 @@ packages: '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/cross-spawn@6.0.6': + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/ejs@3.1.5': + resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -2171,9 +2183,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.10.0: - resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} - axios@1.11.0: resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} @@ -5996,12 +6005,18 @@ snapshots: dependencies: '@types/node': 24.1.0 + '@types/cross-spawn@6.0.6': + dependencies: + '@types/node': 24.1.0 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 '@types/deep-eql@4.0.2': {} + '@types/ejs@3.1.5': {} + '@types/estree@1.0.7': {} '@types/estree@1.0.8': {} @@ -6678,14 +6693,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.10.0: - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.3 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.11.0: dependencies: follow-redirects: 1.15.9 diff --git a/src/node/db/DB.ts b/src/node/db/DB.ts index 663946cd6..4b4899fac 100644 --- a/src/node/db/DB.ts +++ b/src/node/db/DB.ts @@ -21,8 +21,8 @@ * limitations under the License. */ -import {Database} from 'ueberdb2'; -const settings = require('../utils/Settings'); +import {Database, DatabaseType} from 'ueberdb2'; +import settings from '../utils/Settings'; import log4js from 'log4js'; const stats = require('../stats') @@ -37,7 +37,7 @@ exports.db = null; * Initializes the database with the settings provided by the settings module */ exports.init = async () => { - exports.db = new Database(settings.dbType, settings.dbSettings, null, logger); + exports.db = new Database(settings.dbType as DatabaseType, settings.dbSettings, null, logger); await exports.db.init(); if (exports.db.metrics != null) { for (const [metric, value] of Object.entries(exports.db.metrics)) { diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts index e726d8a09..003ec0831 100644 --- a/src/node/db/Pad.ts +++ b/src/node/db/Pad.ts @@ -14,7 +14,7 @@ import AttributePool from '../../static/js/AttributePool'; const Stream = require('../utils/Stream'); const assert = require('assert').strict; const db = require('./DB'); -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; const authorManager = require('./AuthorManager'); const padManager = require('./PadManager'); const padMessageHandler = require('../handler/PadMessageHandler'); diff --git a/src/node/db/PadManager.ts b/src/node/db/PadManager.ts index 54dbbf089..292261531 100644 --- a/src/node/db/PadManager.ts +++ b/src/node/db/PadManager.ts @@ -25,7 +25,7 @@ import {PadType} from "../types/PadType"; const CustomError = require('../utils/customError'); const Pad = require('../db/Pad'); const db = require('./DB'); -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; /** * A cache of all loaded Pads. diff --git a/src/node/db/SecurityManager.ts b/src/node/db/SecurityManager.ts index 3935efba3..219d3f2be 100644 --- a/src/node/db/SecurityManager.ts +++ b/src/node/db/SecurityManager.ts @@ -26,7 +26,7 @@ const hooks = require('../../static/js/pluginfw/hooks'); const padManager = require('./PadManager'); import readOnlyManager from './ReadOnlyManager'; const sessionManager = require('./SessionManager'); -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; const webaccess = require('../hooks/express/webaccess'); const log4js = require('log4js'); const authLogger = log4js.getLogger('auth'); diff --git a/src/node/eejs/index.ts b/src/node/eejs/index.ts index 5d57e4751..85de034b0 100644 --- a/src/node/eejs/index.ts +++ b/src/node/eejs/index.ts @@ -20,12 +20,13 @@ * require("./index").require("./path/to/template.ejs") */ -const ejs = require('ejs'); -const fs = require('fs'); +import ejs from 'ejs'; +import fs from 'fs'; const hooks = require('../../static/js/pluginfw/hooks'); -const path = require('path'); -const resolve = require('resolve'); -const settings = require('../utils/Settings'); +import path from 'node:path'; +// @ts-ignore +import resolve from 'resolve'; +import settings from '../utils/Settings'; import {pluginInstallPath} from '../../static/js/pluginfw/installer' const templateCache = new Map(); diff --git a/src/node/handler/APIHandler.ts b/src/node/handler/APIHandler.ts index 7886d0b46..32ce9d118 100644 --- a/src/node/handler/APIHandler.ts +++ b/src/node/handler/APIHandler.ts @@ -23,7 +23,7 @@ import {MapArrayType} from "../types/MapType"; import { jwtDecode } from "jwt-decode"; const api = require('../db/API'); const padManager = require('../db/PadManager'); -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; import createHTTPError from 'http-errors'; import {Http2ServerRequest} from "node:http2"; import {publicKeyExported} from "../security/OAuth2Provider"; @@ -183,7 +183,7 @@ exports.handle = async function (apiVersion: string, functionName: string, field throw new createHTTPError.Unauthorized('no or wrong API Key'); } try { - const clientIds: string[] = settings.sso.clients?.map((client: {client_id: string}) => client.client_id); + const clientIds: string[] = settings.sso.clients?.map((client: {client_id: string}) => client.client_id) ?? []; const jwtToCheck = req.headers.authorization.replace("Bearer ", "") const payload = jwtDecode(jwtToCheck) // client_credentials diff --git a/src/node/handler/APIKeyHandler.ts b/src/node/handler/APIKeyHandler.ts index 6f1d67907..bdeee2290 100644 --- a/src/node/handler/APIKeyHandler.ts +++ b/src/node/handler/APIKeyHandler.ts @@ -1,9 +1,9 @@ -const absolutePaths = require('../utils/AbsolutePaths'); +import * as absolutePaths from '../utils/AbsolutePaths'; import fs from 'fs'; import log4js from 'log4js'; import randomString from '../utils/randomstring'; -const argv = require('../utils/Cli').argv; -const settings = require('../utils/Settings'); +import {argv} from '../utils/Cli' +import settings from '../utils/Settings'; const apiHandlerLogger = log4js.getLogger('APIHandler'); diff --git a/src/node/handler/ExportHandler.ts b/src/node/handler/ExportHandler.ts index 0bf57e2d1..e1294171a 100644 --- a/src/node/handler/ExportHandler.ts +++ b/src/node/handler/ExportHandler.ts @@ -24,7 +24,7 @@ const exporthtml = require('../utils/ExportHtml'); const exporttxt = require('../utils/ExportTxt'); const exportEtherpad = require('../utils/ExportEtherpad'); import fs from 'fs'; -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; import os from 'os'; const hooks = require('../../static/js/pluginfw/hooks'); import util from 'util'; diff --git a/src/node/handler/ImportHandler.ts b/src/node/handler/ImportHandler.ts index 286b4fb56..e569c12fa 100644 --- a/src/node/handler/ImportHandler.ts +++ b/src/node/handler/ImportHandler.ts @@ -25,7 +25,7 @@ const padManager = require('../db/PadManager'); const padMessageHandler = require('./PadMessageHandler'); import {promises as fs} from 'fs'; import path from 'path'; -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; const {Formidable} = require('formidable'); import os from 'os'; const importHtml = require('../utils/ImportHtml'); diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index 180324d53..115d69a23 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -30,7 +30,11 @@ const AttributeManager = require('../../static/js/AttributeManager'); const authorManager = require('../db/AuthorManager'); import padutils from '../../static/js/pad_utils'; import readOnlyManager from '../db/ReadOnlyManager'; -const settings = require('../utils/Settings'); +import settings, { + exportAvailable, + abiwordAvailable, + sofficeAvailable +} from '../utils/Settings'; const securityManager = require('../db/SecurityManager'); const plugins = require('../../static/js/pluginfw/plugin_defs'); import log4js from 'log4js'; @@ -1021,9 +1025,9 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { serverTimestamp: Date.now(), sessionRefreshInterval: settings.cookie.sessionRefreshInterval, userId: sessionInfo.author, - abiwordAvailable: settings.abiwordAvailable(), - sofficeAvailable: settings.sofficeAvailable(), - exportAvailable: settings.exportAvailable(), + abiwordAvailable: abiwordAvailable(), + sofficeAvailable: sofficeAvailable(), + exportAvailable: exportAvailable(), plugins: { plugins: plugins.plugins, parts: plugins.parts, diff --git a/src/node/handler/RestAPI.ts b/src/node/handler/RestAPI.ts index 7b1b82309..1b0d1d09d 100644 --- a/src/node/handler/RestAPI.ts +++ b/src/node/handler/RestAPI.ts @@ -8,7 +8,7 @@ const apiHandler = require('./APIHandler') import {serve, setup} from 'swagger-ui-express' import express from "express"; -const settings = require('../utils/Settings') +import settings from '../utils/Settings'; type RestAPIMapping = { diff --git a/src/node/handler/SocketIORouter.ts b/src/node/handler/SocketIORouter.ts index 482276834..9e5f4e5cd 100644 --- a/src/node/handler/SocketIORouter.ts +++ b/src/node/handler/SocketIORouter.ts @@ -22,8 +22,8 @@ import {MapArrayType} from "../types/MapType"; import {SocketModule} from "../types/SocketModule"; -const log4js = require('log4js'); -const settings = require('../utils/Settings'); +import log4js from 'log4js'; +import settings from '../utils/Settings'; const stats = require('../../node/stats') const logger = log4js.getLogger('socket.io'); diff --git a/src/node/hooks/express.ts b/src/node/hooks/express.ts index 633a85ae8..fb24cbfe6 100644 --- a/src/node/hooks/express.ts +++ b/src/node/hooks/express.ts @@ -12,7 +12,7 @@ import fs from 'fs'; const hooks = require('../../static/js/pluginfw/hooks'); import log4js from 'log4js'; const SessionStore = require('../db/SessionStore'); -const settings = require('../utils/Settings'); +import settings, {getEpVersion, getGitCommit} from '../utils/Settings'; const stats = require('../stats') import util from 'util'; const webaccess = require('./express/webaccess'); @@ -67,9 +67,9 @@ const closeServer = async () => { exports.createServer = async () => { console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); - serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; + serverName = `Etherpad ${getGitCommit()} (https://etherpad.org)`; - console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); + console.log(`Your Etherpad version is ${getEpVersion()} (${getGitCommit()})`); await exports.restartServer(); @@ -176,7 +176,7 @@ exports.restartServer = async () => { // starts listening to requests as reported in issue #158. Not installing the log4js connect // logger when the log level has a higher severity than INFO since it would not log at that level // anyway. - if (!(settings.loglevel === 'WARN' && settings.loglevel === 'ERROR')) { + if (!(settings.loglevel === 'WARN' || settings.loglevel === 'ERROR')) { app.use(log4js.connectLogger(logger, { level: log4js.levels.DEBUG.levelStr, format: ':status, :method :url', @@ -189,7 +189,12 @@ exports.restartServer = async () => { secretRotator = new SecretRotator( 'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey); await secretRotator.start(); - secret = secretRotator.secrets; + const secrets = secretRotator.secrets; + if (Array.isArray(secrets)) { + secret = secrets[0]; + } else { + secret = secretRotator.secrets as unknown as string; + } } if (!secret) throw new Error('missing cookie signing secret'); @@ -206,7 +211,7 @@ exports.restartServer = async () => { // cleaner :) name: 'express_sid', cookie: { - maxAge: sessionLifetime || null, // Convert 0 to null. + maxAge: sessionLifetime || undefined, // Convert 0 to null. sameSite: settings.cookie.sameSite, // The automatic express-session mechanism for determining if the application is being served diff --git a/src/node/hooks/express/admin.ts b/src/node/hooks/express/admin.ts index 66f02699a..ca0f48668 100644 --- a/src/node/hooks/express/admin.ts +++ b/src/node/hooks/express/admin.ts @@ -2,10 +2,9 @@ import {ArgsExpressType} from "../../types/ArgsExpressType"; import path from "path"; import fs from "fs"; -import * as url from "node:url"; import {MapArrayType} from "../../types/MapType"; -const settings = require('ep_etherpad-lite/node/utils/Settings'); +import settings from 'ep_etherpad-lite/node/utils/Settings'; const ADMIN_PATH = path.join(settings.root, 'src', 'templates'); const PROXY_HEADER = "x-proxy-path" diff --git a/src/node/hooks/express/adminsettings.ts b/src/node/hooks/express/adminsettings.ts index 06411738b..e646323f1 100644 --- a/src/node/hooks/express/adminsettings.ts +++ b/src/node/hooks/express/adminsettings.ts @@ -7,7 +7,7 @@ import log4js from 'log4js'; const fsp = require('fs').promises; const hooks = require('../../../static/js/pluginfw/hooks'); const plugins = require('../../../static/js/pluginfw/plugins'); -const settings = require('../../utils/Settings'); +import settings, {getEpVersion, getGitCommit, reloadSettings} from '../../utils/Settings'; import {getLatestVersion} from '../../utils/UpdateCheck'; const padManager = require('../../db/PadManager'); const api = require('../../db/API'); @@ -73,8 +73,8 @@ exports.socketio = (hookName: string, {io}: any) => { socket.on('help', () => { - const gitCommit = settings.getGitCommit(); - const epVersion = settings.getEpVersion(); + const gitCommit = getGitCommit(); + const epVersion = getEpVersion(); const hooks: Map> = plugins.getHooks('hooks', false); const clientHooks: Map> = plugins.getHooks('client_hooks', false); @@ -287,7 +287,7 @@ exports.socketio = (hookName: string, {io}: any) => { socket.on('restartServer', async () => { logger.info('Admin request to restart server through a socket on /admin/settings'); - settings.reloadSettings(); + reloadSettings(); await plugins.update(); await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('restartServer'); diff --git a/src/node/hooks/express/importexport.ts b/src/node/hooks/express/importexport.ts index 279dea8f3..7f9356844 100644 --- a/src/node/hooks/express/importexport.ts +++ b/src/node/hooks/express/importexport.ts @@ -3,7 +3,7 @@ import {ArgsExpressType} from "../../types/ArgsExpressType"; const hasPadAccess = require('../../padaccess'); -const settings = require('../../utils/Settings'); +import settings, {exportAvailable} from '../../utils/Settings'; const exportHandler = require('../../handler/ExportHandler'); const importHandler = require('../../handler/ImportHandler'); const padManager = require('../../db/PadManager'); @@ -35,7 +35,7 @@ exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Functio } // if abiword is disabled, and this is a format we only support with abiword, output a message - if (settings.exportAvailable() === 'no' && + if (exportAvailable() === 'no' && ['odt', 'pdf', 'doc'].indexOf(req.params.type) !== -1) { console.error(`Impossible to export pad "${req.params.pad}" in ${req.params.type} format.` + ' There is no converter configured'); diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index 8b04adf93..ddd557dc1 100644 --- a/src/node/hooks/express/openapi.ts +++ b/src/node/hooks/express/openapi.ts @@ -24,9 +24,9 @@ const cloneDeep = require('lodash.clonedeep'); const createHTTPError = require('http-errors'); const apiHandler = require('../../handler/APIHandler'); -const settings = require('../../utils/Settings'); +import settings from '../../utils/Settings'; -const log4js = require('log4js'); +import log4js from 'log4js'; const logger = log4js.getLogger('API'); // https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0 diff --git a/src/node/hooks/express/pwa.ts b/src/node/hooks/express/pwa.ts index 918efbc05..a763af5b4 100644 --- a/src/node/hooks/express/pwa.ts +++ b/src/node/hooks/express/pwa.ts @@ -1,5 +1,5 @@ import {ArgsExpressType} from "../../types/ArgsExpressType"; -const settings = require('../../utils/Settings'); +import settings from '../../utils/Settings'; const pwa = { name: settings.title || "Etherpad", diff --git a/src/node/hooks/express/socketio.ts b/src/node/hooks/express/socketio.ts index bbdec1c1c..9184eff88 100644 --- a/src/node/hooks/express/socketio.ts +++ b/src/node/hooks/express/socketio.ts @@ -6,7 +6,7 @@ import events from 'events'; const express = require('../express'); import log4js from 'log4js'; const proxyaddr = require('proxy-addr'); -const settings = require('../../utils/Settings'); +import settings from '../../utils/Settings'; import {Server, Socket} from 'socket.io' const socketIORouter = require('../../handler/SocketIORouter'); const hooks = require('../../../static/js/pluginfw/hooks'); diff --git a/src/node/hooks/express/specialpages.ts b/src/node/hooks/express/specialpages.ts index 4184789d3..5f892ada6 100644 --- a/src/node/hooks/express/specialpages.ts +++ b/src/node/hooks/express/specialpages.ts @@ -6,7 +6,7 @@ import fs from 'node:fs'; const fsp = fs.promises; const toolbar = require('../../utils/toolbar'); const hooks = require('../../../static/js/pluginfw/hooks'); -const settings = require('../../utils/Settings'); +import settings, {getEpVersion} from '../../utils/Settings'; import util from 'node:util'; const webaccess = require('./webaccess'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); @@ -20,14 +20,14 @@ exports.socketio = (hookName: string, {io}: any) => { } -exports.expressPreSession = async (hookName:string, {app, settings}:ArgsExpressType) => { +exports.expressPreSession = async (hookName:string, {app}:ArgsExpressType) => { // This endpoint is intended to conform to: // https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html app.get('/health', (req:any, res:any) => { res.set('Content-Type', 'application/health+json'); res.json({ status: 'pass', - releaseId: settings.getEpVersion(), + releaseId: getEpVersion(), }); }); @@ -43,6 +43,10 @@ exports.expressPreSession = async (hookName:string, {app, settings}:ArgsExpressT }); app.get('/robots.txt', (req:any, res:any) => { + if (!settings.skinName) { + // if no skin is set, send the default robots.txt + return res.sendFile(path.join(settings.root, 'src', 'static', 'robots.txt')); + } let filePath = path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); res.sendFile(filePath, (err:any) => { @@ -66,11 +70,13 @@ exports.expressPreSession = async (hookName:string, {app, settings}:ArgsExpressT } + console.log("Favicon is", settings.favicon) const fns = [ ...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), + settings.skinName && path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), path.join(settings.root, 'src', 'static', 'favicon.ico'), - ]; + ].filter(f=>f != null); + console.log('FNS are', fns) for (const fn of fns) { try { await fsp.access(fn, fs.constants.R_OK); @@ -178,7 +184,8 @@ const handleLiveReload = async (args: ArgsExpressType, padString: string, timeSl req, toolbar, isReadOnly, - entrypoint: '/watch/pad?hash=' + hash + entrypoint: '/watch/pad?hash=' + hash, + settings: settings.getPublicSettings() }) res.send(content); }) @@ -207,7 +214,8 @@ const handleLiveReload = async (args: ArgsExpressType, padString: string, timeSl req, toolbar, isReadOnly, - entrypoint: '/watch/timeslider?hash=' + hash + entrypoint: '/watch/timeslider?hash=' + hash, + settings: settings.getPublicSettings() }) res.send(content); }) @@ -334,7 +342,8 @@ exports.expressCreateServer = async (_hookName: string, args: ArgsExpressType, c req, toolbar, isReadOnly, - entrypoint: "../"+fileNamePad + entrypoint: "../"+fileNamePad, + settings: settings.getPublicSettings() }) res.send(content); }); @@ -348,7 +357,8 @@ exports.expressCreateServer = async (_hookName: string, args: ArgsExpressType, c res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', { req, toolbar, - entrypoint: "../../"+fileNameTimeSlider + entrypoint: "../../"+fileNameTimeSlider, + settings: settings.getPublicSettings() })); }); } else { diff --git a/src/node/hooks/express/static.ts b/src/node/hooks/express/static.ts index f83bb382a..9a8adfa4a 100644 --- a/src/node/hooks/express/static.ts +++ b/src/node/hooks/express/static.ts @@ -8,7 +8,7 @@ import {minify} from '../../utils/Minify'; import path from 'node:path'; import {ArgsExpressType} from "../../types/ArgsExpressType"; const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const settings = require('../../utils/Settings'); +import settings from '../../utils/Settings'; // Rewrite tar to include modules with no extensions and proper rooted paths. const getTar = async () => { diff --git a/src/node/hooks/express/webaccess.ts b/src/node/hooks/express/webaccess.ts index 2c8579623..031224f68 100644 --- a/src/node/hooks/express/webaccess.ts +++ b/src/node/hooks/express/webaccess.ts @@ -6,7 +6,7 @@ import {SocketClientRequest} from "../../types/SocketClientRequest"; import {WebAccessTypes} from "../../types/WebAccessTypes"; import {SettingsUser} from "../../types/SettingsUser"; const httpLogger = log4js.getLogger('http'); -const settings = require('../../utils/Settings'); +import settings from '../../utils/Settings'; const hooks = require('../../../static/js/pluginfw/hooks'); import readOnlyManager from '../../db/ReadOnlyManager'; diff --git a/src/node/hooks/i18n.ts b/src/node/hooks/i18n.ts index 69c313d0d..a59de913f 100644 --- a/src/node/hooks/i18n.ts +++ b/src/node/hooks/i18n.ts @@ -4,12 +4,12 @@ import type {MapArrayType} from "../types/MapType"; import {I18nPluginDefs} from "../types/I18nPluginDefs"; const languages = require('languages4translatewiki'); -const fs = require('fs'); -const path = require('path'); -const _ = require('underscore'); +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'; -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} @@ -73,7 +73,7 @@ const getAllLocales = () => { 'for Customization for Administrators, under Localization.'); if (settings.customLocaleStrings) { if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr; - _.each(settings.customLocaleStrings, (overrides:MapArrayType , langcode:string) => { + _.each(settings.customLocaleStrings, (overrides , langcode) => { if (typeof overrides !== 'object') throw wrongFormatErr; _.each(overrides, (localeString:string|object, key:string) => { if (typeof localeString !== 'string') throw wrongFormatErr; diff --git a/src/node/security/OAuth2Provider.ts b/src/node/security/OAuth2Provider.ts index 3ef7d3a3b..6c069359d 100644 --- a/src/node/security/OAuth2Provider.ts +++ b/src/node/security/OAuth2Provider.ts @@ -3,7 +3,7 @@ import Provider, {Account, Configuration} from 'oidc-provider'; import {generateKeyPair, exportJWK, CryptoKey} from 'jose' import MemoryAdapter from "./OIDCAdapter"; import path from "path"; -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; import {IncomingForm} from 'formidable' import express from 'express'; import {format} from 'url' @@ -138,7 +138,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp } else if (token.kind === "ClientCredentials") { let extraParams: MapArrayType = {} - settings.sso.clients + settings.sso.clients && settings.sso.clients .filter((client:any) => client.client_id === token.clientId) .forEach((client:any) => { if(client.extraParams !== undefined) { diff --git a/src/node/server.ts b/src/node/server.ts index 793909bd2..5ed8d3073 100755 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -29,7 +29,7 @@ import pkg from '../package.json'; import {checkForMigration} from "../static/js/pluginfw/installer"; import axios from "axios"; -const settings = require('./utils/Settings'); +import settings from './utils/Settings'; let wtfnode: any; if (settings.dumpOnUncleanExit) { diff --git a/src/node/types/ArgsExpressType.ts b/src/node/types/ArgsExpressType.ts index cc409c033..996aee757 100644 --- a/src/node/types/ArgsExpressType.ts +++ b/src/node/types/ArgsExpressType.ts @@ -1,9 +1,10 @@ import {Express} from "express"; import {MapArrayType} from "./MapType"; +import {SettingsType} from "../utils/Settings"; export type ArgsExpressType = { app:Express, io: any, server:any - settings: MapArrayType + settings: SettingsType } diff --git a/src/node/utils/Abiword.ts b/src/node/utils/Abiword.ts index c0937fcd9..fd17497ed 100644 --- a/src/node/utils/Abiword.ts +++ b/src/node/utils/Abiword.ts @@ -24,7 +24,7 @@ import {AsyncQueueTask} from "../types/AsyncQueueTask"; const spawn = require('child_process').spawn; const async = require('async'); -const settings = require('./Settings'); +import settings from './Settings'; const os = require('os'); // on windows we have to spawn a process for each convertion, diff --git a/src/node/utils/AbsolutePaths.ts b/src/node/utils/AbsolutePaths.ts index c257440a1..6423ae4d7 100644 --- a/src/node/utils/AbsolutePaths.ts +++ b/src/node/utils/AbsolutePaths.ts @@ -18,9 +18,9 @@ * 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'); +import log4js from 'log4js'; +import path from 'path'; +import _ from 'underscore'; const absPathLogger = log4js.getLogger('AbsolutePaths'); @@ -74,7 +74,7 @@ const popIfEndsWith = (stringArray: string[], lastDesiredElements: string[]): st * @return {string} The identified absolute base path. If such path cannot be * identified, prints a log and exits the application. */ -exports.findEtherpadRoot = () => { +export const findEtherpadRoot = () => { if (etherpadRoot != null) { return etherpadRoot; } @@ -130,12 +130,12 @@ exports.findEtherpadRoot = () => { * it is returned unchanged. Otherwise it is interpreted * relative to exports.root. */ -exports.makeAbsolute = (somePath: string) => { +export const makeAbsolute = (somePath: string) => { if (path.isAbsolute(somePath)) { return somePath; } - const rewrittenPath = path.join(exports.findEtherpadRoot(), somePath); + const rewrittenPath = path.join(findEtherpadRoot(), somePath); absPathLogger.debug(`Relative path "${somePath}" can be rewritten to "${rewrittenPath}"`); return rewrittenPath; @@ -149,7 +149,7 @@ exports.makeAbsolute = (somePath: string) => { * a subdirectory of the base one * @return {boolean} */ -exports.isSubdir = (parent: string, arbitraryDir: string): boolean => { +export const 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); return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); diff --git a/src/node/utils/Cli.ts b/src/node/utils/Cli.ts index 1579dd3ce..a441c4db3 100644 --- a/src/node/utils/Cli.ts +++ b/src/node/utils/Cli.ts @@ -21,32 +21,33 @@ */ // An object containing the parsed command-line options -exports.argv = {}; -const argv = process.argv.slice(2); +export const argv: Record = {}; + +const argvInternal = process.argv.slice(2); let arg, prevArg; // Loop through args -for (let i = 0; i < argv.length; i++) { - arg = argv[i]; +for (let i = 0; i < argvInternal.length; i++) { + arg = argvInternal[i]; // Override location of settings.json file - if (prevArg === '--settings' || prevArg === '-s') { - exports.argv.settings = arg; + if (prevArg && prevArg === '--settings' || prevArg === '-s') { + argv.settings = arg; } // Override location of credentials.json file - if (prevArg === '--credentials') { + if (prevArg && prevArg === '--credentials') { exports.argv.credentials = arg; } // Override location of settings.json file - if (prevArg === '--sessionkey') { + if (prevArg && prevArg === '--sessionkey') { exports.argv.sessionkey = arg; } // Override location of APIKEY.txt file - if (prevArg === '--apikey') { + if (prevArg && prevArg === '--apikey') { exports.argv.apikey = arg; } diff --git a/src/node/utils/LibreOffice.ts b/src/node/utils/LibreOffice.ts index e89ebe460..e73fd144c 100644 --- a/src/node/utils/LibreOffice.ts +++ b/src/node/utils/LibreOffice.ts @@ -23,7 +23,7 @@ const log4js = require('log4js'); const os = require('os'); const path = require('path'); const runCmd = require('./run_cmd'); -const settings = require('./Settings'); +import settings from './Settings'; const logger = log4js.getLogger('LibreOffice'); diff --git a/src/node/utils/Minify.ts b/src/node/utils/Minify.ts index dbd4b247b..8747ff04b 100644 --- a/src/node/utils/Minify.ts +++ b/src/node/utils/Minify.ts @@ -26,7 +26,7 @@ import mime from 'mime-types'; import log4js from 'log4js'; import {compressCSS, compressJS} from './MinifyWorker' -const settings = require('./Settings'); +import settings from './Settings'; import {promises as fs} from 'fs'; import path from 'node:path'; const plugins = require('../../static/js/pluginfw/plugin_defs'); diff --git a/src/node/utils/Settings.ts b/src/node/utils/Settings.ts index e5f3fa302..38aa7dbc5 100644 --- a/src/node/utils/Settings.ts +++ b/src/node/utils/Settings.ts @@ -28,20 +28,19 @@ */ import {MapArrayType} from "../types/MapType"; -import {SettingsNode, SettingsTree} from "./SettingsTree"; -import {coerce} from "semver"; +import {SettingsNode} from "./SettingsTree"; -const absolutePaths = require('./AbsolutePaths'); +import * as absolutePaths from './AbsolutePaths'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -const argv = require('./Cli').argv; +import {argv} from './Cli' import jsonminify from 'jsonminify'; import log4js from 'log4js'; import randomString from './randomstring'; const suppressDisableMsg = ' -- To suppress these warning messages change ' + 'suppressErrorsInPadText to true in your settings.json\n'; -const _ = require('underscore'); +import _ from 'underscore'; const logger = log4js.getLogger('settings'); @@ -63,7 +62,7 @@ const defaultLogLevel = 'INFO'; const defaultLogLayoutType = 'colored'; const initLogging = (config: any) => { - // log4js.configure() modifies exports.logconfig so check for equality first. + // log4js.configure() modifies settings.logconfig so check for equality first. log4js.configure(config); log4js.getLogger('console'); @@ -78,75 +77,278 @@ const initLogging = (config: any) => { // with the user's chosen log level and logger config after the settings have been loaded. initLogging(defaultLogConfig(defaultLogLevel, defaultLogLayoutType)); -/* Root path of the installation */ -exports.root = absolutePaths.findEtherpadRoot(); -logger.info('All relative paths will be interpreted relative to the identified ' + - `Etherpad base dir: ${exports.root}`); -exports.settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); -exports.credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); +// Parse func + + /** - * The app title, visible e.g. in the browser window + * - reads the JSON configuration file settingsFilename from disk + * - strips the comments + * - replaces environment variables calling lookupEnvironmentVariables() + * - returns a parsed Javascript object + * + * The isSettings variable only controls the error logging. */ -exports.title = 'Etherpad'; +const parseSettings = (settingsFilename: string, isSettings: boolean) => { + let settingsStr = ''; -/** - * Pathname of the favicon you want to use. If null, the skin's favicon is - * used if one is provided by the skin, otherwise the default Etherpad favicon - * is used. If this is a relative path it is interpreted as relative to the - * Etherpad root directory. - */ -exports.favicon = null; + let settingsType, notFoundMessage, notFoundFunction; -exports.ttl = { + if (isSettings) { + settingsType = 'settings'; + notFoundMessage = 'Continuing using defaults!'; + notFoundFunction = logger.warn.bind(logger); + } else { + settingsType = 'credentials'; + notFoundMessage = 'Ignoring.'; + notFoundFunction = logger.info.bind(logger); + } + + try { + // read the settings file + settingsStr = fs.readFileSync(settingsFilename).toString(); + } catch (e) { + notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); + + // or maybe undefined! + return null; + } + + try { + settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); + + const settings = JSON.parse(settingsStr); + + logger.info(`${settingsType} loaded from: ${settingsFilename}`); + + return lookupEnvironmentVariables(settings); + } catch (e: any) { + logger.error(`There was an error processing your ${settingsType} ` + + `file from ${settingsFilename}: ${e.message}`); + + process.exit(1); + } +}; + + +// Provide git version if available +export const getGitCommit = () => { + let version = ''; + try { + let rootPath = settings.root; + if (fs.lstatSync(`${rootPath}/.git`).isFile()) { + rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); + rootPath = rootPath.split(' ').pop()?.trim() ?? ''; + } else { + rootPath += '/.git'; + } + const ref = fs.readFileSync(`${rootPath}/HEAD`, 'utf-8'); + if (ref.startsWith('ref: ')) { + const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`; + version = fs.readFileSync(refPath, 'utf-8'); + } else { + version = ref; + } + version = version.substring(0, 7); + } catch (e: any) { + logger.warn(`Can't get git version for server header\n${e.message}`); + } + return version; +}; + +export type SettingsType = { + root: string, + settingsFilename: string, + credentialsFilename: string, + title: string, + favicon: string | null, + ttl: { + AccessToken: number, + AuthorizationCode: number, + ClientCredentials: number, + IdToken: number, + RefreshToken: number, + }, + updateServer: string, + enableDarkMode: boolean, + skinName: string | null, + skinVariants: string, + ip: string, + port: number | string, + suppressErrorsInPadText: boolean, + ssl: false | { + key: string, + cert: string, + ca: string | null, + }, + socketTransportProtocols: any[], + socketIo: { + maxHttpBufferSize: number, + }, + authenticationMethod: string, + dbType: string, + dbSettings: any, + defaultPadText: string, + padOptions: { + noColors: boolean, + showControls: boolean, + showChat: boolean, + showLineNumbers: boolean, + useMonospaceFont: boolean, + userName: string | null, + userColor: string | null, + rtl: boolean, + alwaysShowChat: boolean, + chatAndUsers: boolean, + lang: string | null, + }, + enableMetrics: boolean, + padShortcutEnabled: { + altF9: boolean, + altC: boolean, + delete: boolean, + cmdShift2: boolean, + return: boolean, + esc: boolean, + cmdS: boolean, + tab: boolean, + cmdZ: boolean, + cmdY: boolean, + cmdB: boolean, + cmdI: boolean, + cmdU: boolean, + cmd5: boolean, + cmdShiftL: boolean, + cmdShiftN: boolean, + cmdShift1: boolean, + cmdShiftC: boolean, + cmdH: boolean, + ctrlHome: boolean, + pageUp: boolean, + pageDown: boolean, + }, + toolbar: { + left: string[][], + right: string[][], + timeslider: string[][], + }, + requireSession: boolean, + editOnly: boolean, + maxAge: number, + minify: boolean, + abiword: string | null, + soffice: string | null, + allowUnknownFileEnds: boolean, + loglevel: string, + logLayoutType: string, + disableIPlogging: boolean, + automaticReconnectionTimeout: number, + loadTest: boolean, + dumpOnUncleanExit: boolean, + indentationOnNewLine: boolean, + logconfig: any | null, + sessionKey: string | null, + trustProxy: boolean, + cookie: { + keyRotationInterval: number, + sameSite: boolean | "lax" | "strict" | "none" | undefined, + sessionLifetime: number, + sessionRefreshInterval: number, + }, + requireAuthentication: boolean, + requireAuthorization: boolean, + users: Record, + sso: { + issuer: string, + clients?: {client_id: string}[] + }, + showSettingsInAdminPage: boolean, + cleanup: { + enabled: boolean, + keepRevisions: number, + }, + scrollWhenFocusLineIsOutOfViewport: { + percentage: { + editionAboveViewport: number, + editionBelowViewport: number, + }, + duration: number, + percentageToScrollWhenUserPressesArrowUp: number, + scrollWhenCaretIsInTheLastLineOfViewport: boolean, + }, + exposeVersion: boolean, + customLocaleStrings: Record, + importExportRateLimiting: { + windowMs?: number, + max: number, + }, + commitRateLimiting: { + duration: number, + points: number, + }, + importMaxFileSize: number, + enableAdminUITests: boolean, + lowerCasePadIds: boolean, + randomVersionString: string, + gitVersion: string + getPublicSettings: () => Pick, +} + +const settings: SettingsType = { + /* Root path of the installation */ + root: absolutePaths.findEtherpadRoot(), + settingsFilename: absolutePaths.makeAbsolute(argv.settings || 'settings.json'), + credentialsFilename: absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'), + /** + * The app title, visible e.g. in the browser window + */ + title: 'Etherpad', + /** + * Pathname of the favicon you want to use. If null, the skin's favicon is + * used if one is provided by the skin, otherwise the default Etherpad favicon + * is used. If this is a relative path it is interpreted as relative to the + * Etherpad root directory. + */ + favicon: null, + ttl: { AccessToken: 1 * 60 * 60, // 1 hour in seconds AuthorizationCode: 10 * 60, // 10 minutes in seconds ClientCredentials: 1 * 60 * 60, // 1 hour in seconds IdToken: 1 * 60 * 60, // 1 hour in seconds RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds -} - -exports.updateServer = "https://static.etherpad.org" - -exports.enableDarkMode = true; - -/* + }, + updateServer: "https://static.etherpad.org", + enableDarkMode: true, + /* * Skin name. * * Initialized to null, so we can spot an old configuration file and invite the * user to update it before falling back to the default. */ -exports.skinName = null; - -exports.skinVariants = 'super-light-toolbar super-light-editor light-background'; - -/** - * The IP ep-lite should listen to - */ -exports.ip = '0.0.0.0'; - -/** - * The Port ep-lite should listen to - */ -exports.port = process.env.PORT || 9001; - -/** - * Should we suppress Error messages from being in Pad Contents - */ -exports.suppressErrorsInPadText = false; - -/** - * The SSL signed server key and the Certificate Authority's own certificate - * default case: ep-lite does *not* use SSL. A signed server key is not required in this case. - */ -exports.ssl = false; - -/** - * socket.io transport methods - **/ -exports.socketTransportProtocols = ['websocket', 'polling']; - -exports.socketIo = { + skinName: null, + skinVariants: 'super-light-toolbar super-light-editor light-background', + /** + * The IP ep-lite should listen to + */ + ip: '0.0.0.0', + /** + * The Port ep-lite should listen to + */ + port: process.env.PORT || 9001, + /** + * Should we suppress Error messages from being in Pad Contents + */ + suppressErrorsInPadText: false, + /** + * The SSL signed server key and the Certificate Authority's own certificate + * default case: ep-lite does *not* use SSL. A signed server key is not required in this case. + */ + ssl: false, + /** + * socket.io transport methods + **/ + socketTransportProtocols: ['websocket', 'polling'], + socketIo: { /** * Maximum permitted client message size (in bytes). * @@ -156,42 +358,36 @@ exports.socketIo = { * (malicious clients can exhaust memory). */ maxHttpBufferSize: 50000, -}; - - -/* + }, + /* The authentication method used by the server. The default value is sso If you want to use the old authentication system, change this to apikey */ -exports.authenticationMethod = 'sso' - - -/* + authenticationMethod: 'sso', + /* * The Type of the database */ -exports.dbType = 'rustydb'; -/** - * This setting is passed with dbType to ueberDB to set up the database - */ -exports.dbSettings = {filename: path.join(exports.root, 'var/rusty.db')}; - -/** - * The default Text of a new pad - */ -exports.defaultPadText = [ + dbType: 'rustydb', + /** + * This setting is passed with dbType to ueberDB to set up the database + */ + dbSettings: null, + /** + * The default Text of a new pad + */ + defaultPadText: [ 'Welcome to Etherpad!', '', 'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' + 'text. This allows you to collaborate seamlessly on documents!', '', 'Etherpad on Github: https://github.com/ether/etherpad-lite', -].join('\n'); - -/** - * The default Pad Settings for a user (Can be overridden by changing the setting - */ -exports.padOptions = { + ].join('\n'), + /** + * The default Pad Settings for a user (Can be overridden by changing the setting + */ + padOptions: { noColors: false, showControls: true, showChat: true, @@ -203,18 +399,15 @@ exports.padOptions = { alwaysShowChat: false, chatAndUsers: false, lang: null, -}; - - -/** - * Wether to enable the /stats endpoint. The functionality in the admin menu is untouched for this. - */ -exports.enableMetrics = true - -/** - * Whether certain shortcut keys are enabled for a user in the pad - */ -exports.padShortcutEnabled = { + }, + /** + * Wether to enable the /stats endpoint. The functionality in the admin menu is untouched for this. + */ + enableMetrics: true, + /** + * Whether certain shortcut keys are enabled for a user in the pad + */ + padShortcutEnabled: { altF9: true, altC: true, delete: true, @@ -237,208 +430,167 @@ exports.padShortcutEnabled = { ctrlHome: true, pageUp: true, pageDown: true, -}; - -/** - * The toolbar buttons and order. - */ -exports.toolbar = { + }, + /** + * The toolbar buttons and order. + */ + toolbar: { left: [ - ['bold', 'italic', 'underline', 'strikethrough'], - ['orderedlist', 'unorderedlist', 'indent', 'outdent'], - ['undo', 'redo'], - ['clearauthorship'], + ['bold', 'italic', 'underline', 'strikethrough'], + ['orderedlist', 'unorderedlist', 'indent', 'outdent'], + ['undo', 'redo'], + ['clearauthorship'], ], right: [ - ['importexport', 'timeslider', 'savedrevision'], - ['settings', 'embed', 'home'], - ['showusers'], + ['importexport', 'timeslider', 'savedrevision'], + ['settings', 'embed', 'home'], + ['showusers'], ], timeslider: [ - ['timeslider_export', 'timeslider_settings', 'timeslider_returnToPad'], + ['timeslider_export', 'timeslider_settings', 'timeslider_returnToPad'], ], -}; - -/** - * A flag that requires any user to have a valid session (via the api) before accessing a pad - */ -exports.requireSession = false; - -/** - * A flag that prevents users from creating new pads - */ -exports.editOnly = false; - -/** - * Max age that responses will have (affects caching layer). - */ -exports.maxAge = 1000 * 60 * 60 * 6; // 6 hours - -/** - * A flag that shows if minification is enabled or not - */ -exports.minify = true; - -/** - * The path of the abiword executable - */ -exports.abiword = null; - -/** - * The path of the libreoffice executable - */ -exports.soffice = null; - -/** - * Should we support none natively supported file types on import? - */ -exports.allowUnknownFileEnds = true; - -/** - * The log level of log4js - */ -exports.loglevel = defaultLogLevel; - -/** - * The log layout type of log4js - */ -exports.logLayoutType = defaultLogLayoutType; - -/** - * Disable IP logging - */ -exports.disableIPlogging = false; - -/** - * Number of seconds to automatically reconnect pad - */ -exports.automaticReconnectionTimeout = 0; - -/** - * Disable Load Testing - */ -exports.loadTest = false; - -/** - * Disable dump of objects preventing a clean exit - */ -exports.dumpOnUncleanExit = false; - -/** - * Enable indentation on new lines - */ -exports.indentationOnNewLine = true; - -/* + }, + /** + * A flag that requires any user to have a valid session (via the api) before accessing a pad + */ + requireSession: false, + /** + * A flag that prevents users from creating new pads + */ + editOnly: false, + /** + * Max age that responses will have (affects caching layer). + */ + maxAge: 1000 * 60 * 60 * 6, // 6 hours + /** + * A flag that shows if minification is enabled or not + */ + minify: true, + /** + * The path of the abiword executable + */ + abiword: null, + /** + * The path of the libreoffice executable + */ + soffice: null, + /** + * Should we support none natively supported file types on import? + */ + allowUnknownFileEnds: true, + /** + * The log level of log4js + */ + loglevel: defaultLogLevel, + /** + * The log layout type of log4js + */ + logLayoutType: defaultLogLayoutType, + /** + * Disable IP logging + */ + disableIPlogging: false, + /** + * Number of seconds to automatically reconnect pad + */ + automaticReconnectionTimeout: 0, + /** + * Disable Load Testing + */ + loadTest: false, + /** + * Disable dump of objects preventing a clean exit + */ + dumpOnUncleanExit: false, + /** + * Enable indentation on new lines + */ + indentationOnNewLine: true, + /* * log4js appender configuration */ -exports.logconfig = null; - -/* + logconfig: null, + /* * Deprecated cookie signing key. */ -exports.sessionKey = null; - -/* + sessionKey: null, + /* * Trust Proxy, whether or not trust the x-forwarded-for header. */ -exports.trustProxy = false; - -/* + trustProxy: false, + /* * Settings controlling the session cookie issued by Etherpad. */ -exports.cookie = { + cookie: { keyRotationInterval: 1 * 24 * 60 * 60 * 1000, - /* - * Value of the SameSite cookie property. "Lax" is recommended unless - * Etherpad will be embedded in an iframe from another site, in which case - * this must be set to "None". Note: "None" will not work (the browser will - * not send the cookie to Etherpad) unless https is used to access Etherpad - * (either directly or via a reverse proxy with "trustProxy" set to true). - * - * "Strict" is not recommended because it has few security benefits but - * significant usability drawbacks vs. "Lax". See - * https://stackoverflow.com/q/41841880 for discussion. - */ - sameSite: 'Lax', + sameSite: 'lax', sessionLifetime: 10 * 24 * 60 * 60 * 1000, sessionRefreshInterval: 1 * 24 * 60 * 60 * 1000, -}; - -/* + }, + /* * This setting is used if you need authentication and/or * authorization. Note: /admin always requires authentication, and * either authorization by a module, or a user with is_admin set */ -exports.requireAuthentication = false; -exports.requireAuthorization = false; -exports.users = {}; - -/* + requireAuthentication: false, + requireAuthorization: false, + users: {}, + /* * This setting is used for configuring sso */ -exports.sso = { + sso: { issuer: "http://localhost:9001" -} - -/* + }, + /* * Show settings in admin page, by default it is true */ -exports.showSettingsInAdminPage = true; - -/* + showSettingsInAdminPage: true, + /* * Settings for cleanup of pads */ -exports.cleanup = { - enabled: false, - keepRevisions: 100, -} - -/* + cleanup: { + enabled: false, + keepRevisions: 100, + }, + /* * By default, when caret is moved out of viewport, it scrolls the minimum * height needed to make this line visible. */ -exports.scrollWhenFocusLineIsOutOfViewport = { + scrollWhenFocusLineIsOutOfViewport: { /* - * Percentage of viewport height to be additionally scrolled. - */ + * Percentage of viewport height to be additionally scrolled. + */ percentage: { - editionAboveViewport: 0, - editionBelowViewport: 0, + editionAboveViewport: 0, + editionBelowViewport: 0, }, - /* - * Time (in milliseconds) used to animate the scroll transition. Set to 0 to - * disable animation - */ + * Time (in milliseconds) used to animate the scroll transition. Set to 0 to + * disable animation + */ duration: 0, - /* * Percentage of viewport height to be additionally scrolled when user presses arrow up * in the line of the top of the viewport. */ percentageToScrollWhenUserPressesArrowUp: 0, - /* - * Flag to control if it should scroll when user places the caret in the last - * line of the viewport - */ + * Flag to control if it should scroll when user places the caret in the last + * line of the viewport + */ scrollWhenCaretIsInTheLastLineOfViewport: false, -}; - -/* + }, + /* * Expose Etherpad version in the web interface and in the Server http header. * * Do not enable on production machines. */ -exports.exposeVersion = false; - -/* + exposeVersion: false, + /* * Override any strings found in locale directories */ -exports.customLocaleStrings = {}; - -/* + customLocaleStrings: {}, + /* * From Etherpad 1.8.3 onwards, import and export of pads is always rate * limited. * @@ -447,15 +599,13 @@ exports.customLocaleStrings = {}; * * See https://github.com/nfriedly/express-rate-limit for more options */ -exports.importExportRateLimiting = { + importExportRateLimiting: { // duration of the rate limit window (milliseconds) windowMs: 90000, - // maximum number of requests per IP to allow during the rate limit window max: 10, -}; - -/* + }, + /* * From Etherpad 1.9.0 onwards, commits from individual users are rate limited * * The default is to allow at most 10 changes per IP in a 1 second window. @@ -463,53 +613,71 @@ exports.importExportRateLimiting = { * * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options */ -exports.commitRateLimiting = { + commitRateLimiting: { // duration of the rate limit window (seconds) duration: 1, - - // maximum number of chanes per IP to allow during the rate limit window + // maximum number of changes per IP to allow during the rate limit window points: 10, -}; - -/* + }, + /* * From Etherpad 1.8.3 onwards, the maximum allowed size for a single imported * file is always bounded. * * File size is specified in bytes. Default is 50 MB. */ -exports.importMaxFileSize = 50 * 1024 * 1024; - -/* + importMaxFileSize: 50 * 1024 * 1024, + /* * Disable Admin UI tests */ -exports.enableAdminUITests = false; - -/* + enableAdminUITests: false, + /* * Enable auto conversion of pad Ids to lowercase. * e.g. /p/EtHeRpAd to /p/etherpad */ -exports.lowerCasePadIds = false; + lowerCasePadIds: false, + randomVersionString: '2123', + getPublicSettings: () => { + return { + gitVersion: settings.gitVersion, + toolbar: settings.toolbar, + exposeVersion: settings.exposeVersion, + randomVersionString: settings.randomVersionString, + title: settings.title, + skinName: settings.skinName, + skinVariants: settings.skinVariants, + } + }, + gitVersion: getGitCommit(), +} + +export default settings; + +/** + * This setting is passed with dbType to ueberDB to set up the database + */ +settings.dbSettings = {filename: path.join(settings.root, 'var/rusty.db')}; +// END OF SETTINGS // checks if abiword is avaiable -exports.abiwordAvailable = () => { - if (exports.abiword != null) { +export const abiwordAvailable = () => { + if (settings.abiword != null) { return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { return 'no'; } }; -exports.sofficeAvailable = () => { - if (exports.soffice != null) { +export const sofficeAvailable = () => { + if (settings.soffice != null) { return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { return 'no'; } }; -exports.exportAvailable = () => { - const abiword = exports.abiwordAvailable(); - const soffice = exports.sofficeAvailable(); +export const exportAvailable = () => { + const abiword = abiwordAvailable(); + const soffice = sofficeAvailable(); if (abiword === 'no' && soffice === 'no') { return 'no'; @@ -521,33 +689,9 @@ exports.exportAvailable = () => { } }; -// Provide git version if available -exports.getGitCommit = () => { - let version = ''; - try { - let rootPath = exports.root; - if (fs.lstatSync(`${rootPath}/.git`).isFile()) { - rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); - rootPath = rootPath.split(' ').pop().trim(); - } else { - rootPath += '/.git'; - } - const ref = fs.readFileSync(`${rootPath}/HEAD`, 'utf-8'); - if (ref.startsWith('ref: ')) { - const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`; - version = fs.readFileSync(refPath, 'utf-8'); - } else { - version = ref; - } - version = version.substring(0, 7); - } catch (e: any) { - logger.warn(`Can't get git version for server header\n${e.message}`); - } - return version; -}; // Return etherpad version from package.json -exports.getEpVersion = () => require('../../package.json').version; +export const getEpVersion = () => require('../../package.json').version; @@ -572,11 +716,14 @@ const storeSettings = (settingsObj: any) => { // we know this setting, so we overwrite it // or it's a settings hash, specific to a plugin - if (exports[i] !== undefined || i.indexOf('ep_') === 0) { + // @ts-ignore + if (settings[i] !== undefined || i.indexOf('ep_') === 0) { if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) { - exports[i] = _.defaults(settingsObj[i], exports[i]); + // @ts-ignore + settings[i] = _.defaults(settingsObj[i], settings[i]); } else { - exports[i] = settingsObj[i]; + // @ts-ignore + settings[i] = settingsObj[i]; } } else { // this setting is unknown, output a warning and throw it away @@ -782,158 +929,108 @@ const lookupEnvironmentVariables = (obj: MapArrayType) => { }; -/** - * - reads the JSON configuration file settingsFilename from disk - * - strips the comments - * - replaces environment variables calling lookupEnvironmentVariables() - * - returns a parsed Javascript object - * - * The isSettings variable only controls the error logging. - */ -const parseSettings = (settingsFilename: string, isSettings: boolean) => { - let settingsStr = ''; - let settingsType, notFoundMessage, notFoundFunction; - - if (isSettings) { - settingsType = 'settings'; - notFoundMessage = 'Continuing using defaults!'; - notFoundFunction = logger.warn.bind(logger); - } else { - settingsType = 'credentials'; - notFoundMessage = 'Ignoring.'; - notFoundFunction = logger.info.bind(logger); - } - - try { - // read the settings file - settingsStr = fs.readFileSync(settingsFilename).toString(); - } catch (e) { - notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); - - // or maybe undefined! - return null; - } - - try { - settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); - - const settings = JSON.parse(settingsStr); - - logger.info(`${settingsType} loaded from: ${settingsFilename}`); - - return lookupEnvironmentVariables(settings); - } catch (e: any) { - logger.error(`There was an error processing your ${settingsType} ` + - `file from ${settingsFilename}: ${e.message}`); - - process.exit(1); - } -}; - -exports.reloadSettings = () => { - const settings = parseSettings(exports.settingsFilename, true); - const credentials = parseSettings(exports.credentialsFilename, false); - storeSettings(settings); +export const reloadSettings = () => { + const settingsParsed = parseSettings(settings?.settingsFilename, true); + const credentials = parseSettings(settings.credentialsFilename, false); + storeSettings(settingsParsed); storeSettings(credentials); // Init logging config - exports.logconfig = defaultLogConfig( - exports.loglevel ? exports.loglevel : defaultLogLevel, - exports.logLayoutType ? exports.logLayoutType : defaultLogLayoutType + settings.logconfig = defaultLogConfig( + settings.loglevel ? settings.loglevel : defaultLogLevel, + settings.logLayoutType ? settings.logLayoutType : defaultLogLayoutType ); - logger.warn("loglevel: " + exports.loglevel); - logger.warn("logLayoutType: " + exports.logLayoutType); - initLogging(exports.logconfig); + logger.warn("loglevel: " + settings.loglevel); + logger.warn("logLayoutType: " + settings.logLayoutType); + initLogging(settings.logconfig); - if (!exports.skinName) { + if (!settings.skinName) { logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + 'update your settings.json. Falling back to the default "colibris".'); - exports.skinName = 'colibris'; + settings.skinName = 'colibris'; } - if (!exports.socketTransportProtocols.includes("websocket") || !exports.socketTransportProtocols.includes("polling")) { + if (!settings.socketTransportProtocols.includes("websocket") || !settings.socketTransportProtocols.includes("polling")) { logger.warn("Invalid socketTransportProtocols setting. Please check out settings.json.template and update your settings.json. Falling back to the default ['websocket', 'polling']."); - exports.socketTransportProtocols = ['websocket', 'polling']; + settings.socketTransportProtocols = ['websocket', 'polling']; } // checks if skinName has an acceptable value, otherwise falls back to "colibris" - if (exports.skinName) { - const skinBasePath = path.join(exports.root, 'src', 'static', 'skins'); - const countPieces = exports.skinName.split(path.sep).length; + if (settings.skinName) { + const skinBasePath = path.join(settings.root, 'src', 'static', 'skins'); + const countPieces = settings.skinName.split(path.sep).length; if (countPieces !== 1) { logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + - `not valid: "${exports.skinName}". Falling back to the default "colibris".`); + `not valid: "${settings.skinName}". Falling back to the default "colibris".`); - exports.skinName = 'colibris'; + settings.skinName = 'colibris'; } // informative variable, just for the log messages - let skinPath = path.join(skinBasePath, exports.skinName); + let skinPath = path.join(skinBasePath, settings.skinName); // what if someone sets skinName == ".." or "."? We catch him! - if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { + if (!absolutePaths.isSubdir(skinBasePath, skinPath)) { logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + 'Falling back to the default "colibris".'); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); + settings.skinName = 'colibris'; + skinPath = path.join(skinBasePath, settings.skinName); } - if (fs.existsSync(skinPath) === false) { + if (!fs.existsSync(skinPath)) { logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); + settings.skinName = 'colibris'; + skinPath = path.join(skinBasePath, settings.skinName); } - logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); + logger.info(`Using skin "${settings.skinName}" in dir: ${skinPath}`); } - if (exports.abiword) { + if (settings.abiword) { // Check abiword actually exists - if (exports.abiword != null) { - fs.exists(exports.abiword, (exists: boolean) => { - if (!exists) { - const abiwordError = 'Abiword does not exist at this path, check your settings file.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; - } - logger.error(`${abiwordError} File location: ${exports.abiword}`); - exports.abiword = null; - } - }); + fs.exists(settings.abiword, (exists: boolean) => { + if (!exists) { + const abiwordError = 'Abiword does not exist at this path, check your settings file.'; + if (!settings.suppressErrorsInPadText) { + settings.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; + } + logger.error(`${abiwordError} File location: ${settings.abiword}`); + settings.abiword = null; } + }); } - if (exports.soffice) { - fs.exists(exports.soffice, (exists: boolean) => { + if (settings.soffice) { + fs.exists(settings.soffice, (exists: boolean) => { if (!exists) { const sofficeError = 'soffice (libreoffice) does not exist at this path, check your settings file.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; + if (!settings.suppressErrorsInPadText) { + settings.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; } - logger.error(`${sofficeError} File location: ${exports.soffice}`); - exports.soffice = null; + logger.error(`${sofficeError} File location: ${settings.soffice}`); + settings.soffice = null; } }); } const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); - if (!exports.sessionKey) { + if (!settings.sessionKey) { try { - exports.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); + settings.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); logger.info(`Session key loaded from: ${sessionkeyFilename}`); } catch (err) { /* ignored */ } - const keyRotationEnabled = exports.cookie.keyRotationInterval && exports.cookie.sessionLifetime; - if (!exports.sessionKey && !keyRotationEnabled) { + const keyRotationEnabled = settings.cookie.keyRotationInterval && settings.cookie.sessionLifetime; + if (!settings.sessionKey && !keyRotationEnabled) { logger.info( `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); - exports.sessionKey = randomString(32); - fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); + settings.sessionKey = randomString(32); + fs.writeFileSync(sessionkeyFilename, settings.sessionKey, 'utf8'); } } else { logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' + @@ -941,28 +1038,28 @@ exports.reloadSettings = () => { 'If you are seeing this error after restarting using the Admin User ' + 'Interface then you can ignore this message.'); } - if (exports.sessionKey) { + if (settings.sessionKey) { logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + 'use automatic key rotation instead (see the cookie.keyRotationInterval setting).'); } - if (exports.dbType === 'dirty') { + if (settings.dbType === 'dirty') { const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; + if (!settings.suppressErrorsInPadText) { + settings.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; } - exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); + settings.dbSettings.filename = absolutePaths.makeAbsolute(settings.dbSettings.filename); + logger.warn(`${dirtyWarning} File location: ${settings.dbSettings.filename}`); } - if (exports.dbType === 'rustydb' || exports.dbType === "sqlite") { - exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - logger.warn(`File location: ${exports.dbSettings.filename}`); + if (settings.dbType === 'rustydb' || settings.dbType === "sqlite") { + settings.dbSettings.filename = absolutePaths.makeAbsolute(settings.dbSettings.filename); + logger.warn(`File location: ${settings.dbSettings.filename}`); } - if (exports.ip === '') { + if (settings.ip === '') { // using Unix socket for connectivity logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); @@ -979,13 +1076,13 @@ exports.reloadSettings = () => { * ACHTUNG: this may prevent caching HTTP proxies to work * TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead */ - exports.randomVersionString = randomString(4); - logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); + settings.randomVersionString = randomString(4); + logger.info(`Random string used for versioning assets: ${settings.randomVersionString}`); }; -exports.exportedForTestingOnly = { +export const exportedForTestingOnly = { parseSettings, }; // initially load settings -exports.reloadSettings(); +reloadSettings(); diff --git a/src/node/utils/UpdateCheck.ts b/src/node/utils/UpdateCheck.ts index 764bbcb27..da292e373 100644 --- a/src/node/utils/UpdateCheck.ts +++ b/src/node/utils/UpdateCheck.ts @@ -1,9 +1,9 @@ 'use strict'; import semver from 'semver'; -const settings = require('./Settings'); +import settings, {getEpVersion} from './Settings'; import axios from 'axios'; const headers = { - 'User-Agent': 'Etherpad/' + settings.getEpVersion(), + 'User-Agent': 'Etherpad/' + getEpVersion(), } type Infos = { @@ -45,7 +45,7 @@ export const getLatestVersion = () => { const needsUpdate = async (cb?: Function) => { try { const info = await loadEtherpadInformations() - if (semver.gt(info!.latestVersion, settings.getEpVersion())) { + if (semver.gt(info!.latestVersion, getEpVersion())) { if (cb) return cb(true); } } catch (err) { diff --git a/src/node/utils/run_cmd.ts b/src/node/utils/run_cmd.ts index 463b0f076..c7e37b78c 100644 --- a/src/node/utils/run_cmd.ts +++ b/src/node/utils/run_cmd.ts @@ -5,10 +5,10 @@ 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'); -const settings = require('./Settings'); +import spawn from 'cross-spawn'; +import log4js from 'log4js'; +import path from 'path'; +import settings from './Settings'; const logger = log4js.getLogger('runCmd'); @@ -123,7 +123,7 @@ module.exports = exports = (args: string[], opts:RunCMDOptions = {}) => { // process's `exit` handler so that we get a useful stack trace. const procFailedErr: Error & ErrorExtended = new Error(); - const proc: ChildProcess = spawn(args[0], args.slice(1), opts); + const proc: ChildProcess = spawn(args[0], args.slice(1), opts as any); const streams:[undefined, Readable|null, Readable|null] = [undefined, proc.stdout, proc.stderr]; let px: { reject: any; resolve: any; }; diff --git a/src/package.json b/src/package.json index 477ef4dec..7060626a5 100644 --- a/src/package.json +++ b/src/package.json @@ -85,6 +85,8 @@ "@playwright/test": "^1.54.1", "@types/async": "^3.2.24", "@types/cookie-parser": "^1.4.9", + "@types/cross-spawn": "^6.0.6", + "@types/ejs": "^3.1.5", "@types/express": "^5.0.0", "@types/express-session": "^1.18.2", "@types/formidable": "^3.4.5", diff --git a/src/static/js/pluginfw/LinkInstaller.ts b/src/static/js/pluginfw/LinkInstaller.ts index e5eaf0b5c..ddb8835ca 100644 --- a/src/static/js/pluginfw/LinkInstaller.ts +++ b/src/static/js/pluginfw/LinkInstaller.ts @@ -4,7 +4,7 @@ import {node_modules, pluginInstallPath} from "./installer"; import {accessSync, constants, rmSync, symlinkSync, unlinkSync} from "node:fs"; import {dependencies, name} from '../../../package.json' import {pathToFileURL} from 'node:url'; -const settings = require('../../../node/utils/Settings'); +import settings from '../../../node/utils/Settings'; import {readFileSync} from "fs"; export class LinkInstaller { diff --git a/src/static/js/pluginfw/installer.ts b/src/static/js/pluginfw/installer.ts index 98590e9e5..a85bc77f1 100644 --- a/src/static/js/pluginfw/installer.ts +++ b/src/static/js/pluginfw/installer.ts @@ -13,10 +13,13 @@ import {promises as fs} from "fs"; const plugins = require('./plugins'); const hooks = require('./hooks'); const runCmd = require('../../../node/utils/run_cmd'); -const settings = require('../../../node/utils/Settings'); +import settings, { + getEpVersion, + reloadSettings +} from '../../../node/utils/Settings'; import {LinkInstaller} from "./LinkInstaller"; -const {findEtherpadRoot} = require('../../../node/utils/AbsolutePaths'); +import {findEtherpadRoot} from '../../../node/utils/AbsolutePaths'; const logger = log4js.getLogger('plugins'); export const pluginInstallPath = path.join(settings.root, 'src','plugin_packages'); @@ -27,13 +30,13 @@ export const installedPluginsPath = path.join(settings.root, 'var/installed_plug const onAllTasksFinished = async () => { await plugins.update(); await persistInstalledPlugins(); - settings.reloadSettings(); + reloadSettings(); await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('restartServer'); }; const headers = { - 'User-Agent': `Etherpad/${settings.getEpVersion()}`, + 'User-Agent': `Etherpad/${getEpVersion()}`, }; let tasks = 0; diff --git a/src/static/js/pluginfw/plugins.ts b/src/static/js/pluginfw/plugins.ts index 97c1694e2..f3ca51427 100644 --- a/src/static/js/pluginfw/plugins.ts +++ b/src/static/js/pluginfw/plugins.ts @@ -9,7 +9,9 @@ const runCmd = require('../../../node/utils/run_cmd'); const tsort = require('./tsort'); const pluginUtils = require('./shared'); const defs = require('./plugin_defs'); -const settings = require('../../../node/utils/Settings'); +import settings, { + getEpVersion, +} from '../../../node/utils/Settings'; const logger = log4js.getLogger('plugins'); @@ -136,7 +138,7 @@ exports.getPackages = async () => { newDependencies['ep_etherpad-lite'] = { name: 'ep_etherpad-lite', - version: settings.getEpVersion(), + version: getEpVersion(), path: path.join(settings.root, 'node_modules/ep_etherpad-lite'), realPath: path.join(settings.root, 'src'), }; diff --git a/src/templates/pad.html b/src/templates/pad.html index abc9b7a7d..bb6962b89 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -1,6 +1,5 @@ <% - var settings = require("ep_etherpad-lite/node/utils/Settings") - , langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs + var langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs , pluginUtils = require('ep_etherpad-lite/static/js/pluginfw/shared') ; %> @@ -168,7 +167,7 @@

About

Powered by Etherpad - <% if (settings.exposeVersion) { %>(commit <%=settings.getGitCommit()%>)<% } %> + <% if (settings.exposeVersion) { %>(commit <%=settings.gitVersion()%>)<% } %> diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 02db40648..b5c673326 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -1,6 +1,5 @@ <% - var settings = require("ep_etherpad-lite/node/utils/Settings") - , langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs + var langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs %> diff --git a/src/tests/backend/common.ts b/src/tests/backend/common.ts index 271fb43d4..73bc3f781 100644 --- a/src/tests/backend/common.ts +++ b/src/tests/backend/common.ts @@ -10,7 +10,7 @@ import padutils from '../../static/js/pad_utils'; const process = require('process'); const server = require('../../node/server'); const setCookieParser = require('set-cookie-parser'); -const settings = require('../../node/utils/Settings'); +import settings from '../../node/utils/Settings'; import supertest from 'supertest'; import TestAgent from "supertest/lib/agent"; import {Http2Server} from "node:http2"; diff --git a/src/tests/backend/specs/Pad.ts b/src/tests/backend/specs/Pad.ts index b77bdf672..42bcc33d8 100644 --- a/src/tests/backend/specs/Pad.ts +++ b/src/tests/backend/specs/Pad.ts @@ -9,7 +9,7 @@ const authorManager = require('../../../node/db/AuthorManager'); const common = require('../common'); const padManager = require('../../../node/db/PadManager'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const settings = require('../../../node/utils/Settings'); +import settings from '../../../node/utils/Settings'; describe(__filename, function () { const backups:MapArrayType = {}; diff --git a/src/tests/backend/specs/api/importexportGetPost.ts b/src/tests/backend/specs/api/importexportGetPost.ts index 355699bc2..1e3fb5b02 100644 --- a/src/tests/backend/specs/api/importexportGetPost.ts +++ b/src/tests/backend/specs/api/importexportGetPost.ts @@ -11,7 +11,7 @@ import TestAgent from "supertest/lib/agent"; const assert = require('assert').strict; const common = require('../../common'); const fs = require('fs'); -const settings = require('../../../../node/utils/Settings'); +import settings from '../../../../node/utils/Settings'; const superagent = require('superagent'); const padManager = require('../../../../node/db/PadManager'); const plugins = require('../../../../static/js/pluginfw/plugin_defs'); diff --git a/src/tests/backend/specs/export.ts b/src/tests/backend/specs/export.ts index de436f88c..0abe24cda 100644 --- a/src/tests/backend/specs/export.ts +++ b/src/tests/backend/specs/export.ts @@ -4,7 +4,7 @@ import {MapArrayType} from "../../../node/types/MapType"; const common = require('../common'); const padManager = require('../../../node/db/PadManager'); -const settings = require('../../../node/utils/Settings'); +import settings from '../../../node/utils/Settings'; describe(__filename, function () { let agent:any; diff --git a/src/tests/backend/specs/favicon.ts b/src/tests/backend/specs/favicon.ts index 6b6230b4b..98042dcbf 100644 --- a/src/tests/backend/specs/favicon.ts +++ b/src/tests/backend/specs/favicon.ts @@ -7,7 +7,7 @@ const common = require('../common'); const fs = require('fs'); const fsp = fs.promises; const path = require('path'); -const settings = require('../../../node/utils/Settings'); +import settings from '../../../node/utils/Settings'; const superagent = require('superagent'); describe(__filename, function () { @@ -29,10 +29,13 @@ describe(__filename, function () { backupSettings = {...settings}; skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-')); settings.skinName = path.basename(skinDir); + }); afterEach(async function () { + // @ts-ignore delete settings.favicon; + // @ts-ignore delete settings.skinName; Object.assign(settings, backupSettings); try { diff --git a/src/tests/backend/specs/health.ts b/src/tests/backend/specs/health.ts index 97364a7e5..89ee3aad5 100644 --- a/src/tests/backend/specs/health.ts +++ b/src/tests/backend/specs/health.ts @@ -4,7 +4,9 @@ import {MapArrayType} from "../../../node/types/MapType"; const assert = require('assert').strict; const common = require('../common'); -const settings = require('../../../node/utils/Settings'); +import settings, { + getEpVersion +} from '../../../node/utils/Settings'; const superagent = require('superagent'); describe(__filename, function () { @@ -25,6 +27,7 @@ describe(__filename, function () { beforeEach(async function () { backup.settings = {}; for (const setting of ['requireAuthentication', 'requireAuthorization']) { + // @ts-ignore backup.settings[setting] = settings[setting]; } }); @@ -36,7 +39,7 @@ describe(__filename, function () { it('/health works', async function () { const res = await getHealth(); assert.equal(res.body.status, 'pass'); - assert.equal(res.body.releaseId, settings.getEpVersion()); + assert.equal(res.body.releaseId, getEpVersion()); }); it('auth is not required', async function () { diff --git a/src/tests/backend/specs/lowerCasePadIds.ts b/src/tests/backend/specs/lowerCasePadIds.ts index c85d16c3f..12e8ed831 100644 --- a/src/tests/backend/specs/lowerCasePadIds.ts +++ b/src/tests/backend/specs/lowerCasePadIds.ts @@ -3,7 +3,7 @@ const assert = require('assert').strict; const common = require('../common'); const padManager = require('../../../node/db/PadManager'); -const settings = require('../../../node/utils/Settings'); +import settings from '../../../node/utils/Settings'; describe(__filename, function () { let agent:any; diff --git a/src/tests/backend/specs/settings.ts b/src/tests/backend/specs/settings.ts index d6dcaf71a..89f4bc7b1 100644 --- a/src/tests/backend/specs/settings.ts +++ b/src/tests/backend/specs/settings.ts @@ -1,7 +1,7 @@ 'use strict'; const assert = require('assert').strict; -const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly; +import {exportedForTestingOnly} from '../../../node/utils/Settings' import path from 'path'; import process from 'process'; @@ -21,7 +21,7 @@ describe(__filename, function () { before(async function () { for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val; delete process.env.UNSET_VAR; - settings = parseSettings(path.join(__dirname, 'settings.json'), true); + settings = exportedForTestingOnly.parseSettings(path.join(__dirname, 'settings.json'), true); assert(settings != null); }); @@ -67,26 +67,26 @@ describe(__filename, function () { }) it('should parse plugin settings', async function () { - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.equal(settings.ADMIN.PASSWORD, "test"); + let settings = exportedForTestingOnly.parseSettings(path.join(__dirname, 'settings.json'), true); + assert.equal(settings!.ADMIN.PASSWORD, "test"); }) it('should bundle settings with same path', async function () { process.env["EP__ADMIN__USERNAME"] = "test" - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.deepEqual(settings.ADMIN, {PASSWORD: "test", USERNAME: "test"}); + let settings = exportedForTestingOnly.parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings!.ADMIN, {PASSWORD: "test", USERNAME: "test"}); }) it("Can set the ep themes", async function () { process.env["EP__ep_themes__default_theme"] = "hacker" - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.deepEqual(settings.ep_themes, {"default_theme": "hacker"}); + let settings = exportedForTestingOnly.parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings!.ep_themes, {"default_theme": "hacker"}); }) it("can set the ep_webrtc settings", async function () { process.env["EP__ep_webrtc__enabled"] = "true" - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.deepEqual(settings.ep_webrtc, {"enabled": true}); + let settings = exportedForTestingOnly.parseSettings(path.join(__dirname, 'settings.json'), true); + assert.deepEqual(settings!.ep_webrtc, {"enabled": true}); }) }) }); diff --git a/src/tests/backend/specs/socketio.ts b/src/tests/backend/specs/socketio.ts index 556f28a69..b235974aa 100644 --- a/src/tests/backend/specs/socketio.ts +++ b/src/tests/backend/specs/socketio.ts @@ -7,7 +7,7 @@ const common = require('../common'); const padManager = require('../../../node/db/PadManager'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); import readOnlyManager from '../../../node/db/ReadOnlyManager'; -const settings = require('../../../node/utils/Settings'); +import settings from '../../../node/utils/Settings'; const socketIoRouter = require('../../../node/handler/SocketIORouter'); describe(__filename, function () { @@ -35,6 +35,7 @@ describe(__filename, function () { } backups.settings = {}; for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users']) { + // @ts-ignore backups.settings[setting] = settings[setting]; } settings.editOnly = false; diff --git a/src/tests/backend/specs/specialpages.ts b/src/tests/backend/specs/specialpages.ts index fbb446c49..edb675ee5 100644 --- a/src/tests/backend/specs/specialpages.ts +++ b/src/tests/backend/specs/specialpages.ts @@ -3,7 +3,7 @@ import {MapArrayType} from "../../../node/types/MapType"; const common = require('../common'); -const settings = require('../../../node/utils/Settings'); +import settings from '../../../node/utils/Settings'; @@ -15,6 +15,7 @@ describe(__filename, function () { beforeEach(async function () { backups.settings = {}; for (const setting of ['requireAuthentication', 'requireAuthorization']) { + // @ts-ignore backups.settings[setting] = settings[setting]; } settings.requireAuthentication = false; diff --git a/src/tests/backend/specs/webaccess.ts b/src/tests/backend/specs/webaccess.ts index 96c2265fc..919bb1a41 100644 --- a/src/tests/backend/specs/webaccess.ts +++ b/src/tests/backend/specs/webaccess.ts @@ -7,7 +7,7 @@ import {SettingsUser} from "../../../node/types/SettingsUser"; const assert = require('assert').strict; const common = require('../common'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); -const settings = require('../../../node/utils/Settings'); +import settings from '../../../node/utils/Settings'; describe(__filename, function () { this.timeout(30000); @@ -32,6 +32,7 @@ describe(__filename, function () { } backups.settings = {}; for (const setting of ['requireAuthentication', 'requireAuthorization', 'users']) { + // @ts-ignore backups.settings[setting] = settings[setting]; } settings.requireAuthentication = false;