diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5e4f96ae5b..89d9925e08b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -297,6 +297,9 @@ importers: '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 + '@types/jsonminify': + specifier: ^0.4.3 + version: 0.4.3 '@types/jsonwebtoken': specifier: ^9.0.6 version: 9.0.6 @@ -1516,6 +1519,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonminify@0.4.3': + resolution: {integrity: sha512-+oz7EbPz1Nwmn/sr3UztgXpRhdFpvFrjGi5ictEYxUri5ZvQMTcdTi36MTfD/gCb1A5xhJKdH8Hwz2uz5k6s9A==} + '@types/jsonwebtoken@9.0.6': resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} @@ -5703,6 +5709,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonminify@0.4.3': {} + '@types/jsonwebtoken@9.0.6': dependencies: '@types/node': 22.4.0 diff --git a/src/node/db/AuthorManager.ts b/src/node/db/AuthorManager.ts index 4bcfa2c0d4a..41fb93251a0 100644 --- a/src/node/db/AuthorManager.ts +++ b/src/node/db/AuthorManager.ts @@ -19,7 +19,7 @@ * limitations under the License. */ -const db = require('./DB'); +import db from './DB'; const CustomError = require('../utils/customError'); const hooks = require('../../static/js/pluginfw/hooks'); import padutils, {randomString} from "../../static/js/pad_utils"; @@ -131,6 +131,7 @@ const mapAuthorWithDBKey = async (mapperkey: string, mapper:string) => { // there is an author with this mapper // update the timestamp of this author + // @ts-ignore await db.setSub(`globalAuthor:${author}`, ['timestamp'], Date.now()); // return the author @@ -222,6 +223,7 @@ exports.getAuthor = async (author: string) => await db.get(`globalAuthor:${autho * Returns the color Id of the author * @param {String} author The id of the author */ +// @ts-ignore exports.getAuthorColorId = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['colorId']); /** @@ -230,12 +232,14 @@ exports.getAuthorColorId = async (author: string) => await db.getSub(`globalAuth * @param {String} colorId The color id of the author */ exports.setAuthorColorId = async (author: string, colorId: string) => await db.setSub( - `globalAuthor:${author}`, ['colorId'], colorId); + // @ts-ignore + `globalAuthor:${author}`, ['colorId'], colorId); /** * Returns the name of the author * @param {String} author The id of the author */ +// @ts-ignore exports.getAuthorName = async (author: string) => await db.getSub(`globalAuthor:${author}`, ['name']); /** @@ -244,7 +248,8 @@ exports.getAuthorName = async (author: string) => await db.getSub(`globalAuthor: * @param {String} name The name of the author */ exports.setAuthorName = async (author: string, name: string) => await db.setSub( - `globalAuthor:${author}`, ['name'], name); + // @ts-ignore + `globalAuthor:${author}`, ['name'], name); /** * Returns an array of all pads this author contributed to diff --git a/src/node/db/DB.ts b/src/node/db/DB.ts index 663946cd6ad..785f586d428 100644 --- a/src/node/db/DB.ts +++ b/src/node/db/DB.ts @@ -22,39 +22,57 @@ */ import {Database} from 'ueberdb2'; -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; import log4js from 'log4js'; -const stats = require('../stats') +import stats from '../stats'; const logger = log4js.getLogger('ueberDB'); /** * The UeberDB Object that provides the database functions */ -exports.db = null; +export let db:Database|null = 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); - await exports.db.init(); - if (exports.db.metrics != null) { - for (const [metric, value] of Object.entries(exports.db.metrics)) { +export const init = async () => { + db = new Database(settings.dbType, settings.dbSettings, null, logger); + await db.init(); + if (db.metrics != null) { + for (const [metric, value] of Object.entries(db.metrics)) { if (typeof value !== 'number') continue; - stats.gauge(`ueberdb_${metric}`, () => exports.db.metrics[metric]); + stats.gauge(`ueberdb_${metric}`, () => db!.metrics[metric]); } } for (const fn of ['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove']) { - const f = exports.db[fn]; - exports[fn] = async (...args:string[]) => await f.call(exports.db, ...args); - Object.setPrototypeOf(exports[fn], Object.getPrototypeOf(f)); - Object.defineProperties(exports[fn], Object.getOwnPropertyDescriptors(f)); + // @ts-ignore + const f = db[fn]; + // @ts-ignore + dbInstance[fn] = async (...args:string[]) => await f.call(db, ...args); + // @ts-ignore + Object.setPrototypeOf(dbInstance[fn], Object.getPrototypeOf(f)); + // @ts-ignore + Object.defineProperties(dbInstance[fn], Object.getOwnPropertyDescriptors(f)); } }; -exports.shutdown = async (hookName: string, context:any) => { - if (exports.db != null) await exports.db.close(); - exports.db = null; +export const shutdown = async (hookName: string, context:any) => { + if (db != null) await db.close(); + db = null; logger.log('Database closed'); }; + +let dbInstance = {} as { + get: (key:string) => any; + set: (key:string, value:any) => void; + findKeys: (key:string) => string[]; + getSub: (key:string, subkey:string) => any; + setSub: (key:string, subkey:string, value:any) => void; + remove: (key:string) => void; + init: () => Promise; +} + +dbInstance.init = init + +export default dbInstance diff --git a/src/node/db/GroupManager.ts b/src/node/db/GroupManager.ts index b8cb6db02b4..9a74eb52956 100644 --- a/src/node/db/GroupManager.ts +++ b/src/node/db/GroupManager.ts @@ -21,7 +21,7 @@ const CustomError = require('../utils/customError'); import {randomString} from "../../static/js/pad_utils"; -const db = require('./DB'); +import db from './DB'; const padManager = require('./PadManager'); const sessionManager = require('./SessionManager'); @@ -69,6 +69,7 @@ exports.deleteGroup = async (groupID: string): Promise => { // UeberDB's setSub() method atomically reads the record, updates the appropriate property, and // writes the result. Setting a property to `undefined` deletes that property (JSON.stringify() // ignores such properties). + // @ts-ignore db.setSub('groups', [groupID], undefined), ...Object.keys(group.mappings || {}).map(async (m) => await db.remove(`mapper2group:${m}`)), ]); @@ -99,6 +100,7 @@ exports.createGroup = async () => { // Add the group to the `groups` record after the group's individual record is created so that // the state is consistent. Note: UeberDB's setSub() method atomically reads the record, updates // the appropriate property, and writes the result. + // @ts-ignore await db.setSub('groups', [groupID], 1); return {groupID}; }; @@ -121,6 +123,7 @@ exports.createGroupIfNotExistsFor = async (groupMapper: string|object) => { // deleted. Although the core Etherpad API does not support multiple mappings for the same // group, the database record does support multiple mappings in case a plugin decides to extend // the core Etherpad functionality. (It's also easy to implement it this way.) + // @ts-ignore db.setSub(`group:${result.groupID}`, ['mappings', groupMapper], 1), ]); return result; @@ -157,6 +160,7 @@ exports.createGroupPad = async (groupID: string, padName: string, text: string, await padManager.getPad(padID, text, authorId); // create an entry in the group for this pad + // @ts-ignore await db.setSub(`group:${groupID}`, ['pads', padID], 1); return {padID}; @@ -176,6 +180,7 @@ exports.listPads = async (groupID: string): Promise<{ padIDs: string[]; }> => { } // group exists, let's get the pads + // @ts-ignore const result = await db.getSub(`group:${groupID}`, ['pads']); const padIDs = Object.keys(result); diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts index c3e11aa15a6..082f6027919 100644 --- a/src/node/db/Pad.ts +++ b/src/node/db/Pad.ts @@ -13,8 +13,8 @@ import ChatMessage from '../../static/js/ChatMessage'; 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 db from './DB'; +import settings from '../utils/Settings'; const authorManager = require('./AuthorManager'); const padManager = require('./PadManager'); const padMessageHandler = require('../handler/PadMessageHandler'); @@ -56,6 +56,7 @@ class Pad { * own database table, or to validate imported pad data before it is written to the database. */ constructor(id:string, database = db) { + // @ts-ignore this.db = database; this.atext = makeAText('\n'); this.pool = new AttributePool(); @@ -428,6 +429,7 @@ class Pad { yield* Stream.range(0, this.chatHead + 1).map((i) => copyRecord(`:chat:${i}`)); // @ts-ignore yield this.copyAuthorInfoToDestinationPad(destinationID); + // @ts-ignore if (destGroupID) yield db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); }).call(this); for (const p of new Stream(promises).batch(100).buffer(99)) await p; @@ -511,6 +513,7 @@ class Pad { // Group pad? Add it to the group's list if (destGroupID) { + // @ts-ignore await db.setSub(`group:${destGroupID}`, ['pads', destinationID], 1); } diff --git a/src/node/db/PadManager.ts b/src/node/db/PadManager.ts index 54dbbf0892e..487f485f335 100644 --- a/src/node/db/PadManager.ts +++ b/src/node/db/PadManager.ts @@ -24,8 +24,8 @@ 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 db from './DB'; +import settings from '../utils/Settings'; /** * A cache of all loaded Pads. @@ -74,6 +74,7 @@ const padList = new class { async getPads() { if (!this._loaded) { this._loaded = (async () => { + // @ts-ignore const dbData = await db.findKeys('pad:*', '*:*:*'); if (dbData == null) return; for (const val of dbData) this.addPad(val.replace(/^pad:/, '')); diff --git a/src/node/db/ReadOnlyManager.ts b/src/node/db/ReadOnlyManager.ts index 23639d6656b..f16d92ad9b0 100644 --- a/src/node/db/ReadOnlyManager.ts +++ b/src/node/db/ReadOnlyManager.ts @@ -20,8 +20,8 @@ */ -const db = require('./DB'); -const randomString = require('../utils/randomstring'); +import db from './DB'; +import randomString from '../utils/randomstring'; /** diff --git a/src/node/db/SecurityManager.ts b/src/node/db/SecurityManager.ts index c2f209a01ae..542f2be5cbb 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'); const readOnlyManager = require('./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/db/SessionManager.ts b/src/node/db/SessionManager.ts index c0e43a6592c..de2438669fc 100644 --- a/src/node/db/SessionManager.ts +++ b/src/node/db/SessionManager.ts @@ -23,7 +23,7 @@ const CustomError = require('../utils/customError'); const promises = require('../utils/promises'); const randomString = require('../utils/randomstring'); -const db = require('./DB'); +import db from './DB'; const groupManager = require('./GroupManager'); const authorManager = require('./AuthorManager'); @@ -151,7 +151,9 @@ exports.createSession = async (groupID: string, authorID: string, validUntil: nu await Promise.all([ // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object // property, and writes the result. + // @ts-ignore db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1), + // @ts-ignore db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1), ]); @@ -196,7 +198,9 @@ exports.deleteSession = async (sessionID:string) => { // UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object // property, and writes the result. Setting a property to `undefined` deletes that property // (JSON.stringify() ignores such properties). + // @ts-ignore db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined), + // @ts-ignore db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined), ]); diff --git a/src/node/db/SessionStore.ts b/src/node/db/SessionStore.ts index 0b398efad6d..e2b28a7ce85 100644 --- a/src/node/db/SessionStore.ts +++ b/src/node/db/SessionStore.ts @@ -1,6 +1,6 @@ 'use strict'; -const DB = require('./DB'); +import DB from './DB'; const Store = require('@etherpad/express-session').Store; const log4js = require('log4js'); const util = require('util'); @@ -19,7 +19,7 @@ class SessionStore extends Store { * Etherpad is restarted. Use `null` to prevent `touch()` from ever updating the record. * Ignored if the cookie does not expire. */ - constructor(refresh = null) { + constructor(refresh:number|null = null) { super(); this._refresh = refresh; // Maps session ID to an object with the following properties: @@ -111,4 +111,4 @@ for (const m of ['get', 'set', 'destroy', 'touch']) { SessionStore.prototype[m] = util.callbackify(SessionStore.prototype[`_${m}`]); } -module.exports = SessionStore; +export default SessionStore diff --git a/src/node/eejs/index.ts b/src/node/eejs/index.ts index 5d57e475132..086c862114b 100644 --- a/src/node/eejs/index.ts +++ b/src/node/eejs/index.ts @@ -25,7 +25,7 @@ const fs = require('fs'); const hooks = require('../../static/js/pluginfw/hooks'); const path = require('path'); const resolve = require('resolve'); -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; import {pluginInstallPath} from '../../static/js/pluginfw/installer' const templateCache = new Map(); diff --git a/src/node/handler/APIKeyHandler.ts b/src/node/handler/APIKeyHandler.ts index b4e70f6e4b0..f63bcaf0c26 100644 --- a/src/node/handler/APIKeyHandler.ts +++ b/src/node/handler/APIKeyHandler.ts @@ -1,15 +1,15 @@ const absolutePaths = require('../utils/AbsolutePaths'); import fs from 'fs'; import log4js from 'log4js'; -const randomString = require('../utils/randomstring'); -const argv = require('../utils/Cli').argv; -const settings = require('../utils/Settings'); +import randomString from '../utils/randomstring'; +import {argvP} from '../utils/Cli' +import settings from '../utils/Settings'; const apiHandlerLogger = log4js.getLogger('APIHandler'); // ensure we have an apikey export let apikey:string|null = null; -const apikeyFilename = absolutePaths.makeAbsolute(argv.apikey || './APIKEY.txt'); +const apikeyFilename = absolutePaths.makeAbsolute(argvP.apikey || './APIKEY.txt'); if(settings.authenticationMethod === 'apikey') { diff --git a/src/node/handler/ImportHandler.ts b/src/node/handler/ImportHandler.ts index 286b4fb5699..e569c12fa15 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 6d2ee0d5785..a12c7211398 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -30,14 +30,14 @@ const AttributeManager = require('../../static/js/AttributeManager'); const authorManager = require('../db/AuthorManager'); import padutils from '../../static/js/pad_utils'; const readOnlyManager = require('../db/ReadOnlyManager'); -const settings = require('../utils/Settings'); +import settings from '../utils/Settings'; const securityManager = require('../db/SecurityManager'); const plugins = require('../../static/js/pluginfw/plugin_defs'); import log4js from 'log4js'; const messageLogger = log4js.getLogger('message'); const accessLogger = log4js.getLogger('access'); -const hooks = require('../../static/js/pluginfw/hooks'); -const stats = require('../stats') +const hooks = require('../../static/js/pluginfw/hooks.js'); +import stats from '../stats'; const assert = require('assert').strict; import {RateLimiterMemory} from 'rate-limiter-flexible'; import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest"; diff --git a/src/node/handler/SocketIORouter.ts b/src/node/handler/SocketIORouter.ts index 482276834df..abdf73d6916 100644 --- a/src/node/handler/SocketIORouter.ts +++ b/src/node/handler/SocketIORouter.ts @@ -22,9 +22,9 @@ import {MapArrayType} from "../types/MapType"; import {SocketModule} from "../types/SocketModule"; -const log4js = require('log4js'); -const settings = require('../utils/Settings'); -const stats = require('../../node/stats') +import log4js from 'log4js'; +import settings from '../utils/Settings'; +import stats from '../../node/stats'; const logger = log4js.getLogger('socket.io'); diff --git a/src/node/hooks/express.ts b/src/node/hooks/express.ts index d4e92dd35e3..b0c400fb372 100644 --- a/src/node/hooks/express.ts +++ b/src/node/hooks/express.ts @@ -13,9 +13,9 @@ import expressSession from '@etherpad/express-session'; 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'); -const stats = require('../stats') +import SessionStore from '../db/SessionStore'; +import settings from '../utils/Settings'; +import stats from '../stats'; import util from 'util'; const webaccess = require('./express/webaccess'); @@ -190,6 +190,7 @@ exports.restartServer = async () => { secretRotator = new SecretRotator( 'expressSessionSecrets', keyRotationInterval, sessionLifetime, settings.sessionKey); await secretRotator.start(); + // @ts-ignore secret = secretRotator.secrets; } if (!secret) throw new Error('missing cookie signing secret'); diff --git a/src/node/hooks/express/admin.ts b/src/node/hooks/express/admin.ts index e802750f25c..4847d47137b 100644 --- a/src/node/hooks/express/admin.ts +++ b/src/node/hooks/express/admin.ts @@ -5,7 +5,7 @@ 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 63d901f2126..19ec5dad7cc 100644 --- a/src/node/hooks/express/adminsettings.ts +++ b/src/node/hooks/express/adminsettings.ts @@ -9,7 +9,7 @@ const eejs = require('../../eejs'); 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 from '../../utils/Settings'; const UpdateCheck = require('../../utils/UpdateCheck'); const padManager = require('../../db/PadManager'); const api = require('../../db/API'); diff --git a/src/node/hooks/express/errorhandling.ts b/src/node/hooks/express/errorhandling.ts index 2de819b0edb..734d915b28c 100644 --- a/src/node/hooks/express/errorhandling.ts +++ b/src/node/hooks/express/errorhandling.ts @@ -3,7 +3,7 @@ import {ArgsExpressType} from "../../types/ArgsExpressType"; import {ErrorCaused} from "../../types/ErrorCaused"; -const stats = require('../../stats') +import stats from '../../stats'; exports.expressCreateServer = (hook_name:string, args: ArgsExpressType, cb:Function) => { exports.app = args.app; diff --git a/src/node/hooks/express/importexport.ts b/src/node/hooks/express/importexport.ts index 898606e4979..d9ad6003f65 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 from '../../utils/Settings'; const exportHandler = require('../../handler/ExportHandler'); const importHandler = require('../../handler/ImportHandler'); const padManager = require('../../db/PadManager'); diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index 8b04adf9363..6084ba0aa46 100644 --- a/src/node/hooks/express/openapi.ts +++ b/src/node/hooks/express/openapi.ts @@ -24,7 +24,7 @@ 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'); const logger = log4js.getLogger('API'); diff --git a/src/node/hooks/express/socketio.ts b/src/node/hooks/express/socketio.ts index bbdec1c1cf4..9184eff8831 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 9e4642ca8a8..31882d115c4 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 from '../../utils/Settings'; import util from 'node:util'; const webaccess = require('./webaccess'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); @@ -40,7 +40,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { app.get('/robots.txt', (req:any, res:any) => { let filePath = - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); + path.join(settings.root, 'src', 'static', 'skins', settings.skinName!, 'robots.txt'); res.sendFile(filePath, (err:any) => { // there is no custom robots.txt, send the default robots.txt which dissallows all if (err) { @@ -64,7 +64,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { const fns = [ ...(settings.favicon ? [path.resolve(settings.root, settings.favicon)] : []), - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), + path.join(settings.root, 'src', 'static', 'skins', settings.skinName!, 'favicon.ico'), path.join(settings.root, 'src', 'static', 'favicon.ico'), ]; for (const fn of fns) { @@ -174,6 +174,7 @@ const handleLiveReload = async (args: any, padString: string, timeSliderString: req, toolbar, isReadOnly, + settings: settings, entrypoint: '/watch/pad?hash=' + hash }) res.send(content); @@ -203,6 +204,7 @@ const handleLiveReload = async (args: any, padString: string, timeSliderString: req, toolbar, isReadOnly, + settings: settings, entrypoint: '/watch/timeslider?hash=' + hash }) res.send(content); diff --git a/src/node/hooks/express/static.ts b/src/node/hooks/express/static.ts index 07a7f3a21cf..372b5cc3be8 100644 --- a/src/node/hooks/express/static.ts +++ b/src/node/hooks/express/static.ts @@ -7,7 +7,7 @@ const fs = require('fs').promises; import {minify} from '../../utils/Minify'; import path from 'node:path'; 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/tests.ts b/src/node/hooks/express/tests.ts index f8a1417ef71..5be3450f10d 100644 --- a/src/node/hooks/express/tests.ts +++ b/src/node/hooks/express/tests.ts @@ -7,7 +7,7 @@ const path = require('path'); const fsp = require('fs').promises; const plugins = require('../../../static/js/pluginfw/plugin_defs'); const sanitizePathname = require('../../utils/sanitizePathname'); -const settings = require('../../utils/Settings'); +import settings from '../../utils/Settings'; // Returns all *.js files under specDir (recursively) as relative paths to specDir, using '/' // instead of path.sep to separate pathname components. diff --git a/src/node/hooks/express/webaccess.ts b/src/node/hooks/express/webaccess.ts index cb6884dc358..1b768db6c56 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'); const readOnlyManager = require('../../db/ReadOnlyManager'); diff --git a/src/node/hooks/i18n.ts b/src/node/hooks/i18n.ts index c4cc58bdd0b..320afdc6cf9 100644 --- a/src/node/hooks/i18n.ts +++ b/src/node/hooks/i18n.ts @@ -9,7 +9,7 @@ const path = require('path'); const _ = require('underscore'); const pluginDefs = require('../../static/js/pluginfw/plugin_defs'); const existsSync = require('../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:...} diff --git a/src/node/security/OAuth2Provider.ts b/src/node/security/OAuth2Provider.ts index e212113504b..2ec4b1a488f 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, KeyLike} 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, {Request, Response} from 'express'; import {format} from 'url' @@ -137,7 +137,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp } else if (token.kind === "ClientCredentials") { let extraParams: MapArrayType = {} - 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/security/SecretRotator.ts b/src/node/security/SecretRotator.ts index ee5bec7728a..777e32e83ae 100644 --- a/src/node/security/SecretRotator.ts +++ b/src/node/security/SecretRotator.ts @@ -5,7 +5,7 @@ import {LegacyParams} from "../types/LegacyParams"; const {Buffer} = require('buffer'); const crypto = require('./crypto'); -const db = require('../db/DB'); +import db from '../db/DB'; const log4js = require('log4js'); class Kdf { @@ -173,6 +173,7 @@ export class SecretRotator { // TODO: This is racy. If two instances start up at the same time and there are no existing // matching publications, each will generate and publish their own paramters. In practice this // is unlikely to happen, and if it does it can be fixed by restarting both Etherpad instances. + // @ts-ignore const dbKeys:string[] = await db.findKeys(`${this._dbPrefix}:*`, null) || []; let currentParams:any = null; let currentId = null; diff --git a/src/node/server.ts b/src/node/server.ts index f96db3ab194..211388ba8ac 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) { @@ -69,13 +69,13 @@ NodeVersion.enforceMinNodeVersion(pkg.engines.node.replace(">=", "")); NodeVersion.checkDeprecationStatus(pkg.engines.node.replace(">=", ""), '2.1.0'); const UpdateCheck = require('./utils/UpdateCheck'); -const db = require('./db/DB'); +import db from './db/DB'; const express = require('./hooks/express'); const hooks = require('../static/js/pluginfw/hooks'); const pluginDefs = require('../static/js/pluginfw/plugin_defs'); const plugins = require('../static/js/pluginfw/plugins'); const {Gate} = require('./utils/promises'); -const stats = require('./stats') +import stats from './stats'; const logger = log4js.getLogger('server'); diff --git a/src/node/stats.ts b/src/node/stats.ts index f1fc0cccfdd..79eadf75f58 100644 --- a/src/node/stats.ts +++ b/src/node/stats.ts @@ -1,10 +1,13 @@ 'use strict'; -const measured = require('measured-core'); +// @ts-ignore +import measured from 'measured-core'; + +const coll = measured.createCollection() -module.exports = measured.createCollection(); +export default coll; // @ts-ignore -module.exports.shutdown = async (hookName, context) => { - module.exports.end(); -}; \ No newline at end of file +export const shutdown = async (hookName, context) => { + coll.end(); +}; diff --git a/src/node/utils/Abiword.ts b/src/node/utils/Abiword.ts index c0937fcd9fc..fd17497ed12 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 c257440a1f7..b82f25dcf59 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'); @@ -43,8 +43,8 @@ let etherpadRoot: string|null = null; const popIfEndsWith = (stringArray: string[], lastDesiredElements: string[]): string[] | false => { if (stringArray.length <= lastDesiredElements.length) { absPathLogger.debug(`In order to pop "${lastDesiredElements.join(path.sep)}" ` + - `from "${stringArray.join(path.sep)}", it should contain at least ` + - `${lastDesiredElements.length + 1} elements`); + `from "${stringArray.join(path.sep)}", it should contain at least ` + + `${lastDesiredElements.length + 1} elements`); return false; } @@ -55,7 +55,7 @@ const popIfEndsWith = (stringArray: string[], lastDesiredElements: string[]): st } absPathLogger.debug( - `${stringArray.join(path.sep)} does not end with "${lastDesiredElements.join(path.sep)}"`); + `${stringArray.join(path.sep)} does not end with "${lastDesiredElements.join(path.sep)}"`); return false; }; @@ -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; } @@ -104,7 +104,7 @@ exports.findEtherpadRoot = () => { if (maybeEtherpadRoot === false) { absPathLogger.error('Could not identity Etherpad base path in this ' + - `${process.platform} installation in "${foundRoot}"`); + `${process.platform} installation in "${foundRoot}"`); process.exit(1); } @@ -116,7 +116,7 @@ exports.findEtherpadRoot = () => { } absPathLogger.error( - `To run, Etherpad has to identify an absolute base path. This is not: "${etherpadRoot}"`); + `To run, Etherpad has to identify an absolute base path. This is not: "${etherpadRoot}"`); process.exit(1); }; @@ -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 1579dd3ce91..4ef436ce2af 100644 --- a/src/node/utils/Cli.ts +++ b/src/node/utils/Cli.ts @@ -21,7 +21,9 @@ */ // An object containing the parsed command-line options -exports.argv = {}; +import {MapArrayType} from "../types/MapType"; + +export const argvP: MapArrayType = {}; const argv = process.argv.slice(2); let arg, prevArg; @@ -32,22 +34,22 @@ for (let i = 0; i < argv.length; i++) { // Override location of settings.json file if (prevArg === '--settings' || prevArg === '-s') { - exports.argv.settings = arg; + argvP.settings = arg; } // Override location of credentials.json file if (prevArg === '--credentials') { - exports.argv.credentials = arg; + argvP.credentials = arg; } // Override location of settings.json file if (prevArg === '--sessionkey') { - exports.argv.sessionkey = arg; + argvP.sessionkey = arg; } // Override location of APIKEY.txt file if (prevArg === '--apikey') { - exports.argv.apikey = arg; + argvP.apikey = arg; } prevArg = arg; diff --git a/src/node/utils/ImportEtherpad.ts b/src/node/utils/ImportEtherpad.ts index cf34107c73e..492a1e4c614 100644 --- a/src/node/utils/ImportEtherpad.ts +++ b/src/node/utils/ImportEtherpad.ts @@ -22,7 +22,7 @@ import AttributePool from '../../static/js/AttributePool'; const {Pad} = require('../db/Pad'); const Stream = require('./Stream'); const authorManager = require('../db/AuthorManager'); -const db = require('../db/DB'); +import db from '../db/DB'; const hooks = require('../../static/js/pluginfw/hooks'); import log4js from 'log4js'; const supportedElems = require('../../static/js/contentcollector').supportedElems; diff --git a/src/node/utils/LibreOffice.ts b/src/node/utils/LibreOffice.ts index e89ebe460c7..e73fd144c28 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 9660c909866..1dba1f2585f 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 d798e10bffc..2fee4fccdb4 100644 --- a/src/node/utils/Settings.ts +++ b/src/node/utils/Settings.ts @@ -28,124 +28,111 @@ */ import {MapArrayType} from "../types/MapType"; -import {SettingsNode, SettingsTree} from "./SettingsTree"; -import {coerce} from "semver"; - -const absolutePaths = require('./AbsolutePaths'); -const deepEqual = require('fast-deep-equal/es6'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const argv = require('./Cli').argv; -const jsonminify = require('jsonminify'); -const log4js = require('log4js'); -const randomString = require('./randomstring'); -const suppressDisableMsg = ' -- To suppress these warning messages change ' + - 'suppressErrorsInPadText to true in your settings.json\n'; -const _ = require('underscore'); +import {SettingsNode} from "./SettingsTree"; +import {version} from '../../package.json' +import {findEtherpadRoot, isSubdir, makeAbsolute} from './AbsolutePaths'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import {argvP} from "./Cli"; +import jsonminify from 'jsonminify'; +import log4js from 'log4js'; +import randomString from './randomstring'; -const logger = log4js.getLogger('settings'); +import _ from 'underscore'; -// Exported values that settings.json and credentials.json cannot override. -const nonSettings = [ - 'credentialsFilename', - 'settingsFilename', -]; -// This is a function to make it easy to create a new instance. It is important to not reuse a -// config object after passing it to log4js.configure() because that method mutates the object. :( -const defaultLogConfig = (level: string, layoutType: string) => ({ - appenders: {console: {type: 'console', layout: {type: layoutType}}}, - categories: { - default: {appenders: ['console'], level}, - } -}); -const defaultLogLevel = 'INFO'; -const defaultLogLayoutType = 'colored'; -const initLogging = (config: any) => { - // log4js.configure() modifies exports.logconfig so check for equality first. - log4js.configure(config); - log4js.getLogger('console'); - - // Overwrites for console output methods - console.debug = logger.debug.bind(logger); - console.log = logger.info.bind(logger); - console.warn = logger.warn.bind(logger); - console.error = logger.error.bind(logger); -}; +class Settings { -// Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized + constructor() { + // Initialize logging as early as possible with reasonable defaults. Logging will be re-initialized // 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'); - -/** - * The app title, visible e.g. in the browser window - */ -exports.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. + this.initLogging(this.defaultLogConfig(this.defaultLogLevel)); + this.logger.info('All relative paths will be interpreted relative to the identified ' + + `Etherpad base dir: ${this.root}`); + // initially load settings + this.reloadSettings(); + } + /** + * The app title, visible e.g. in the browser window + */ + title = 'Etherpad'; + settingsFilename = makeAbsolute(argvP.settings || 'settings.json'); + credentialsFilename = makeAbsolute(argvP.credentials || 'credentials.json'); + + suppressDisableMsg = ' -- To suppress these warning messages change ' + + 'suppressErrorsInPadText to true in your settings.json\n'; + defaultLogLevel = 'INFO'; + private logger = log4js.getLogger('settings'); + /* Root path of the installation */ + root = findEtherpadRoot(); + /** + * 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: string|null = null; +// Exported values that settings.json and credentials.json cannot override. + nonSettings = [ + 'credentialsFilename', + 'settingsFilename', + ] + /* + * 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.favicon = null; - -exports.ttl = { + skinName: string | null = null; + skinVariants = 'super-light-toolbar super-light-editor light-background'; + 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 -} - - + } + /** + * Should we suppress Error messages from being in Pad Contents + */ + suppressErrorsInPadText = false; + + /** + * The Port ep-lite should listen to + */ + port = process.env.PORT as unknown as number || 9001; + + /** + * The IP ep-lite should listen to + */ + ip: string = '0.0.0.0'; -/* - * 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 = { +// This is a function to make it easy to create a new instance. It is important to not reuse a +// config object after passing it to log4js.configure() because that method mutates the object. :( + private defaultLogConfig = (level: string) => ({ + appenders: {console: {type: 'console'}}, + categories: { + default: {appenders: ['console'], level}, + } + }) + + /** + * 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:{ + key:string, + cert:string + ca?: string[] + }|false = false; + + /** + * socket.io transport methods + **/ + socketTransportProtocols: any[] = ['websocket', 'polling']; + socketIo = { /** * Maximum permitted client message size (in bytes). * @@ -155,42 +142,40 @@ 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 = 'dirty'; -/** - * This setting is passed with dbType to ueberDB to set up the database - */ -exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')}; + dbType = 'dirty'; -/** - * The default Text of a new pad - */ -exports.defaultPadText = [ + /** + * This setting is passed with dbType to ueberDB to set up the database + */ + dbSettings = {filename: path.join(this.root, 'var/dirty.db')}; + /** + * 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'); + ].join('\n'); -/** - * The default Pad Settings for a user (Can be overridden by changing the setting - */ -exports.padOptions = { + + /** + * The default Pad Settings for a user (Can be overridden by changing the setting + */ + padOptions = { noColors: false, showControls: true, showChat: true, @@ -202,12 +187,12 @@ exports.padOptions = { alwaysShowChat: false, chatAndUsers: false, lang: null, -}; + }; -/** - * Whether certain shortcut keys are enabled for a user in the pad - */ -exports.padShortcutEnabled = { + /** + * Whether certain shortcut keys are enabled for a user in the pad + */ + padShortcutEnabled = { altF9: true, altC: true, delete: true, @@ -230,117 +215,114 @@ exports.padShortcutEnabled = { ctrlHome: true, pageUp: true, pageDown: true, -}; + }; -/** - * The toolbar buttons and order. - */ -exports.toolbar = { + /** + * The toolbar buttons and order. + */ + public 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'], - ['showusers'], + ['importexport', 'timeslider', 'savedrevision'], + ['settings', 'embed'], + ['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; - -/* - * log4js appender configuration - */ -exports.logconfig = null; - -/* - * Deprecated cookie signing key. - */ -exports.sessionKey = null; - -/* - * Trust Proxy, whether or not trust the x-forwarded-for header. - */ -exports.trustProxy = false; - -/* - * Settings controlling the session cookie issued by Etherpad. - */ -exports.cookie = { + }; + + /** + * 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: any = this.defaultLogLevel; + + + + /** + * 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 + */ + private logconfig: { categories: { default: { level: string, appenders: string[] } }, appenders: { console: { type: string } } } | null = null; + + /* + * Deprecated cookie signing key. + */ + sessionKey: string|null = null; + + /* + * Trust Proxy, whether or not trust the x-forwarded-for header. + */ + trustProxy = false; + + /* + * Settings controlling the session cookie issued by Etherpad. + */ + cookie = { keyRotationInterval: 1 * 24 * 60 * 60 * 1000, /* * Value of the SameSite cookie property. "Lax" is recommended unless @@ -356,40 +338,44 @@ exports.cookie = { 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 = {}; - -/* - * This setting is used for configuring sso - */ -exports.sso = { + }; + + /* + * 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 + */ + requireAuthentication = false; + requireAuthorization = false; + users = {}; + + /* + * This setting is used for configuring sso + */ + sso:{ + issuer: string, + clients?: any[], + + } = { issuer: "http://localhost:9001" -} - -/* - * Show settings in admin page, by default it is true - */ -exports.showSettingsInAdminPage = true; - -/* - * By default, when caret is moved out of viewport, it scrolls the minimum - * height needed to make this line visible. - */ -exports.scrollWhenFocusLineIsOutOfViewport = { + } + + /* + * Show settings in admin page, by default it is true + */ + public showSettingsInAdminPage = true; + + /* + * By default, when caret is moved out of viewport, it scrolls the minimum + * height needed to make this line visible. + */ + scrollWhenFocusLineIsOutOfViewport = { /* * Percentage of viewport height to be additionally scrolled. */ percentage: { - editionAboveViewport: 0, - editionBelowViewport: 0, + editionAboveViewport: 0, + editionBelowViewport: 0, }, /* @@ -409,168 +395,185 @@ exports.scrollWhenFocusLineIsOutOfViewport = { * 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; - -/* - * Override any strings found in locale directories - */ -exports.customLocaleStrings = {}; - -/* - * From Etherpad 1.8.3 onwards, import and export of pads is always rate - * limited. - * - * The default is to allow at most 10 requests per IP in a 90 seconds window. - * After that the import/export request is rejected. - * - * See https://github.com/nfriedly/express-rate-limit for more options - */ -exports.importExportRateLimiting = { + }; + + /* + * Expose Etherpad version in the web interface and in the Server http header. + * + * Do not enable on production machines. + */ + exposeVersion = false; + + /* + * Override any strings found in locale directories + */ + customLocaleStrings = {}; + + /* + * From Etherpad 1.8.3 onwards, import and export of pads is always rate + * limited. + * + * The default is to allow at most 10 requests per IP in a 90 seconds window. + * After that the import/export request is rejected. + * + * See https://github.com/nfriedly/express-rate-limit for more options + */ + importExportRateLimiting: { max: number, windowMs?: number } = { // 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. - * After that the change is rejected. - * - * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options - */ -exports.commitRateLimiting = { + }; + + /* + * 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. + * After that the change is rejected. + * + * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options + */ + commitRateLimiting = { // duration of the rate limit window (seconds) duration: 1, // maximum number of chanes 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; - -/* - * Disable Admin UI tests - */ -exports.enableAdminUITests = false; - -/* - * Enable auto conversion of pad Ids to lowercase. - * e.g. /p/EtHeRpAd to /p/etherpad - */ -exports.lowerCasePadIds = false; + }; + + /* + * 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. + */ + importMaxFileSize = 50 * 1024 * 1024; + + /* + * Disable Admin UI tests + */ + enableAdminUITests = false; + + /* + * Enable auto conversion of pad Ids to lowercase. + * e.g. /p/EtHeRpAd to /p/etherpad + */ + lowerCasePadIds = false; + + randomVersionString: string|null = null; + + private initLogging = (config: any) => { + // log4js.configure() modifies logconfig so check for equality first. + log4js.configure(config); + log4js.getLogger('console'); -// checks if abiword is avaiable -exports.abiwordAvailable = () => { - if (exports.abiword != null) { - return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; + // Overwrites for console output methods + console.debug = this.logger.debug.bind(this.logger); + console.log = this.logger.info.bind(this.logger); + console.warn = this.logger.warn.bind(this.logger); + console.error = this.logger.error.bind(this.logger); + } + + // checks if abiword is avaiable + abiwordAvailable = () => { + if (this.abiword != null) { + return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { - return 'no'; + return 'no'; } -}; + }; -exports.sofficeAvailable = () => { - if (exports.soffice != null) { - return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; + sofficeAvailable = () => { + if (this.soffice != null) { + return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; } else { - return 'no'; + return 'no'; } -}; + }; -exports.exportAvailable = () => { - const abiword = exports.abiwordAvailable(); - const soffice = exports.sofficeAvailable(); + exportAvailable = () => { + const abiword = this.abiwordAvailable(); + const soffice = this.sofficeAvailable(); if (abiword === 'no' && soffice === 'no') { - return 'no'; + return 'no'; } else if ((abiword === 'withoutPDF' && soffice === 'no') || - (abiword === 'no' && soffice === 'withoutPDF')) { - return 'withoutPDF'; + (abiword === 'no' && soffice === 'withoutPDF')) { + return 'withoutPDF'; } else { - return 'yes'; + return 'yes'; } -}; + }; // Provide git version if available -exports.getGitCommit = () => { + 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); + let rootPath = this.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}`); + this.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; - - - -/** - * Receives a settingsObj and, if the property name is a valid configuration - * item, stores it in the module's exported properties via a side effect. - * - * This code refactors a previous version that copied & pasted the same code for - * both "settings.json" and "credentials.json". - */ -const storeSettings = (settingsObj: any) => { + } + + // Return etherpad version from package.json + getEpVersion = () => version; + + /** + * Receives a settingsObj and, if the property name is a valid configuration + * item, stores it in the module's exported properties via a side effect. + * + * This code refactors a previous version that copied & pasted the same code for + * both "settings.json" and "credentials.json". + */ + private storeSettings = (settingsObj: any) => { for (const i of Object.keys(settingsObj || {})) { - if (nonSettings.includes(i)) { - logger.warn(`Ignoring setting: '${i}'`); - continue; - } - - // test if the setting starts with a lowercase character - if (i.charAt(0).search('[a-z]') !== 0) { - logger.warn(`Settings should start with a lowercase character: '${i}'`); - } - - // 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) { - if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) { - exports[i] = _.defaults(settingsObj[i], exports[i]); - } else { - exports[i] = settingsObj[i]; - } + if (this.nonSettings.includes(i)) { + this.logger.warn(`Ignoring setting: '${i}'`); + continue; + } + + // test if the setting starts with a lowercase character + if (i.charAt(0).search('[a-z]') !== 0) { + this.logger.warn(`Settings should start with a lowercase character: '${i}'`); + } + + // we know this setting, so we overwrite it + // or it's a settings hash, specific to a plugin + // @ts-ignore + if (this[i] !== undefined || i.indexOf('ep_') === 0) { + if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) { + // @ts-ignore + this[i] = _.defaults(settingsObj[i], exports[i]); } else { - // this setting is unknown, output a warning and throw it away - logger.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); + // @ts-ignore + this[i] = settingsObj[i]; } + } else { + // this setting is unknown, output a warning and throw it away + this.logger.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); + } } -}; + } -/* + + + /* * If stringValue is a numeric string, or its value is "true" or "false", coerce * them to appropriate JS types. Otherwise return stringValue as-is. * @@ -582,159 +585,159 @@ const storeSettings = (settingsObj: any) => { * short syntax "${ABIWORD}", and not "${ABIWORD:null}": the latter would result * in the literal string "null", instead. */ -const coerceValue = (stringValue: string) => { + private coerceValue = (stringValue: string) => { // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number // @ts-ignore const isNumeric = !isNaN(stringValue) && !isNaN(parseFloat(stringValue) && isFinite(stringValue)); if (isNumeric) { - // detected numeric string. Coerce to a number + // detected numeric string. Coerce to a number - return +stringValue; + return +stringValue; } switch (stringValue) { - case 'true': - return true; - case 'false': - return false; - case 'undefined': - return undefined; - case 'null': - return null; - default: - return stringValue; + case 'true': + return true; + case 'false': + return false; + case 'undefined': + return undefined; + case 'null': + return null; + default: + return stringValue; } -}; - -/** - * Takes a javascript object containing Etherpad's configuration, and returns - * another object, in which all the string properties whose value is of the form - * "${ENV_VAR}" or "${ENV_VAR:default_value}" got their value replaced with the - * contents of the given environment variable, or with a default value. - * - * By definition, an environment variable's value is always a string. However, - * the code base makes use of the various json types. To maintain compatiblity, - * some heuristics is applied: - * - * - if ENV_VAR does not exist in the environment, null is returned; - * - if ENV_VAR's value is "true" or "false", it is converted to the js boolean - * values true or false; - * - if ENV_VAR's value looks like a number, it is converted to a js number - * (details in the code). - * - * The following is a scheme of the behaviour of this function: - * - * +---------------------------+---------------+------------------+ - * | Configuration string in | Value of | Resulting confi- | - * | settings.json | ENV_VAR | guration value | - * |---------------------------|---------------|------------------| - * | "${ENV_VAR}" | "some_string" | "some_string" | - * | "${ENV_VAR}" | "9001" | 9001 | - * | "${ENV_VAR}" | undefined | null | - * | "${ENV_VAR:some_default}" | "some_string" | "some_string" | - * | "${ENV_VAR:some_default}" | undefined | "some_default" | - * +---------------------------+---------------+------------------+ - * - * IMPLEMENTATION NOTE: variable substitution is performed doing a round trip - * conversion to/from json, using a custom replacer parameter in - * JSON.stringify(), and parsing the JSON back again. This ensures that - * environment variable replacement is performed even on nested objects. - * - * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter - */ -const lookupEnvironmentVariables = (obj: MapArrayType) => { + }; + + /** + * Takes a javascript object containing Etherpad's configuration, and returns + * another object, in which all the string properties whose value is of the form + * "${ENV_VAR}" or "${ENV_VAR:default_value}" got their value replaced with the + * contents of the given environment variable, or with a default value. + * + * By definition, an environment variable's value is always a string. However, + * the code base makes use of the various json types. To maintain compatiblity, + * some heuristics is applied: + * + * - if ENV_VAR does not exist in the environment, null is returned; + * - if ENV_VAR's value is "true" or "false", it is converted to the js boolean + * values true or false; + * - if ENV_VAR's value looks like a number, it is converted to a js number + * (details in the code). + * + * The following is a scheme of the behaviour of this function: + * + * +---------------------------+---------------+------------------+ + * | Configuration string in | Value of | Resulting confi- | + * | settings.json | ENV_VAR | guration value | + * |---------------------------|---------------|------------------| + * | "${ENV_VAR}" | "some_string" | "some_string" | + * | "${ENV_VAR}" | "9001" | 9001 | + * | "${ENV_VAR}" | undefined | null | + * | "${ENV_VAR:some_default}" | "some_string" | "some_string" | + * | "${ENV_VAR:some_default}" | undefined | "some_default" | + * +---------------------------+---------------+------------------+ + * + * IMPLEMENTATION NOTE: variable substitution is performed doing a round trip + * conversion to/from json, using a custom replacer parameter in + * JSON.stringify(), and parsing the JSON back again. This ensures that + * environment variable replacement is performed even on nested objects. + * + * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter + */ + private lookupEnvironmentVariables = (obj: MapArrayType) => { const replaceEnvs = (obj: MapArrayType) => { - for (let [key, value] of Object.entries(obj)) { - /* - * the first invocation of replacer() is with an empty key. Just go on, or - * we would zap the entire object. - */ - if (key === '') { - obj[key] = value; - continue - } - - /* - * If we received from the configuration file a number, a boolean or - * something that is not a string, we can be sure that it was a literal - * value. No need to perform any variable substitution. - * - * The environment variable expansion syntax "${ENV_VAR}" is just a string - * of specific form, after all. - */ - - if(key === 'undefined' || value === undefined) { - delete obj[key] - continue - } - - if ((typeof value !== 'string' && typeof value !== 'object') || value === null) { - obj[key] = value; - continue - } - - if (typeof obj[key] === "object") { - replaceEnvs(obj[key]); - continue - } - - - /* - * Let's check if the string value looks like a variable expansion (e.g.: - * "${ENV_VAR}" or "${ENV_VAR:default_value}") - */ - // MUXATOR 2019-03-21: we could use named capture groups here once we migrate to nodejs v10 - const match = value.match(/^\$\{([^:]*)(:((.|\n)*))?\}$/); - - if (match == null) { - // no match: use the value literally, without any substitution - obj[key] = value; - continue - } - - /* - * We found the name of an environment variable. Let's read its actual value - * and its default value, if given - */ - const envVarName = match[1]; - const envVarValue = process.env[envVarName]; - const defaultValue = match[3]; - - if ((envVarValue === undefined) && (defaultValue === undefined)) { - logger.warn(`Environment variable "${envVarName}" does not contain any value for ` + - `configuration key "${key}", and no default was given. Using null. ` + - 'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' + - 'explicitly use "null" as the default if you want to continue to use null.'); - - /* - * We have to return null, because if we just returned undefined, the - * configuration item "key" would be stripped from the returned object. - */ - obj[key] = null; - continue - } - - if ((envVarValue === undefined) && (defaultValue !== undefined)) { - logger.debug(`Environment variable "${envVarName}" not found for ` + - `configuration key "${key}". Falling back to default value.`); - - obj[key] = coerceValue(defaultValue); - continue - } - - // envVarName contained some value. - - /* - * For numeric and boolean strings let's convert it to proper types before - * returning it, in order to maintain backward compatibility. - */ - logger.debug( - `Configuration key "${key}" will be read from environment variable "${envVarName}"`); - - obj[key] = coerceValue(envVarValue!); + for (let [key, value] of Object.entries(obj)) { + /* + * the first invocation of replacer() is with an empty key. Just go on, or + * we would zap the entire object. + */ + if (key === '') { + obj[key] = value; + continue + } + + /* + * If we received from the configuration file a number, a boolean or + * something that is not a string, we can be sure that it was a literal + * value. No need to perform any variable substitution. + * + * The environment variable expansion syntax "${ENV_VAR}" is just a string + * of specific form, after all. + */ + + if (key === 'undefined' || value === undefined) { + delete obj[key] + continue + } + + if ((typeof value !== 'string' && typeof value !== 'object') || value === null) { + obj[key] = value; + continue + } + + if (typeof obj[key] === "object") { + replaceEnvs(obj[key]); + continue + } + + + /* + * Let's check if the string value looks like a variable expansion (e.g.: + * "${ENV_VAR}" or "${ENV_VAR:default_value}") + */ + // MUXATOR 2019-03-21: we could use named capture groups here once we migrate to nodejs v10 + const match = value.match(/^\$\{([^:]*)(:((.|\n)*))?\}$/); + + if (match == null) { + // no match: use the value literally, without any substitution + obj[key] = value; + continue + } + + /* + * We found the name of an environment variable. Let's read its actual value + * and its default value, if given + */ + const envVarName = match[1]; + const envVarValue = process.env[envVarName]; + const defaultValue = match[3]; + + if ((envVarValue === undefined) && (defaultValue === undefined)) { + this.logger.warn(`Environment variable "${envVarName}" does not contain any value for ` + + `configuration key "${key}", and no default was given. Using null. ` + + 'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' + + 'explicitly use "null" as the default if you want to continue to use null.'); + + /* + * We have to return null, because if we just returned undefined, the + * configuration item "key" would be stripped from the returned object. + */ + obj[key] = null; + continue + } + + if ((envVarValue === undefined) && (defaultValue !== undefined)) { + this.logger.debug(`Environment variable "${envVarName}" not found for ` + + `configuration key "${key}". Falling back to default value.`); + + obj[key] = this.coerceValue(defaultValue); + continue } - return obj + + // envVarName contained some value. + + /* + * For numeric and boolean strings let's convert it to proper types before + * returning it, in order to maintain backward compatibility. + */ + this.logger.debug( + `Configuration key "${key}" will be read from environment variable "${envVarName}"`); + + obj[key] = this.coerceValue(envVarValue!); + } + return obj } replaceEnvs(obj); @@ -749,202 +752,195 @@ const lookupEnvironmentVariables = (obj: MapArrayType) => { const root = new SettingsNode("EP") for (let [env, envVal] of Object.entries(process.env)) { - if (!env.startsWith("EP")) continue - treeEntries.set(env, envVal) + if (!env.startsWith("EP")) continue + treeEntries.set(env, envVal) } treeEntries.forEach((value, key) => { - let pathToKey = key.split("__") - let currentNode = root - let depth = 0 - depth++ - currentNode.addChild(pathToKey, value!) + let pathToKey = key.split("__") + let currentNode = root + let depth = 0 + depth++ + currentNode.addChild(pathToKey, value!) }) //console.log(root.collectFromLeafsUpwards()) const rooting = root.collectFromLeafsUpwards() obj = Object.assign(obj, rooting) return obj; -}; + } -/** - * - 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) => { + + /** + * - 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. + */ + parseSettings = (settingsFilename: string, isSettings: boolean) => { let settingsStr = ''; let settingsType, notFoundMessage, notFoundFunction; if (isSettings) { - settingsType = 'settings'; - notFoundMessage = 'Continuing using defaults!'; - notFoundFunction = logger.warn.bind(logger); + settingsType = 'settings'; + notFoundMessage = 'Continuing using defaults!'; + notFoundFunction = this.logger.warn.bind(this.logger); } else { - settingsType = 'credentials'; - notFoundMessage = 'Ignoring.'; - notFoundFunction = logger.info.bind(logger); + settingsType = 'credentials'; + notFoundMessage = 'Ignoring.'; + notFoundFunction = this.logger.info.bind(this.logger); } try { - // read the settings file - settingsStr = fs.readFileSync(settingsFilename).toString(); + // read the settings file + settingsStr = fs.readFileSync(settingsFilename).toString(); } catch (e) { - notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); + notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); - // or maybe undefined! - return null; + // or maybe undefined! + return null; } try { - settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); + settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); - const settings = JSON.parse(settingsStr); + const settings = JSON.parse(settingsStr); - logger.info(`${settingsType} loaded from: ${settingsFilename}`); + this.logger.info(`${settingsType} loaded from: ${settingsFilename}`); - return lookupEnvironmentVariables(settings); + return this.lookupEnvironmentVariables(settings); } catch (e: any) { - logger.error(`There was an error processing your ${settingsType} ` + - `file from ${settingsFilename}: ${e.message}`); + this.logger.error(`There was an error processing your ${settingsType} ` + + `file from ${settingsFilename}: ${e.message}`); - process.exit(1); + process.exit(1); } -}; + } -exports.reloadSettings = () => { - const settings = parseSettings(exports.settingsFilename, true); - const credentials = parseSettings(exports.credentialsFilename, false); - storeSettings(settings); - storeSettings(credentials); + reloadSettings = () => { + const settings = this.parseSettings(this.settingsFilename, true); + const credentials = this.parseSettings(this.credentialsFilename, false); + this.storeSettings(settings); + this.storeSettings(credentials); // Init logging config - exports.logconfig = defaultLogConfig( - exports.loglevel ? exports.loglevel : defaultLogLevel, - exports.logLayoutType ? exports.logLayoutType : defaultLogLayoutType - ); - logger.warn("loglevel: " + exports.loglevel); - logger.warn("logLayoutType: " + exports.logLayoutType); - initLogging(exports.logconfig); - - if (!exports.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'; + this.logconfig = this.defaultLogConfig(this.loglevel ? this.loglevel : this.defaultLogLevel); + this.initLogging(this.logconfig); + + if (!this.skinName) { + this.logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + + 'update your settings.json. Falling back to the default "colibris".'); + this.skinName = 'colibris'; } - if (!exports.socketTransportProtocols.includes("websocket") || !exports.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']; + if (!this.socketTransportProtocols.includes("websocket") || this.socketTransportProtocols.includes("polling")) { + this.logger.warn("Invalid socketTransportProtocols setting. Please check out settings.json.template and update your settings.json. Falling back to the default ['websocket', 'polling']."); + this.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 (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".`); - - exports.skinName = 'colibris'; - } - - // informative variable, just for the log messages - let skinPath = path.join(skinBasePath, exports.skinName); - - // what if someone sets skinName == ".." or "."? We catch him! - if (absolutePaths.isSubdir(skinBasePath, skinPath) === false) { - 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); - } - - if (fs.existsSync(skinPath) === false) { - logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); - exports.skinName = 'colibris'; - skinPath = path.join(skinBasePath, exports.skinName); - } - - logger.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); + if (this.skinName) { + const skinBasePath = path.join(this.root, 'src', 'static', 'skins'); + const countPieces = this.skinName.split(path.sep).length; + + if (countPieces !== 1) { + this.logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + + `not valid: "${this.skinName}". Falling back to the default "colibris".`); + this.skinName = 'colibris'; + } + + // informative variable, just for the log messages + let skinPath = path.join(skinBasePath, this.skinName); + + // what if someone sets skinName == ".." or "."? We catch him! + if (isSubdir(skinBasePath, skinPath) === false) { + this.logger.error(`Skin path ${skinPath} must be a subdirectory of ${skinBasePath}. ` + + 'Falling back to the default "colibris".'); + + this.skinName = 'colibris'; + skinPath = path.join(skinBasePath, this.skinName); + } + + if (fs.existsSync(skinPath) === false) { + this.logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); + this.skinName = 'colibris'; + skinPath = path.join(skinBasePath,this.skinName); + } + + this.logger.info(`Using skin "${this.skinName}" in dir: ${skinPath}`); } - if (exports.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; - } - }); + if (this.abiword) { + // Check abiword actually exists + if (this.abiword != null) { + let exists = fs.existsSync(this.abiword) + if (!exists) { + const abiwordError = 'Abiword does not exist at this path, check your settings file.'; + if (!this.suppressErrorsInPadText) { + this.defaultPadText += `\nError: ${abiwordError}${this.suppressDisableMsg}`; + } + this.logger.error(`${abiwordError} File location: ${this.abiword}`); + this.abiword = null; } + } } - if (exports.soffice) { - fs.exists(exports.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}`; - } - logger.error(`${sofficeError} File location: ${exports.soffice}`); - exports.soffice = null; - } - }); - } + if (this.soffice) { + let exists = fs.existsSync(this.soffice) + if (!exists) { + const sofficeError = + 'soffice (libreoffice) does not exist at this path, check your settings file.'; - const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); - if (!exports.sessionKey) { - try { - exports.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) { - logger.info( - `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); - exports.sessionKey = randomString(32); - fs.writeFileSync(sessionkeyFilename, exports.sessionKey, 'utf8'); + if (!this.suppressErrorsInPadText) { + this.defaultPadText += `\nError: ${sofficeError}${this.suppressDisableMsg}`; } + this.logger.error(`${sofficeError} File location: ${this.soffice}`); + this.soffice = null; + } + } + + const sessionkeyFilename = makeAbsolute(argvP.sessionkey || './SESSIONKEY.txt'); + if (!this.sessionKey) { + try { + this.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); + this.logger.info(`Session key loaded from: ${sessionkeyFilename}`); + } catch (err) { /* ignored */ + } + const keyRotationEnabled = this.cookie.keyRotationInterval && this.cookie.sessionLifetime; + if (!this.sessionKey && !keyRotationEnabled) { + this.logger.info( + `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); + this.sessionKey = randomString(32); + fs.writeFileSync(sessionkeyFilename, this.sessionKey, 'utf8'); + } } else { - logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' + - 'This value is auto-generated now. Please remove the setting from the file. -- ' + - 'If you are seeing this error after restarting using the Admin User ' + - 'Interface then you can ignore this message.'); + this.logger.warn('Declaring the sessionKey in the settings.json is deprecated. ' + + 'This value is auto-generated now. Please remove the setting from the file. -- ' + + 'If you are seeing this error after restarting using the Admin User ' + + 'Interface then you can ignore this message.'); } - if (exports.sessionKey) { - logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + - 'use automatic key rotation instead (see the cookie.keyRotationInterval setting).'); + if (this.sessionKey) { + this.logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + + 'use automatic key rotation instead (see the cookie.keyRotationInterval setting).'); } - if (exports.dbType === 'dirty') { - const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; - } + if (this.dbType === 'dirty') { + const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; + if (!this.suppressErrorsInPadText) { + this.defaultPadText += `\nWarning: ${dirtyWarning}${this.suppressDisableMsg}`; + } - exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); + this.dbSettings.filename = makeAbsolute(this.dbSettings.filename); + this.logger.warn(`${dirtyWarning} File location: ${this.dbSettings.filename}`); } - if (exports.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.'); + if (this.ip === '') { + // using Unix socket for connectivity + this.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.'); } /* @@ -958,13 +954,14 @@ 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}`); -}; + this.randomVersionString = randomString(4); + this.logger.info(`Random string used for versioning assets: ${this.randomVersionString}`); + }; +} +const settings = new Settings() + +export default settings + + -exports.exportedForTestingOnly = { - parseSettings, -}; -// initially load settings -exports.reloadSettings(); diff --git a/src/node/utils/UpdateCheck.ts b/src/node/utils/UpdateCheck.ts index 534c5c640fa..abc6a68a6e9 100644 --- a/src/node/utils/UpdateCheck.ts +++ b/src/node/utils/UpdateCheck.ts @@ -1,6 +1,6 @@ 'use strict'; const semver = require('semver'); -const settings = require('./Settings'); +import settings from './Settings'; import axios from 'axios'; const headers = { 'User-Agent': 'Etherpad/' + settings.getEpVersion(), diff --git a/src/node/utils/randomstring.ts b/src/node/utils/randomstring.ts index a86d2856687..548cb09a829 100644 --- a/src/node/utils/randomstring.ts +++ b/src/node/utils/randomstring.ts @@ -3,8 +3,8 @@ * Generates a random String with the given length. Is needed to generate the * Author, Group, readonly, session Ids */ -const cryptoMod = require('crypto'); +import cryptoMod from 'crypto'; const randomString = (len: number) => cryptoMod.randomBytes(len).toString('hex'); -module.exports = randomString; +export default randomString diff --git a/src/node/utils/run_cmd.ts b/src/node/utils/run_cmd.ts index 463b0f07672..4c15f9f589e 100644 --- a/src/node/utils/run_cmd.ts +++ b/src/node/utils/run_cmd.ts @@ -8,7 +8,7 @@ import {Readable} from "node:stream"; const spawn = require('cross-spawn'); const log4js = require('log4js'); const path = require('path'); -const settings = require('./Settings'); +import settings from './Settings'; const logger = log4js.getLogger('runCmd'); diff --git a/src/package.json b/src/package.json index 80031af7af9..cde0331c8b6 100644 --- a/src/package.json +++ b/src/package.json @@ -88,6 +88,7 @@ "@types/jquery": "^3.5.30", "@types/js-cookie": "^3.0.6", "@types/jsdom": "^21.1.7", + "@types/jsonminify": "^0.4.3", "@types/jsonwebtoken": "^9.0.6", "@types/mime-types": "^2.1.4", "@types/mocha": "^10.0.7", diff --git a/src/static/js/pluginfw/LinkInstaller.ts b/src/static/js/pluginfw/LinkInstaller.ts index f4f782de524..963360410e7 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 effed768a80..44e6c25acfe 100644 --- a/src/static/js/pluginfw/installer.ts +++ b/src/static/js/pluginfw/installer.ts @@ -13,7 +13,7 @@ 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 from '../../../node/utils/Settings'; import {LinkInstaller} from "./LinkInstaller"; const {findEtherpadRoot} = require('../../../node/utils/AbsolutePaths'); diff --git a/src/static/js/pluginfw/plugins.ts b/src/static/js/pluginfw/plugins.ts index 97c1694e217..38f51dcbf03 100644 --- a/src/static/js/pluginfw/plugins.ts +++ b/src/static/js/pluginfw/plugins.ts @@ -9,7 +9,7 @@ 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 from '../../../node/utils/Settings' const logger = log4js.getLogger('plugins'); diff --git a/src/templates/pad.html b/src/templates/pad.html index c3e253791f4..f80dc4f16e8 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') ; %> diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 02db406481d..b5c67332632 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-new/specs/admin_utils.ts b/src/tests/backend-new/specs/admin_utils.ts index b115a38a637..254accbfc7d 100644 --- a/src/tests/backend-new/specs/admin_utils.ts +++ b/src/tests/backend-new/specs/admin_utils.ts @@ -1,5 +1,5 @@ -'use strict'; - +import {cleanComments, minify} from "admin/src/utils/utils"; +import {expect, describe, it, beforeAll} from 'vitest' import {strict as assert} from "assert"; import {cleanComments, minify} from "admin/src/utils/utils"; diff --git a/src/tests/backend-new/specs/settings.json b/src/tests/backend-new/specs/settings.json new file mode 100644 index 00000000000..12b4748c097 --- /dev/null +++ b/src/tests/backend-new/specs/settings.json @@ -0,0 +1,39 @@ +// line comment +/* + * block comment + */ +{ + "trailing commas": { + "lists": { + "multiple lines": [ + "", + ] + }, + "objects": { + "multiple lines": { + "key": "", + } + } + }, + "environment variable substitution": { + "set": { + "true": "${SET_VAR_TRUE}", + "false": "${SET_VAR_FALSE}", + "null": "${SET_VAR_NULL}", + "undefined": "${SET_VAR_UNDEFINED}", + "number": "${SET_VAR_NUMBER}", + "string": "${SET_VAR_STRING}", + "empty string": "${SET_VAR_EMPTY_STRING}" + }, + "unset": { + "no default": "${UNSET_VAR}", + "true": "${UNSET_VAR:true}", + "false": "${UNSET_VAR:false}", + "null": "${UNSET_VAR:null}", + "undefined": "${UNSET_VAR:undefined}", + "number": "${UNSET_VAR:123}", + "string": "${UNSET_VAR:foo}", + "empty string": "${UNSET_VAR:}" + } + } +} diff --git a/src/tests/backend/specs/settings.ts b/src/tests/backend-new/specs/settings.ts similarity index 65% rename from src/tests/backend/specs/settings.ts rename to src/tests/backend-new/specs/settings.ts index d6dcaf71ae9..17fc3c28d8c 100644 --- a/src/tests/backend/specs/settings.ts +++ b/src/tests/backend-new/specs/settings.ts @@ -1,9 +1,8 @@ -'use strict'; +import settingsMod from '../../../node/utils/Settings'; -const assert = require('assert').strict; -const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly; import path from 'path'; import process from 'process'; +import {expect, describe, it, beforeAll} from 'vitest' describe(__filename, function () { describe('parseSettings', function () { @@ -18,11 +17,11 @@ describe(__filename, function () { {name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''}, ]; - before(async function () { + beforeAll(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); - assert(settings != null); + settings = settingsMod.parseSettings(path.join(__dirname, 'settings.json'), true); + expect(settings).not.toBe(null); }); describe('environment variable substitution', function () { @@ -31,9 +30,9 @@ describe(__filename, function () { it(tc.name, async function () { const obj = settings['environment variable substitution'].set; if (tc.name === 'undefined') { - assert(!(tc.name in obj)); + expect(obj[tc.name]).toBe(undefined); } else { - assert.equal(obj[tc.name], tc.want); + expect(obj[tc.name]).toBe(tc.want); } }); } @@ -42,16 +41,16 @@ describe(__filename, function () { describe('unset', function () { it('no default', async function () { const obj = settings['environment variable substitution'].unset; - assert.equal(obj['no default'], null); + expect(obj['no default']).toBe(null); }); for (const tc of envVarSubstTestCases) { it(tc.name, async function () { const obj = settings['environment variable substitution'].unset; if (tc.name === 'undefined') { - assert(!(tc.name in obj)); + expect(obj[tc.name]).toBe(undefined); } else { - assert.equal(obj[tc.name], tc.want); + expect(obj[tc.name]).toBe(tc.want); } }); } @@ -62,31 +61,31 @@ describe(__filename, function () { describe("Parse plugin settings", function () { - before(async function () { + beforeAll(async function () { process.env["EP__ADMIN__PASSWORD"] = "test" }) it('should parse plugin settings', async function () { - let settings = parseSettings(path.join(__dirname, 'settings.json'), true); - assert.equal(settings.ADMIN.PASSWORD, "test"); + let settings = settingsMod.parseSettings(path.join(__dirname, 'settings.json'), true); + expect(settings!.ADMIN.PASSWORD).toBe("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 = settingsMod.parseSettings(path.join(__dirname, 'settings.json'), true); + expect(settings!.ADMIN).toEqual({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 = settingsMod.parseSettings(path.join(__dirname, 'settings.json'), true); + expect(settings!.ep_themes.default_theme).toBe("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 = settingsMod.parseSettings(path.join(__dirname, 'settings.json'), true); + expect(settings!.ep_webrtc.enabled).toBe(true); }) }) }); diff --git a/src/tests/backend/common.ts b/src/tests/backend/common.ts index 4f39375462c..bae3f2aafab 100644 --- a/src/tests/backend/common.ts +++ b/src/tests/backend/common.ts @@ -8,9 +8,9 @@ const io = require('socket.io-client'); const log4js = require('log4js'); import padutils from '../../static/js/pad_utils'; const process = require('process'); -const server = require('../../node/server'); +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"; @@ -26,7 +26,7 @@ export let baseUrl:string|null = null; export let httpServer: Http2Server|null = null; export const logger = log4js.getLogger('test'); -const logLevel = logger.level; +const logLevel = logger.level as log4js.Level; // Mocha doesn't monitor unhandled Promise rejections, so convert them to uncaught exceptions. // https://github.com/mochajs/mocha/issues/2640 diff --git a/src/tests/backend/specs/ImportEtherpad.ts b/src/tests/backend/specs/ImportEtherpad.ts index b9ac9baaf96..969346bbbc1 100644 --- a/src/tests/backend/specs/ImportEtherpad.ts +++ b/src/tests/backend/specs/ImportEtherpad.ts @@ -4,7 +4,7 @@ import {MapArrayType} from "../../../node/types/MapType"; const assert = require('assert').strict; const authorManager = require('../../../node/db/AuthorManager'); -const db = require('../../../node/db/DB'); +import db from '../../../node/db/DB'; const importEtherpad = require('../../../node/utils/ImportEtherpad'); const padManager = require('../../../node/db/PadManager'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); diff --git a/src/tests/backend/specs/SecretRotator.ts b/src/tests/backend/specs/SecretRotator.ts index d95b6dba1b1..d06975f73fb 100644 --- a/src/tests/backend/specs/SecretRotator.ts +++ b/src/tests/backend/specs/SecretRotator.ts @@ -3,7 +3,7 @@ import {strict} from "assert"; const common = require('../common'); const crypto = require('../../../node/security/crypto'); -const db = require('../../../node/db/DB'); +import db from '../../../node/db/DB'; const SecretRotator = require("../../../node/security/SecretRotator").SecretRotator; const logger = common.logger; @@ -121,7 +121,8 @@ describe(__filename, function () { if (sr != null) sr.stop(); sr = null; await Promise.all( - (await db.findKeys(`${dbPrefix}:*`, null)).map(async (dbKey: string) => await db.remove(dbKey))); + // @ts-ignore + (await db.findKeys(`${dbPrefix}:*`, null)).map(async (dbKey: string) => await db.remove(dbKey))); }); describe('constructor', function () { @@ -163,6 +164,7 @@ describe(__filename, function () { sr = newRotator(); const fc = setFakeClock(sr); await sr.start(); + // @ts-ignore const dbKeys = await db.findKeys(`${dbPrefix}:*`, null); strict.equal(dbKeys.length, 1); const [id] = dbKeys; @@ -196,12 +198,14 @@ describe(__filename, function () { const fc = setFakeClock(sr); await sr.start(); const {secrets} = sr; + // @ts-ignore const dbKeys = await db.findKeys(`${dbPrefix}:*`, null); sr.stop(); sr = newRotator(); setFakeClock(sr, fc); await sr.start(); strict.deepEqual(sr.secrets, secrets); + // @ts-ignore strict.deepEqual(await db.findKeys(`${dbPrefix}:*`, null), dbKeys); }); @@ -209,6 +213,7 @@ describe(__filename, function () { sr = newRotator(); const fc = setFakeClock(sr); await sr.start(); + // @ts-ignore const [oldId] = await db.findKeys(`${dbPrefix}:*`, null); strict(oldId != null); sr.stop(); @@ -217,6 +222,7 @@ describe(__filename, function () { sr = newRotator(); setFakeClock(sr, fc); await sr.start(); + // @ts-ignore const ids = await db.findKeys(`${dbPrefix}:*`, null); strict.equal(ids.length, 1); const [newId] = ids; @@ -229,6 +235,7 @@ describe(__filename, function () { await sr.start(); const [, , future] = sr.secrets; sr.stop(); + // @ts-ignore const [origId] = await db.findKeys(`${dbPrefix}:*`, null); const p = await db.get(origId); await fc.advance(p.end + p.lifetime + p.interval - 1); @@ -237,6 +244,7 @@ describe(__filename, function () { await sr.start(); strict(sr.secrets.slice(1).includes(future)); // It should have created a new publication, not extended the life of the old publication. + // @ts-ignore strict.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); strict.deepEqual(await db.get(origId), p); }); @@ -247,10 +255,12 @@ describe(__filename, function () { await sr.start(); strict.equal(fc.timeouts.size, 1); const secrets = [...sr.secrets]; + // @ts-ignore const dbKeys = await db.findKeys(`${dbPrefix}:*`, null); await sr.start(); strict.equal(fc.timeouts.size, 1); strict.deepEqual(sr.secrets, secrets); + // @ts-ignore strict.deepEqual(await db.findKeys(`${dbPrefix}:*`, null), dbKeys); }); @@ -335,6 +345,7 @@ describe(__filename, function () { await sr.start(); strict.equal(sr.secrets.length, 4); // 1 for the legacy secret, 3 for past, current, future strict(sr.secrets.slice(1).includes('legacy')); // Should not be the current secret. + // @ts-ignore const ids = await db.findKeys(`${dbPrefix}:*`, null); const params = (await Promise.all(ids.map(async (id:string) => await db.get(id)))) .sort((a, b) => a.algId - b.algId); @@ -380,6 +391,7 @@ describe(__filename, function () { await sr.start(); strict.equal(sr.secrets.length, 5); // s0 through s3 and the legacy secret. strict.deepEqual(sr.secrets, [s2, s1, s0, sr.secrets[3], 'legacy']); + // @ts-ignore const ids = await db.findKeys(`${dbPrefix}:*`, null); const params = (await Promise.all(ids.map(async (id:string) => await db.get(id)))) .sort((a, b) => a.algId - b.algId); @@ -425,6 +437,7 @@ describe(__filename, function () { await sr.start(); strict.deepEqual(sr.secrets, [...new Set(sr.secrets)]); // There shouldn't be multiple publications for the same legacy secret. + // @ts-ignore strict.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); }); @@ -511,8 +524,10 @@ describe(__filename, function () { sr = newRotator(); setFakeClock(sr, fc); await sr.start(); + // @ts-ignore strict.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 2); await fc.advance(lifetime + (3 * origInterval)); + // @ts-ignore strict.equal((await db.findKeys(`${dbPrefix}:*`, null)).length, 1); }); diff --git a/src/tests/backend/specs/SessionStore.ts b/src/tests/backend/specs/SessionStore.ts index 5dfc44ff2b2..010ff7b6574 100644 --- a/src/tests/backend/specs/SessionStore.ts +++ b/src/tests/backend/specs/SessionStore.ts @@ -1,9 +1,9 @@ 'use strict'; -const SessionStore = require('../../../node/db/SessionStore'); +import SessionStore from '../../../node/db/SessionStore'; import {strict as assert} from 'assert'; const common = require('../common'); -const db = require('../../../node/db/DB'); +import db from '../../../node/db/DB'; import util from 'util'; type Session = { @@ -15,7 +15,7 @@ type Session = { } describe(__filename, function () { - let ss: Session|null; + let ss: SessionStore|null; let sid: string|null; const set = async (sess: string|null) => await util.promisify(ss!.set).call(ss, sid, sess); diff --git a/src/tests/backend/specs/api/sessionsAndGroups.ts b/src/tests/backend/specs/api/sessionsAndGroups.ts index a7e85fbe97a..b6d20e17ca5 100644 --- a/src/tests/backend/specs/api/sessionsAndGroups.ts +++ b/src/tests/backend/specs/api/sessionsAndGroups.ts @@ -5,7 +5,7 @@ import {agent, generateJWTToken, init, logger} from "../../common"; import TestAgent from "supertest/lib/agent"; import supertest from "supertest"; const assert = require('assert').strict; -const db = require('../../../../node/db/DB'); +import db from '../../../../node/db/DB'; let apiVersion = 1; let groupID = ''; diff --git a/src/tests/backend/specs/regression-db.ts b/src/tests/backend/specs/regression-db.ts index ba50e524096..ca5117aa208 100644 --- a/src/tests/backend/specs/regression-db.ts +++ b/src/tests/backend/specs/regression-db.ts @@ -2,14 +2,14 @@ const AuthorManager = require('../../../node/db/AuthorManager'); import {strict as assert} from "assert"; -const common = require('../common'); -const db = require('../../../node/db/DB'); +import {init} from '../common'; +import db from '../../../node/db/DB'; describe(__filename, function () { let setBackup: Function; before(async function () { - await common.init(); + await init(); setBackup = db.set; db.set = async (...args:any) => { @@ -20,7 +20,7 @@ describe(__filename, function () { }); after(async function () { - db.set = setBackup; + db.set = setBackup as any; }); it('regression test for missing await in createAuthor (#5000)', async function () {