diff --git a/src/bin/createUserSession.js b/src/bin/createUserSession.js index cf04dd4e3..95fbce90b 100644 --- a/src/bin/createUserSession.js +++ b/src/bin/createUserSession.js @@ -12,7 +12,7 @@ process.on('unhandledRejection', (err) => { throw err; }); const fs = require('fs'); const path = require('path'); const querystring = require('querystring'); -import * as settings from '../node/utils/Settings'; +import {settings} from '../node/utils/Settings'; const supertest = require('supertest'); (async () => { diff --git a/src/bin/importSqlFile.js b/src/bin/importSqlFile.js index 517d11ec8..50029a88c 100644 --- a/src/bin/importSqlFile.js +++ b/src/bin/importSqlFile.js @@ -49,7 +49,7 @@ const unescape = (val) => { const fs = require('fs'); const log4js = require('log4js'); const readline = require('readline'); - import * as settings from '../node/utils/Settings'; + import {settings} from '../node/utils/Settings'; const ueberDB = require('ueberdb2'); const dbWrapperSettings = { diff --git a/src/bin/migrateDirtyDBtoRealDB.js b/src/bin/migrateDirtyDBtoRealDB.js index 25b5d85af..8af06e8a6 100644 --- a/src/bin/migrateDirtyDBtoRealDB.js +++ b/src/bin/migrateDirtyDBtoRealDB.js @@ -14,7 +14,7 @@ process.on('unhandledRejection', (err) => { throw err; }); const dirtyDb = require('dirty'); const log4js = require('log4js'); - import * as settings from '../node/utils/Settings'; + import {settings} from '../node/utils/Settings'; const ueberDB = require('ueberdb2'); const util = require('util'); diff --git a/src/bin/repairPad.js b/src/bin/repairPad.js index afb5a3207..0874572fa 100644 --- a/src/bin/repairPad.js +++ b/src/bin/repairPad.js @@ -19,7 +19,7 @@ let valueCount = 0; (async () => { // initialize database - import * as settings from '../node/utils/Settings'; + import {settings} from '../node/utils/Settings'; const db = require('../node/db/DB'); await db.init(); diff --git a/src/node/db/DB.ts b/src/node/db/DB.ts index f248003dc..934892f8c 100644 --- a/src/node/db/DB.ts +++ b/src/node/db/DB.ts @@ -21,7 +21,7 @@ * limitations under the License. */ -import {dbSettings, dbType} from "../utils/Settings"; +import {settings} from "../utils/Settings"; const ueberDB = require('ueberdb2'); const log4js = require('log4js'); @@ -38,7 +38,7 @@ exports.db = null; * Initializes the database with the settings provided by the settings module */ exports.init = async () => { - exports.db = new ueberDB.Database(dbType, dbSettings, null, logger); + exports.db = new ueberDB.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)) { diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index 5bdd0915a..9eb07e124 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -10,7 +10,7 @@ const AttributePool = require('../../static/js/AttributePool'); const Stream = require('../utils/Stream'); const assert = require('assert').strict; const db = require('./DB'); -import * as settings from '../utils/Settings'; +import {settings} from '../utils/Settings'; const authorManager = require('./AuthorManager'); const padManager = require('./PadManager'); const padMessageHandler = require('../handler/PadMessageHandler'); diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 28eca950c..b07978de4 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -22,7 +22,7 @@ const CustomError = require('../utils/customError'); const Pad = require('../db/Pad'); const db = require('./DB'); -import * as settings from '../utils/Settings'; +import {settings} from '../utils/Settings'; /** * A cache of all loaded Pads. diff --git a/src/node/db/SecurityManager.js b/src/node/db/SecurityManager.js index 694f77cb2..30f4819cb 100644 --- a/src/node/db/SecurityManager.js +++ b/src/node/db/SecurityManager.js @@ -24,7 +24,7 @@ const hooks = require('../../static/js/pluginfw/hooks.js'); const padManager = require('./PadManager'); const readOnlyManager = require('./ReadOnlyManager'); const sessionManager = require('./SessionManager'); -import * as settings from '../utils/Settings'; +import {settings} from '../utils/Settings'; const webaccess = require('../hooks/express/webaccess'); const log4js = require('log4js'); const authLogger = log4js.getLogger('auth'); diff --git a/src/node/eejs/index.js b/src/node/eejs/index.ts similarity index 90% rename from src/node/eejs/index.js rename to src/node/eejs/index.ts index f8165f3a3..85b72ec05 100644 --- a/src/node/eejs/index.js +++ b/src/node/eejs/index.ts @@ -25,7 +25,7 @@ const fs = require('fs'); const hooks = require('../../static/js/pluginfw/hooks.js'); const path = require('path'); const resolve = require('resolve'); -import * as settings from '../utils/Settings' +import {root, settings} from '../utils/Settings' const templateCache = new Map(); @@ -38,16 +38,16 @@ exports.info = { const getCurrentFile = () => exports.info.file_stack[exports.info.file_stack.length - 1]; -exports._init = (b, recursive) => { +exports._init = (b:string, recursive: boolean) => { exports.info.__output_stack.push(exports.info.__output); exports.info.__output = b; }; -exports._exit = (b, recursive) => { +exports._exit = (b: string, recursive: boolean) => { exports.info.__output = exports.info.__output_stack.pop(); }; -exports.begin_block = (name) => { +exports.begin_block = (name:string) => { exports.info.block_stack.push(name); exports.info.__output_stack.push(exports.info.__output.get()); exports.info.__output.set(''); @@ -63,7 +63,7 @@ exports.end_block = () => { exports.info.__output.set(exports.info.__output.get().concat(args.content)); }; -exports.require = (name, args, mod) => { +exports.require = (name: string, args: any, mod: any) => { if (args == null) args = {}; let basedir = __dirname; @@ -76,7 +76,7 @@ exports.require = (name, args, mod) => { basedir = path.dirname(mod.filename); paths = mod.paths; } - paths.push(settings.root + '/plugin_packages') + paths.push(root + '/plugin_packages') const ejspath = resolve.sync(name, {paths, basedir, extensions: ['.html', '.ejs']}); diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index 64f64b95d..d6680b485 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -24,7 +24,7 @@ const exporthtml = require('../utils/ExportHtml'); const exporttxt = require('../utils/ExportTxt'); const exportEtherpad = require('../utils/ExportEtherpad'); const fs = require('fs'); -import * as settings from '../utils/Settings'; +import {settings} from '../utils/Settings'; const os = require('os'); const hooks = require('../../static/js/pluginfw/hooks'); const util = require('util'); diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js index f24d04644..63c20f46b 100644 --- a/src/node/handler/ImportHandler.js +++ b/src/node/handler/ImportHandler.js @@ -25,7 +25,7 @@ const padManager = require('../db/PadManager'); const padMessageHandler = require('./PadMessageHandler'); const fs = require('fs').promises; const path = require('path'); -import * as settings from '../utils/Settings'; +import {settings} from '../utils/Settings'; const {Formidable} = require('formidable'); const os = require('os'); const importHtml = require('../utils/ImportHtml'); diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 4123b3925..9f02355d7 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -28,7 +28,7 @@ const AttributeManager = require('../../static/js/AttributeManager'); const authorManager = require('../db/AuthorManager'); const {padutils} = require('../../static/js/pad_utils'); const readOnlyManager = require('../db/ReadOnlyManager'); -import * as settings from '../utils/Settings'; +import {settings} from '../utils/Settings'; const securityManager = require('../db/SecurityManager'); const plugins = require('../../static/js/pluginfw/plugin_defs.js'); const log4js = require('log4js'); diff --git a/src/node/handler/SocketIORouter.js b/src/node/handler/SocketIORouter.js index 43581ae8f..bbc05cc87 100644 --- a/src/node/handler/SocketIORouter.js +++ b/src/node/handler/SocketIORouter.js @@ -21,7 +21,7 @@ */ const log4js = require('log4js'); -import * as settings from '../utils/Settings'; +import {settings} from '../utils/Settings'; const stats = require('../../node/stats') const logger = log4js.getLogger('socket.io'); diff --git a/src/node/hooks/express.ts b/src/node/hooks/express.ts index 23d0b30a2..7fff7a8a2 100644 --- a/src/node/hooks/express.ts +++ b/src/node/hooks/express.ts @@ -14,7 +14,7 @@ import fs from 'fs'; const hooks = require('../../static/js/pluginfw/hooks'); import log4js from 'log4js'; const SessionStore = require('../db/SessionStore'); -import * as settings from '../utils/Settings'; +import {getEpVersion, getGitCommit, settings} from '../utils/Settings'; const stats = require('../stats') import util from 'util'; const webaccess = require('./express/webaccess'); @@ -68,9 +68,9 @@ const closeServer = async () => { exports.createServer = async () => { console.log('Report bugs at https://github.com/ether/etherpad-lite/issues'); - serverName = `Etherpad ${settings.getGitCommit()} (https://etherpad.org)`; + serverName = `Etherpad ${getGitCommit()} (https://etherpad.org)`; - console.log(`Your Etherpad version is ${settings.getEpVersion()} (${settings.getGitCommit()})`); + console.log(`Your Etherpad version is ${getEpVersion()} (${getGitCommit()})`); await exports.restartServer(); diff --git a/src/node/hooks/express/adminplugins.ts b/src/node/hooks/express/adminplugins.ts index 0a550abbf..4de0236a9 100644 --- a/src/node/hooks/express/adminplugins.ts +++ b/src/node/hooks/express/adminplugins.ts @@ -7,7 +7,7 @@ import {QueryType} from "../../types/QueryType"; import {PluginType} from "../../types/Plugin"; const eejs = require('../../eejs'); -import * as settings from '../../utils/Settings'; +import {getEpVersion, getGitCommit} from '../../utils/Settings'; const installer = require('../../../static/js/pluginfw/installer'); const pluginDefs = require('../../../static/js/pluginfw/plugin_defs'); const plugins = require('../../../static/js/pluginfw/plugins'); @@ -24,8 +24,8 @@ exports.expressCreateServer = (hookName:string, args: ArgsExpressType, cb:Functi }); args.app.get('/admin/plugins/info', (req:any, res:any) => { - const gitCommit = settings.getGitCommit(); - const epVersion = settings.getEpVersion(); + const gitCommit = getGitCommit(); + const epVersion = getEpVersion(); res.send(eejs.require('ep_etherpad-lite/templates/admin/plugins-info.html', { gitCommit, diff --git a/src/node/hooks/express/adminsettings.ts b/src/node/hooks/express/adminsettings.ts index 4db29bc78..ab9ebb27b 100644 --- a/src/node/hooks/express/adminsettings.ts +++ b/src/node/hooks/express/adminsettings.ts @@ -4,8 +4,7 @@ const eejs = require('../../eejs'); const fsp = require('fs').promises; const hooks = require('../../../static/js/pluginfw/hooks'); const plugins = require('../../../static/js/pluginfw/plugins'); -import {reloadSettings, settingsFilename, showSettingsInAdminPage} from '../../utils/Settings'; -import * as settings from '../../utils/Settings'; +import {reloadSettings, settings} from '../../utils/Settings'; exports.expressCreateServer = (hookName:string, {app}:any) => { app.get('/admin/settings', (req:any, res:any) => { @@ -26,12 +25,12 @@ exports.socketio = (hookName:string, {io}:any) => { socket.on('load', async (query:string):Promise => { let data; try { - data = await fsp.readFile(settingsFilename, 'utf8'); + data = await fsp.readFile(settings.settingsFilename, 'utf8'); } catch (err) { return console.log(err); } // if showSettingsInAdminPage is set to false, then return NOT_ALLOWED in the result - if (!showSettingsInAdminPage) { + if (!settings.showSettingsInAdminPage) { socket.emit('settings', {results: 'NOT_ALLOWED'}); } else { socket.emit('settings', {results: data}); @@ -39,7 +38,7 @@ exports.socketio = (hookName:string, {io}:any) => { }); socket.on('saveSettings', async (newSettings:string) => { - await fsp.writeFile(settingsFilename, newSettings); + await fsp.writeFile(settings.settingsFilename, newSettings); socket.emit('saveprogress', 'saved'); }); diff --git a/src/node/hooks/express/apicalls.ts b/src/node/hooks/express/apicalls.ts index 91c44e389..7c2ee8ad7 100644 --- a/src/node/hooks/express/apicalls.ts +++ b/src/node/hooks/express/apicalls.ts @@ -2,7 +2,7 @@ const log4js = require('log4js'); const clientLogger = log4js.getLogger('client'); -const {Formidable} = require('formidable'); +import {Formidable} from 'formidable'; const apiHandler = require('../../handler/APIHandler'); const util = require('util'); @@ -25,6 +25,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { // The Etherpad client side sends information about client side javscript errors app.post('/jserror', (req:any, res:any, next:Function) => { (async () => { + // @ts-ignore const data = JSON.parse(await parseJserrorForm(req)); clientLogger.warn(`${data.msg} --`, { [util.inspect.custom]: (depth: number, options:any) => { diff --git a/src/node/hooks/express/importexport.ts b/src/node/hooks/express/importexport.ts index 329356fe1..27659bbc2 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'); -import {exportAvailable, importExportRateLimiting} from '../../utils/Settings'; +import {exportAvailable, settings} from '../../utils/Settings'; const exportHandler = require('../../handler/ExportHandler'); const importHandler = require('../../handler/ImportHandler'); const padManager = require('../../db/PadManager'); @@ -14,7 +14,7 @@ const webaccess = require('./webaccess'); exports.expressCreateServer = (hookName:string, args:ArgsExpressType, cb:Function) => { const limiter = rateLimit({ - ...importExportRateLimiting, + ...settings.importExportRateLimiting, handler: (request:any) => { if (request.rateLimit.current === request.rateLimit.limit + 1) { // when the rate limiter triggers, write a warning in the logs diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index 1434d5b51..51a9d73d7 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'); -import {ssl} from '../../utils/Settings'; +import {settings} from '../../utils/Settings'; const log4js = require('log4js'); const logger = log4js.getLogger('API'); @@ -724,5 +724,5 @@ const getApiRootForVersion = (version:string, style:any = APIPathStyle.FLAT): st const generateServerForApiVersion = (apiRoot:string, req:any): { url:string } => ({ - url: `${ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`, + url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`, }); diff --git a/src/node/hooks/express/socketio.ts b/src/node/hooks/express/socketio.ts index 87df60050..a202b02a1 100644 --- a/src/node/hooks/express/socketio.ts +++ b/src/node/hooks/express/socketio.ts @@ -6,7 +6,7 @@ const events = require('events'); const express = require('../express'); const log4js = require('log4js'); const proxyaddr = require('proxy-addr'); -import * as settings from '../../utils/Settings'; +import {settings} from '../../utils/Settings'; import {Server} from 'socket.io' const socketIORouter = require('../../handler/SocketIORouter'); const hooks = require('../../../static/js/pluginfw/hooks'); diff --git a/src/node/hooks/express/specialpages.js b/src/node/hooks/express/specialpages.js index cf7debdfb..6b658582d 100644 --- a/src/node/hooks/express/specialpages.js +++ b/src/node/hooks/express/specialpages.js @@ -6,7 +6,7 @@ const fs = require('fs'); const fsp = fs.promises; const toolbar = require('../../utils/toolbar'); const hooks = require('../../../static/js/pluginfw/hooks'); -import * as settings from '../../utils/Settings'; +import {getEpVersion, root, settings} from '../../utils/Settings'; const util = require('util'); const webaccess = require('./webaccess'); @@ -17,7 +17,7 @@ exports.expressPreSession = async (hookName, {app}) => { res.set('Content-Type', 'application/health+json'); res.json({ status: 'pass', - releaseId: settings.getEpVersion(), + releaseId: getEpVersion(), }); }); @@ -31,11 +31,11 @@ exports.expressPreSession = async (hookName, {app}) => { app.get('/robots.txt', (req, res) => { let filePath = - path.join(settings.root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); + path.join(root, 'src', 'static', 'skins', settings.skinName, 'robots.txt'); res.sendFile(filePath, (err) => { // there is no custom robots.txt, send the default robots.txt which dissallows all if (err) { - filePath = path.join(settings.root, 'src', 'static', 'robots.txt'); + filePath = path.join(root, 'src', 'static', 'robots.txt'); res.sendFile(filePath); } }); @@ -54,9 +54,9 @@ exports.expressPreSession = async (hookName, {app}) => { 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', 'favicon.ico'), + ...(settings.favicon ? [path.resolve(root, settings.favicon)] : []), + path.join(root, 'src', 'static', 'skins', settings.skinName, 'favicon.ico'), + path.join(root, 'src', 'static', 'favicon.ico'), ]; for (const fn of fns) { try { diff --git a/src/node/hooks/express/static.js b/src/node/hooks/express/static.ts similarity index 76% rename from src/node/hooks/express/static.js rename to src/node/hooks/express/static.ts index d6608cd60..3ebfb2d44 100644 --- a/src/node/hooks/express/static.js +++ b/src/node/hooks/express/static.ts @@ -1,34 +1,37 @@ 'use strict'; + const fs = require('fs').promises; const minify = require('../../utils/Minify'); const path = require('path'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); -import * as settings from '../../utils/Settings'; +import {root, settings} from '../../utils/Settings'; const CachingMiddleware = require('../../utils/caching_middleware'); const Yajsml = require('etherpad-yajsml'); // Rewrite tar to include modules with no extensions and proper rooted paths. const getTar = async () => { - const prefixLocalLibraryPath = (path) => { + const prefixLocalLibraryPath = (path: string) => { if (path.charAt(0) === '$') { return path.slice(1); } else { return `ep_etherpad-lite/static/js/${path}`; } }; - const tarJson = await fs.readFile(path.join(settings.root, 'src/node/utils/tar.json'), 'utf8'); + const tarJson = await fs.readFile(path.join(root, 'src/node/utils/tar.json'), 'utf8'); const tar = {}; for (const [key, relativeFiles] of Object.entries(JSON.parse(tarJson))) { + // @ts-ignore const files = relativeFiles.map(prefixLocalLibraryPath); + // @ts-ignore tar[prefixLocalLibraryPath(key)] = files - .concat(files.map((p) => p.replace(/\.js$/, ''))) - .concat(files.map((p) => `${p.replace(/\.js$/, '')}/index.js`)); + .concat(files.map((p:string) => p.replace(/\.js$/, ''))) + .concat(files.map((p:string) => `${p.replace(/\.js$/, '')}/index.js`)); } return tar; }; -exports.expressPreSession = async (hookName, {app}) => { +exports.expressPreSession = async (hookName: string, {app}:any) => { // Cache both minified and static. const assetCache = new CachingMiddleware(); app.all(/\/javascripts\/(.*)/, assetCache.handle.bind(assetCache)); @@ -58,11 +61,13 @@ exports.expressPreSession = async (hookName, {app}) => { // serve plugin definitions // not very static, but served here so that client can do // require("pluginfw/static/js/plugin-definitions.js"); - app.get('/pluginfw/plugin-definitions.json', (req, res, next) => { - const clientParts = plugins.parts.filter((part) => part.client_hooks != null); + app.get('/pluginfw/plugin-definitions.json', (req:any, res:any, next:Function) => { + const clientParts = plugins.parts.filter((part:any) => part.client_hooks != null); const clientPlugins = {}; - for (const name of new Set(clientParts.map((part) => part.plugin))) { + for (const name of new Set(clientParts.map((part:any) => part.plugin))) { + // @ts-ignore clientPlugins[name] = {...plugins.plugins[name]}; + // @ts-ignore delete clientPlugins[name].package; } res.setHeader('Content-Type', 'application/json; charset=utf-8'); diff --git a/src/node/hooks/express/tests.js b/src/node/hooks/express/tests.js index 7959a9ca7..3b8623207 100644 --- a/src/node/hooks/express/tests.js +++ b/src/node/hooks/express/tests.js @@ -4,7 +4,7 @@ const path = require('path'); const fsp = require('fs').promises; const plugins = require('../../../static/js/pluginfw/plugin_defs'); const sanitizePathname = require('../../utils/sanitizePathname'); -import * as settings from '../../utils/Settings'; +import {root, 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. @@ -57,7 +57,7 @@ exports.expressPreSession = async (hookName, {app}) => { })().catch((err) => next(err || new Error(err))); }); - const rootTestFolder = path.join(settings.root, 'src/tests/frontend/'); + const rootTestFolder = path.join(root, 'src/tests/frontend/'); app.get('/tests/frontend/index.html', (req, res) => { res.redirect(['./', ...req.url.split('?').slice(1)].join('?')); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 38e683d76..b8793f659 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -3,7 +3,7 @@ const assert = require('assert').strict; const log4js = require('log4js'); const httpLogger = log4js.getLogger('http'); -import * as settings from '../../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 37ecefbf4..7dcc26de1 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.js'); const existsSync = require('../utils/path_exists'); -import {customLocaleStrings, maxAge, root} from '../utils/Settings'; +import {settings, root} from '../utils/Settings'; // returns all existing messages merged together and grouped by langcode // {es: {"foo": "string"}, en:...} @@ -71,9 +71,9 @@ const getAllLocales = () => { const wrongFormatErr = Error( 'customLocaleStrings in wrong format. See documentation ' + 'for Customization for Administrators, under Localization.'); - if (customLocaleStrings) { - if (typeof customLocaleStrings !== 'object') throw wrongFormatErr; - _.each(customLocaleStrings, (overrides:MapArrayType , langcode:string) => { + if (settings.customLocaleStrings) { + if (typeof settings.customLocaleStrings !== 'object') throw wrongFormatErr; + _.each(settings.customLocaleStrings, (overrides:MapArrayType , langcode:string) => { if (typeof overrides !== 'object') throw wrongFormatErr; _.each(overrides, (localeString:string|object, key:string) => { if (typeof localeString !== 'string') throw wrongFormatErr; @@ -133,7 +133,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { // works with /locale/en and /locale/en.json requests const locale = req.params.locale.split('.')[0]; if (Object.prototype.hasOwnProperty.call(exports.availableLangs, locale)) { - res.setHeader('Cache-Control', `public, max-age=${maxAge}`); + res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(`{"${locale}":${JSON.stringify(locales[locale])}}`); } else { @@ -142,7 +142,7 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { }); app.get('/locales.json', (req: any, res:any) => { - res.setHeader('Cache-Control', `public, max-age=${maxAge}`); + res.setHeader('Cache-Control', `public, max-age=${settings.maxAge}`); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.send(localeIndex); }); diff --git a/src/node/server.ts b/src/node/server.ts index 3804c29cd..005d725dd 100755 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -28,11 +28,10 @@ import {PluginType} from "./types/Plugin"; import {ErrorCaused} from "./types/ErrorCaused"; import log4js from 'log4js'; -import {dumpOnUncleanExit} from './utils/Settings'; -import * as settings from './utils/Settings'; +import {settings} from './utils/Settings'; let wtfnode: any; -if (dumpOnUncleanExit) { +if (settings.dumpOnUncleanExit) { // wtfnode should be loaded after log4js.replaceConsole() so that it uses log4js for logging, and // it should be above everything else so that it can hook in before resources are used. wtfnode = require('wtfnode'); @@ -266,7 +265,7 @@ exports.exit = async (err: ErrorCaused|string|null = null) => { logger.error('Something that should have been cleaned up during the shutdown hook (such as ' + 'a timer, worker thread, or open connection) is preventing Node.js from exiting'); - if (dumpOnUncleanExit) { + if (settings.dumpOnUncleanExit) { wtfnode.dump(); } else { logger.error('Enable `dumpOnUncleanExit` setting to get a dump of objects preventing a ' + diff --git a/src/node/types/SettingsObj.ts b/src/node/types/SettingsObj.ts new file mode 100644 index 000000000..d256c2573 --- /dev/null +++ b/src/node/types/SettingsObj.ts @@ -0,0 +1,113 @@ +export type SettingsObj = { + settingsFilename: string; + credentialsFilename: string; + title: string; + favicon: string|null; + skinName: string|null; + skinVariants: string; + ip: string; + port: string|number, + suppressErrorsInPadText: boolean, + ssl:false|{ + key: string; + cert: string; + ca: string[]; + }, + socketTransportProtocols: string[]; + socketIo: { + maxHttpBufferSize: number; + }, + dbType: string; + dbSettings: any, + defaultPadText: string, + padOptions:{ + noColors: boolean; + showControls: boolean; + showChat: boolean; + showLineNumbers: boolean; + useMonospaceFont: boolean; + userName: string|null; + userColor: string|null; + rtl: boolean; + alwaysShowChat: boolean; + chatAndUsers: boolean; + lang: string|null; + }, + padShortcutEnabled: { + altF9: boolean; + altC: boolean; + delete: boolean; + cmdShift2: boolean; + return: boolean; + esc: boolean; + cmdS: boolean; + tab: boolean; + cmdZ: boolean; + cmdY: boolean; + cmdB: boolean; + cmdI: boolean; + cmdU: boolean; + cmd5: boolean; + cmdShiftL: boolean; + cmdShiftN: boolean; + cmdShift1: boolean; + cmdShiftC: boolean; + cmdH: boolean; + ctrlHome: boolean; + pageUp: boolean; + pageDown: boolean; + }, + toolbar: { + left: string[][]; + right: string[][]; + timeslider: string[][]; + }, + requireSession: boolean; + editOnly: boolean; + maxAge: number; + minify: boolean; + abiword: string|null; + soffice: string|null; + allowUnknownFileEnds: boolean; + loglevel: string; + disableIPlogging: boolean; + automaticReconnectionTimeout: number; + loadTest: boolean; + dumpOnUncleanExit: boolean; + indentationOnNewLine: boolean; + logconfig: any; + sessionKey:string|null|string[], + trustProxy: boolean; + cookie:{ + keyRotationInterval: number; + sameSite: string; + sessionLifetime: number; + sessionRefreshInterval: number; + }, + requireAuthentication: boolean; + requireAuthorization: boolean; + users: object, + showSettingsInAdminPage: boolean; + scrollWhenFocusLineIsOutOfViewport:{ + percentage: { + editionAboveViewport: number; + editionBelowViewport: number; + }, + duration: number; + percentageToScrollWhenUserPressesArrowUp: number, + scrollWhenCaretIsInTheLastLineOfViewport: boolean + }, + exposeVersion: boolean; + customLocaleStrings: {}, + importExportRateLimiting: { + windowMs: number; + max: number; + }, + commitRateLimiting: { + duration: number; + points: number; + }, + importMaxFileSize: number; + enableAdminUITests: boolean; + lowerCasePadIds: boolean; +} diff --git a/src/node/utils/Abiword.ts b/src/node/utils/Abiword.ts index 01c1df77c..0e1921708 100644 --- a/src/node/utils/Abiword.ts +++ b/src/node/utils/Abiword.ts @@ -24,14 +24,14 @@ import {AsyncQueueTask} from "../types/AsyncQueueTask"; const spawn = require('child_process').spawn; const async = require('async'); -import {abiword} from './Settings'; +import {settings} from './Settings'; const os = require('os'); // on windows we have to spawn a process for each convertion, // cause the plugin abicommand doesn't exist on this platform if (os.type().indexOf('Windows') > -1) { exports.convertFile = async (srcFile: string, destFile: string, type: string) => { - const _abiword = spawn(abiword, [`--to=${destFile}`, srcFile]); + const _abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]); let stdoutBuffer = ''; _abiword.stdout.on('data', (data: string) => { stdoutBuffer += data.toString(); }); _abiword.stderr.on('data', (data: string) => { stdoutBuffer += data.toString(); }); @@ -47,12 +47,12 @@ if (os.type().indexOf('Windows') > -1) { }; // on unix operating systems, we can start abiword with abicommand and // communicate with it via stdin/stdout - // thats much faster, about factor 10 + // that's much faster, about factor 10 } else { let _abiword: ChildProcess; let stdoutCallback: Function|null = null; const spawnAbiword = () => { - _abiword = spawn(abiword, ['--plugin', 'AbiCommand']); + _abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']); let stdoutBuffer = ''; let firstPrompt = true; _abiword.stderr!.on('data', (data) => { stdoutBuffer += data.toString(); }); diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 2e8a2d960..9b54c4eaf 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -21,7 +21,7 @@ * limitations under the License. */ -const settings = require('./Settings'); +import {root, settings} from './Settings'; const fs = require('fs').promises; const path = require('path'); const plugins = require('../../static/js/pluginfw/plugin_defs'); @@ -33,7 +33,7 @@ const sanitizePathname = require('./sanitizePathname'); const logger = log4js.getLogger('Minify'); -const ROOT_DIR = path.join(settings.root, 'src/static/'); +const ROOT_DIR = path.join(root, 'src/static/'); const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2); diff --git a/src/node/utils/Settings.ts b/src/node/utils/Settings.ts index 370492b71..74defbeb8 100644 --- a/src/node/utils/Settings.ts +++ b/src/node/utils/Settings.ts @@ -35,6 +35,7 @@ import jsonminify from 'jsonminify'; import log4js from 'log4js'; import randomString from './randomstring'; import _ from 'underscore'; +import {SettingsObj} from "../types/SettingsObj"; const absolutePaths = require('./AbsolutePaths'); const argv = require('./Cli').argv; @@ -45,459 +46,425 @@ const logger = log4js.getLogger('settings'); // Exported values that settings.json and credentials.json cannot override. const nonSettings = [ - 'credentialsFilename', - 'settingsFilename', + '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) => ({appenders: {console: {type: 'console'}}, - categories: { - default: {appenders: ['console'], level}, - }}); +const defaultLogConfig = (level: string) => ({ + appenders: {console: {type: 'console'}}, + categories: { + default: {appenders: ['console'], level}, + } +}); const defaultLogLevel = 'INFO'; -const initLogging = (config:any) => { - // log4js.configure() modifies logconfig so check for equality first. - log4js.configure(config); - log4js.getLogger('console'); +const initLogging = (config: any) => { + // log4js.configure() modifies 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); + // 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); }; // 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)); + +export const root = absolutePaths.findEtherpadRoot() + +export const settings: SettingsObj = { + settingsFilename: absolutePaths.makeAbsolute(argv.settings || 'settings.json'), + credentialsFilename: absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'), + /** + * The app title, visible e.g. in the browser window + */ + title: 'Etherpad', + /** + * Pathname of the favicon you want to use. If null, the skin's favicon is + * used if one is provided by the skin, otherwise the default Etherpad favicon + * is used. If this is a relative path it is interpreted as relative to the + * Etherpad root directory. + */ + favicon: null, + + /* + * 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. + */ + skinName: null, + + skinVariants: 'super-light-toolbar super-light-editor light-background', + /** + * The IP ep-lite should listen to + */ + ip: '0.0.0.0', + /** + * The Port ep-lite should listen to + */ + port: process.env.PORT || 9001, + /** + * Should we suppress Error messages from being in Pad Contents + */ + suppressErrorsInPadText: false, + /** + * The SSL signed server key and the Certificate Authority's own certificate + * default case: ep-lite does *not* use SSL. A signed server key is not required in this case. + */ + ssl: false, + /** + * socket.io transport methods + **/ + socketTransportProtocols: ['websocket', 'polling'], + socketIo: { + /** + * Maximum permitted client message size (in bytes). + * + * All messages from clients that are larger than this will be rejected. Large values make it + * possible to paste large amounts of text, and plugins may require a larger value to work + * properly, but increasing the value increases susceptibility to denial of service attacks + * (malicious clients can exhaust memory). + */ + maxHttpBufferSize: 10000, + }, + /* + * The Type of the database + */ + dbType: 'dirty', + /** + * This setting is passed with dbType to ueberDB to set up the database + */ + dbSettings: { + filename: path.join(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'), + padOptions: { + noColors: false, + showControls: true, + showChat: true, + showLineNumbers: true, + useMonospaceFont: false, + userName: null, + userColor: null, + rtl: false, + alwaysShowChat: false, + chatAndUsers: false, + /** + * The default Pad Settings for a user (Can be overridden by changing the setting + */ + lang: null, + }, + + + /** + * Whether certain shortcut keys are enabled for a user in the pad + */ + padShortcutEnabled: { + altF9: true, + altC: true, + delete: true, + cmdShift2: true, + return: true, + esc: true, + cmdS: true, + tab: true, + cmdZ: true, + cmdY: true, + cmdB: true, + cmdI: true, + cmdU: true, + cmd5: true, + cmdShiftL: true, + cmdShiftN: true, + cmdShift1: true, + cmdShiftC: true, + cmdH: true, + ctrlHome: true, + pageUp: true, + pageDown: true, + }, + /** + * The toolbar buttons and order. + */ + toolbar: { + left: [ + ['bold', 'italic', 'underline', 'strikethrough'], + ['orderedlist', 'unorderedlist', 'indent', 'outdent'], + ['undo', 'redo'], + ['clearauthorship'], + ], + right: [ + ['importexport', 'timeslider', 'savedrevision'], + ['settings', 'embed'], + ['showusers'], + ], + timeslider: [ + ['timeslider_export', 'timeslider_settings', 'timeslider_returnToPad'], + ], + }, + /** + * A flag that requires any user to have a valid session (via the api) before accessing a pad + */ + requireSession: false, + /** + * A flag that prevents users from creating new pads + */ + editOnly: false, + /** + * Max age that responses will have (affects caching layer). + */ + maxAge: 1000 * 60 * 60 * 6, // 6 hours + /** + * A flag that shows if minification is enabled or not + */ + minify: true, + /** + * The path of the abiword executable + */ + abiword: null, + /** + * The path of the libreoffice executable + */ + soffice: null, + /** + * Should we support none natively supported file types on import? + */ + allowUnknownFileEnds: true, + /** + * The log level of log4js + */ + loglevel: defaultLogLevel, + /** + * 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, + logconfig: null, + /* + * Deprecated cookie signing key. + */ + sessionKey: null, + /* + * Trust Proxy, whether 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 + * Etherpad will be embedded in an iframe from another site, in which case + * this must be set to "None". Note: "None" will not work (the browser will + * not send the cookie to Etherpad) unless https is used to access Etherpad + * (either directly or via a reverse proxy with "trustProxy" set to true). + * + * "Strict" is not recommended because it has few security benefits but + * significant usability drawbacks vs. "Lax". See + * https://stackoverflow.com/q/41841880 for discussion. + */ + sameSite: 'Lax', + 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 + */ + requireAuthentication: false, + requireAuthorization: false, + users: {}, + /* + * Show settings in admin page, by default it is true + */ + 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, + }, + + /* + * Time (in milliseconds) used to animate the scroll transition. Set to 0 to + * disable animation + */ + duration: 0, + + /* + * Percentage of viewport height to be additionally scrolled when user presses arrow up + * in the line of the top of the viewport. + */ + percentageToScrollWhenUserPressesArrowUp: 0, + + /* + * Flag to control if it should scroll when user places the caret in the last + * line of the viewport + */ + scrollWhenCaretIsInTheLastLineOfViewport: false, + }, + /* + * 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: { + // 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 + */ + 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. + */ + 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, +} + /* Root path of the installation */ -export const root = absolutePaths.findEtherpadRoot(); logger.info('All relative paths will be interpreted relative to the identified ' + `Etherpad base dir: ${root}`); -export const settingsFilename = absolutePaths.makeAbsolute(argv.settings || 'settings.json'); -export const credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || 'credentials.json'); -/** - * The app title, visible e.g. in the browser window - */ -export const 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. - */ -export let favicon = null; - -/* - * 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. - */ -export let skinName:string|null = null; - -export const skinVariants = 'super-light-toolbar super-light-editor light-background'; - -/** - * The IP ep-lite should listen to - */ -export const ip:string = '0.0.0.0'; - -/** - * The Port ep-lite should listen to - */ -export const port = process.env.PORT || 9001; - -/** - * Should we suppress Error messages from being in Pad Contents - */ -export const 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. - */ -export const ssl:false|{ - key: string; - cert: string; - ca: string[]; -} = false; - -/** - * socket.io transport methods - **/ -export const socketTransportProtocols = ['websocket', 'polling']; - -export const socketIo = { - /** - * Maximum permitted client message size (in bytes). - * - * All messages from clients that are larger than this will be rejected. Large values make it - * possible to paste large amounts of text, and plugins may require a larger value to work - * properly, but increasing the value increases susceptibility to denial of service attacks - * (malicious clients can exhaust memory). - */ - maxHttpBufferSize: 10000, -}; - -/* - * The Type of the database - */ -export const dbType = 'dirty'; -/** - * This setting is passed with dbType to ueberDB to set up the database - */ -export const dbSettings = {filename: path.join(root, 'var/dirty.db')}; - -/** - * The default Text of a new pad - */ -export let defaultPadText = [ - 'Welcome to Etherpad!', - '', - 'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' + - 'text. This allows you to collaborate seamlessly on documents!', - '', - 'Etherpad on Github: https://github.com/ether/etherpad-lite', -].join('\n'); - -/** - * The default Pad Settings for a user (Can be overridden by changing the setting - */ -export const padOptions = { - noColors: false, - showControls: true, - showChat: true, - showLineNumbers: true, - useMonospaceFont: false, - userName: null, - userColor: null, - rtl: false, - alwaysShowChat: false, - chatAndUsers: false, - lang: null, -}; - -/** - * Whether certain shortcut keys are enabled for a user in the pad - */ -export const padShortcutEnabled = { - altF9: true, - altC: true, - delete: true, - cmdShift2: true, - return: true, - esc: true, - cmdS: true, - tab: true, - cmdZ: true, - cmdY: true, - cmdB: true, - cmdI: true, - cmdU: true, - cmd5: true, - cmdShiftL: true, - cmdShiftN: true, - cmdShift1: true, - cmdShiftC: true, - cmdH: true, - ctrlHome: true, - pageUp: true, - pageDown: true, -}; - -/** - * The toolbar buttons and order. - */ -export const toolbar = { - left: [ - ['bold', 'italic', 'underline', 'strikethrough'], - ['orderedlist', 'unorderedlist', 'indent', 'outdent'], - ['undo', 'redo'], - ['clearauthorship'], - ], - right: [ - ['importexport', 'timeslider', 'savedrevision'], - ['settings', 'embed'], - ['showusers'], - ], - timeslider: [ - ['timeslider_export', 'timeslider_settings', 'timeslider_returnToPad'], - ], -}; - -/** - * A flag that requires any user to have a valid session (via the api) before accessing a pad - */ -export const requireSession = false; - -/** - * A flag that prevents users from creating new pads - */ -export const editOnly = false; - -/** - * Max age that responses will have (affects caching layer). - */ -export const maxAge = 1000 * 60 * 60 * 6; // 6 hours - -/** - * A flag that shows if minification is enabled or not - */ -export const minify = true; - -/** - * The path of the abiword executable - */ -export let abiword = null; - -/** - * The path of the libreoffice executable - */ -export let soffice = null; - -/** - * Should we support none natively supported file types on import? - */ -export const allowUnknownFileEnds = true; - -/** - * The log level of log4js - */ -export const loglevel:string = defaultLogLevel; - -/** - * Disable IP logging - */ -export const disableIPlogging = false; - -/** - * Number of seconds to automatically reconnect pad - */ -export const automaticReconnectionTimeout = 0; - -/** - * Disable Load Testing - */ -export const loadTest = false; - -/** - * Disable dump of objects preventing a clean exit - */ -export const dumpOnUncleanExit = false; - -/** - * Enable indentation on new lines - */ -export const indentationOnNewLine = true; - -/* - * log4js appender configuration - */ -export let logconfig:any = null; - -/* - * Deprecated cookie signing key. - */ -export let sessionKey:string|null|string[] = null; - -/* - * Trust Proxy, whether trust the x-forwarded-for header. - */ -export const trustProxy = false; - -/* - * Settings controlling the session cookie issued by Etherpad. - */ -export const cookie = { - keyRotationInterval: 1 * 24 * 60 * 60 * 1000, - /* - * Value of the SameSite cookie property. "Lax" is recommended unless - * Etherpad will be embedded in an iframe from another site, in which case - * this must be set to "None". Note: "None" will not work (the browser will - * not send the cookie to Etherpad) unless https is used to access Etherpad - * (either directly or via a reverse proxy with "trustProxy" set to true). - * - * "Strict" is not recommended because it has few security benefits but - * significant usability drawbacks vs. "Lax". See - * https://stackoverflow.com/q/41841880 for discussion. - */ - sameSite: 'Lax', - 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 - */ -export let requireAuthentication = false; -export let requireAuthorization = false; -export const users = {}; - -/* - * Show settings in admin page, by default it is true - */ -export const showSettingsInAdminPage = true; - -/* - * By default, when caret is moved out of viewport, it scrolls the minimum - * height needed to make this line visible. - */ -export const scrollWhenFocusLineIsOutOfViewport = { - /* - * Percentage of viewport height to be additionally scrolled. - */ - percentage: { - editionAboveViewport: 0, - editionBelowViewport: 0, - }, - - /* - * Time (in milliseconds) used to animate the scroll transition. Set to 0 to - * disable animation - */ - duration: 0, - - /* - * Percentage of viewport height to be additionally scrolled when user presses arrow up - * in the line of the top of the viewport. - */ - percentageToScrollWhenUserPressesArrowUp: 0, - - /* - * Flag to control if it should scroll when user places the caret in the last - * line of the viewport - */ - scrollWhenCaretIsInTheLastLineOfViewport: false, -}; - -/* - * Expose Etherpad version in the web interface and in the Server http header. - * - * Do not enable on production machines. - */ -export const exposeVersion = false; - -/* - * Override any strings found in locale directories - */ -export const 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 - */ -export const importExportRateLimiting = { - // duration of the rate limit window (milliseconds) - windowMs: 90000, - - // maximum number of requests per IP to allow during the rate limit window - max: 10, -}; - -/* - * From Etherpad 1.9.0 onwards, commits from individual users are rate limited - * - * The default is to allow at most 10 changes per IP in a 1 second window. - * 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 - */ -export const 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. - */ -export const importMaxFileSize = 50 * 1024 * 1024; - -/* - * Disable Admin UI tests - */ -export const enableAdminUITests = false; - -/* - * Enable auto conversion of pad Ids to lowercase. - * e.g. /p/EtHeRpAd to /p/etherpad - */ -export const lowerCasePadIds = false; // checks if abiword is avaiable export const abiwordAvailable = () => { - if (abiword != null) { - return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; - } else { - return 'no'; - } + if (settings.abiword != null) { + return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; + } else { + return 'no'; + } }; export const sofficeAvailable = () => { - if (soffice != null) { - return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; - } else { - return 'no'; - } + if (settings.soffice != null) { + return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; + } else { + return 'no'; + } }; export const exportAvailable = () => { - const abiword = abiwordAvailable(); - const soffice = sofficeAvailable(); + const abiword = abiwordAvailable(); + const soffice = sofficeAvailable(); - if (abiword === 'no' && soffice === 'no') { - return 'no'; - } else if ((abiword === 'withoutPDF' && soffice === 'no') || - (abiword === 'no' && soffice === 'withoutPDF')) { - return 'withoutPDF'; - } else { - return 'yes'; - } + if (abiword === 'no' && soffice === 'no') { + return 'no'; + } else if ((abiword === 'withoutPDF' && soffice === 'no') || + (abiword === 'no' && soffice === 'withoutPDF')) { + return 'withoutPDF'; + } else { + return 'yes'; + } }; // Provide git version if available export const getGitCommit = () => { - let version = ''; - try { - let rootPath = root; - if (fs.lstatSync(`${rootPath}/.git`).isFile()) { - rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); - rootPath = rootPath.split(' ').pop().trim(); - } else { - rootPath += '/.git'; + let version = ''; + try { + let rootPath = 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}`); } - const ref = fs.readFileSync(`${rootPath}/HEAD`, 'utf-8'); - if (ref.startsWith('ref: ')) { - const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`; - version = fs.readFileSync(refPath, 'utf-8'); - } else { - version = ref; - } - version = version.substring(0, 7); - } catch (e: any) { - logger.warn(`Can't get git version for server header\n${e.message}`); - } - return version; + return version; }; // Return etherpad version from package.json @@ -510,33 +477,34 @@ export const getEpVersion = () => require('../../package.json').version; * This code refactors a previous version that copied & pasted the same code for * both "settings.json" and "credentials.json". */ -const storeSettings = (settingsObj:MapArrayType) => { - for (const i of Object.keys(settingsObj || {})) { - if (nonSettings.includes(i)) { - logger.warn(`Ignoring setting: '${i}'`); - continue; - } +const storeSettings = (settingsObj: MapArrayType) => { + 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}'`); - } + // 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}'`); + } - console.log(globalThis) - - // 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]; - } - } 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`); + // we know this setting, so we overwrite it, + // or it's a settings hash, specific to a plugin + // @ts-ignore + if (settings[i] !== undefined || i.indexOf('ep_') === 0) { + if (_.isObject(settingsObj[i]) && !Array.isArray(settingsObj[i])) { + // @ts-ignore + settings[i] = _.defaults(settingsObj[i], settings[i]); + } else { + // @ts-ignore + settings[i] = settingsObj[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`); + } } - } }; /* @@ -552,23 +520,28 @@ const storeSettings = (settingsObj:MapArrayType) => { * in the literal string "null", instead. */ const 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)); + // 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 + if (isNumeric) { + // 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; - } + switch (stringValue) { + case 'true': + return true; + case 'false': + return false; + case 'undefined': + return undefined; + case 'null': + return null; + default: + return stringValue; + } }; /** @@ -608,81 +581,81 @@ const coerceValue = (stringValue: string) => { * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter */ const lookupEnvironmentVariables = (obj: object) => { - const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => { - /* - * the first invocation of replacer() is with an empty key. Just go on, or - * we would zap the entire object. - */ - if (key === '') { - return value; - } + const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => { + /* + * the first invocation of replacer() is with an empty key. Just go on, or + * we would zap the entire object. + */ + if (key === '') { + return value; + } - /* - * 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 (typeof value !== 'string') { - return value; - } + /* + * 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 (typeof value !== 'string') { + return value; + } - /* - * 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)*))?\}$/); + /* + * 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 + if (match == null) { + // no match: use the value literally, without any substitution - return value; - } + return value; + } - /* - * 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]; + /* + * 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.'); + 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. - */ - return null; - } + /* + * We have to return null, because if we just returned undefined, the + * configuration item "key" would be stripped from the returned object. + */ + return null; + } - if ((envVarValue === undefined) && (defaultValue !== undefined)) { - logger.debug(`Environment variable "${envVarName}" not found for ` + - `configuration key "${key}". Falling back to default value.`); + if ((envVarValue === undefined) && (defaultValue !== undefined)) { + logger.debug(`Environment variable "${envVarName}" not found for ` + + `configuration key "${key}". Falling back to default value.`); - return coerceValue(defaultValue); - } + return coerceValue(defaultValue); + } - // envVarName contained some value. + // 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}"`); + /* + * 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}"`); - return coerceValue(envVarValue!); - }); + return coerceValue(envVarValue!); + }); - return JSON.parse(stringifiedAndReplaced); + return JSON.parse(stringifiedAndReplaced); }; /** @@ -693,187 +666,188 @@ const lookupEnvironmentVariables = (obj: object) => { * * The isSettings variable only controls the error logging. */ -const parseSettings = (settingsFilename: string, isSettings: boolean) => { - let settingsStr = ''; +export const parseSettings = (settingsFilename: string, isSettings: boolean) => { + let settingsStr = ''; - let settingsType, notFoundMessage, notFoundFunction; + let settingsType, notFoundMessage, notFoundFunction; - if (isSettings) { - settingsType = 'settings'; - notFoundMessage = 'Continuing using defaults!'; - notFoundFunction = logger.warn.bind(logger); - } else { - settingsType = 'credentials'; - notFoundMessage = 'Ignoring.'; - notFoundFunction = logger.info.bind(logger); - } + if (isSettings) { + settingsType = 'settings'; + notFoundMessage = 'Continuing using defaults!'; + notFoundFunction = logger.warn.bind(logger); + } else { + settingsType = 'credentials'; + notFoundMessage = 'Ignoring.'; + notFoundFunction = logger.info.bind(logger); + } - try { - // read the settings file - settingsStr = fs.readFileSync(settingsFilename).toString(); - } catch (e) { - notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); + try { + // read the settings file + settingsStr = fs.readFileSync(settingsFilename).toString(); + } catch (e) { + notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); - // or maybe undefined! - return null; - } + // or maybe undefined! + return null; + } - try { - settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); + try { + settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}'); - const settings = JSON.parse(settingsStr); + const settings = JSON.parse(settingsStr); - logger.info(`${settingsType} loaded from: ${settingsFilename}`); + logger.info(`${settingsType} loaded from: ${settingsFilename}`); - return lookupEnvironmentVariables(settings); - } catch (e:any) { - logger.error(`There was an error processing your ${settingsType} ` + - `file from ${settingsFilename}: ${e.message}`); + return lookupEnvironmentVariables(settings); + } catch (e: any) { + logger.error(`There was an error processing your ${settingsType} ` + + `file from ${settingsFilename}: ${e.message}`); - process.exit(1); - } + process.exit(1); + } }; export let randomVersionString: string | undefined export const reloadSettings = () => { - const settings = parseSettings(settingsFilename, true); - const credentials = parseSettings(credentialsFilename, false); - storeSettings(settings); - storeSettings(credentials); + const settingsParsed = parseSettings(settings.settingsFilename, true); + const credentials = parseSettings(settings.credentialsFilename, false); + storeSettings(settingsParsed); + storeSettings(credentials); - // Init logging config - logconfig = defaultLogConfig(loglevel ? loglevel : defaultLogLevel); - initLogging(logconfig); + // Init logging config + settings.logconfig = defaultLogConfig(settings.loglevel ? settings.loglevel : defaultLogLevel); + initLogging(settings.logconfig); - if (!skinName) { - logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + - 'update your settings.json. Falling back to the default "colibris".'); - skinName = 'colibris'; - } - - // checks if skinName has an acceptable value, otherwise falls back to "colibris" - if (skinName) { - const skinBasePath = path.join(root, 'src', 'static', 'skins'); - const countPieces = skinName.split(path.sep).length; - - if (countPieces !== 1) { - logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + - `not valid: "${skinName}". Falling back to the default "colibris".`); - - skinName = 'colibris'; + if (!settings.skinName) { + logger.warn('No "skinName" parameter found. Please check out settings.json.template and ' + + 'update your settings.json. Falling back to the default "colibris".'); + settings.skinName = 'colibris'; } - // informative variable, just for the log messages - let skinPath = path.join(skinBasePath, skinName); + // checks if skinName has an acceptable value, otherwise falls back to "colibris" + if (settings.skinName) { + const skinBasePath = path.join(root, 'src', 'static', 'skins'); + const countPieces = settings.skinName.split(path.sep).length; - // 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".'); + if (countPieces !== 1) { + logger.error(`skinName must be the name of a directory under "${skinBasePath}". This is ` + + `not valid: "${settings.skinName}". Falling back to the default "colibris".`); - skinName = 'colibris'; - skinPath = path.join(skinBasePath, skinName); - } - - if (!fs.existsSync(skinPath)) { - logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); - skinName = 'colibris'; - skinPath = path.join(skinBasePath, skinName); - } - - logger.info(`Using skin "${skinName}" in dir: ${skinPath}`); - } - - if (abiword) { - // Check abiword actually exists - if (abiword != null) { - fs.exists(abiword, (exists) => { - if (!exists) { - const abiwordError = 'Abiword does not exist at this path, check your settings file.'; - if (!suppressErrorsInPadText) { - defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; - } - logger.error(`${abiwordError} File location: ${abiword}`); - abiword = null; + settings.skinName = 'colibris'; } - }); - } - } - if (soffice) { - fs.exists(soffice, (exists) => { - if (!exists) { - const sofficeError = - 'soffice (libreoffice) does not exist at this path, check your settings file.'; + // informative variable, just for the log messages + let skinPath = path.join(skinBasePath, settings.skinName); - if (!suppressErrorsInPadText) { - defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; + // 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".'); + + settings.skinName = 'colibris'; + skinPath = path.join(skinBasePath, settings.skinName); } - logger.error(`${sofficeError} File location: ${soffice}`); - soffice = null; - } - }); - } - const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); - if (!sessionKey) { - try { - sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); - logger.info(`Session key loaded from: ${sessionkeyFilename}`); - } catch (err) { /* ignored */ } - const keyRotationEnabled = cookie.keyRotationInterval && cookie.sessionLifetime; - if (!sessionKey && !keyRotationEnabled) { - logger.info( - `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); - sessionKey = randomString(32); - fs.writeFileSync(sessionkeyFilename, 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.'); - } - if (sessionKey) { - logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + - 'use automatic key rotation instead (see the cookie.keyRotationInterval setting).'); - } + if (!fs.existsSync(skinPath)) { + logger.error(`Skin path ${skinPath} does not exist. Falling back to the default "colibris".`); + settings.skinName = 'colibris'; + skinPath = path.join(skinBasePath, settings.skinName); + } - if (dbType === 'dirty') { - const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; - if (!suppressErrorsInPadText) { - defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; + logger.info(`Using skin "${settings.skinName}" in dir: ${skinPath}`); } - dbSettings.filename = absolutePaths.makeAbsolute(dbSettings.filename); - logger.warn(`${dirtyWarning} File location: ${dbSettings.filename}`); - } + if (settings.abiword) { + // Check abiword actually exists + if (settings.abiword != null) { + fs.exists(settings.abiword, (exists) => { + if (!exists) { + const abiwordError = 'Abiword does not exist at this path, check your settings file.'; + if (!settings.suppressErrorsInPadText) { + settings.defaultPadText += `\nError: ${abiwordError}${suppressDisableMsg}`; + } + logger.error(`${abiwordError} File location: ${settings.abiword}`); + settings.abiword = null; + } + }); + } + } - if (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 (settings.soffice) { + fs.exists(settings.soffice, (exists) => { + if (!exists) { + const sofficeError = + 'soffice (libreoffice) does not exist at this path, check your settings file.'; - /* - * At each start, Etherpad generates a random string and appends it as query - * parameter to the URLs of the static assets, in order to force their reload. - * Subsequent requests will be cached, as long as the server is not reloaded. - * - * For the rationale behind this choice, see - * https://github.com/ether/etherpad-lite/pull/3958 - * - * ACHTUNG: this may prevent caching HTTP proxies to work - * TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead - */ - randomVersionString = randomString(4); - logger.info(`Random string used for versioning assets: ${randomVersionString}`); + if (!settings.suppressErrorsInPadText) { + settings.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; + } + logger.error(`${sofficeError} File location: ${settings.soffice}`); + settings.soffice = null; + } + }); + } + + const sessionkeyFilename = absolutePaths.makeAbsolute(argv.sessionkey || './SESSIONKEY.txt'); + if (!settings.sessionKey) { + try { + settings.sessionKey = fs.readFileSync(sessionkeyFilename, 'utf8'); + logger.info(`Session key loaded from: ${sessionkeyFilename}`); + } catch (err) { /* ignored */ + } + const keyRotationEnabled = settings.cookie.keyRotationInterval && settings.cookie.sessionLifetime; + if (!settings.sessionKey && !keyRotationEnabled) { + logger.info( + `Session key file "${sessionkeyFilename}" not found. Creating with random contents.`); + settings.sessionKey = randomString(32); + fs.writeFileSync(sessionkeyFilename, settings.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.'); + } + if (settings.sessionKey) { + logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + + 'use automatic key rotation instead (see the cookie.keyRotationInterval setting).'); + } + + if (settings.dbType === 'dirty') { + const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; + if (!settings.suppressErrorsInPadText) { + settings.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; + } + + settings.dbSettings.filename = absolutePaths.makeAbsolute(settings.dbSettings.filename); + logger.warn(`${dirtyWarning} File location: ${settings.dbSettings.filename}`); + } + + if (settings.ip === '') { + // using Unix socket for connectivity + logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' + + '"port" parameter will be interpreted as the path to a Unix socket to bind at.'); + } + + /* + * At each start, Etherpad generates a random string and appends it as query + * parameter to the URLs of the static assets, in order to force their reload. + * Subsequent requests will be cached, as long as the server is not reloaded. + * + * For the rationale behind this choice, see + * https://github.com/ether/etherpad-lite/pull/3958 + * + * ACHTUNG: this may prevent caching HTTP proxies to work + * TODO: remove the "?v=randomstring" parameter, and replace with hashed filenames instead + */ + randomVersionString = randomString(4); + logger.info(`Random string used for versioning assets: ${randomVersionString}`); }; export const exportedForTestingOnly = { - parseSettings, + parseSettings, }; // initially load settings diff --git a/src/node/utils/UpdateCheck.ts b/src/node/utils/UpdateCheck.ts index 193a40a98..2e7816b67 100644 --- a/src/node/utils/UpdateCheck.ts +++ b/src/node/utils/UpdateCheck.ts @@ -1,9 +1,9 @@ 'use strict'; const semver = require('semver'); -const settings = require('./Settings'); +import {getEpVersion, settings} from './Settings'; const axios = require('axios'); const headers = { - 'User-Agent': 'Etherpad/' + settings.getEpVersion(), + 'User-Agent': 'Etherpad/' + getEpVersion(), } type Infos = { @@ -45,7 +45,7 @@ exports.getLatestVersion = () => { exports.needsUpdate = async (cb: Function) => { await loadEtherpadInformations() .then((info:Infos) => { - if (semver.gt(info.latestVersion, settings.getEpVersion())) { + if (semver.gt(info.latestVersion, getEpVersion())) { if (cb) return cb(true); } }).catch((err: Error) => { diff --git a/src/node/utils/caching_middleware.ts b/src/node/utils/caching_middleware.ts index d5866b019..199f03e5f 100644 --- a/src/node/utils/caching_middleware.ts +++ b/src/node/utils/caching_middleware.ts @@ -21,7 +21,7 @@ const fs = require('fs'); const fsp = fs.promises; const path = require('path'); const zlib = require('zlib'); -const settings = require('./Settings'); +import {root} from './Settings'; const existsSync = require('./path_exists'); const util = require('util'); @@ -40,7 +40,7 @@ const util = require('util'); const _crypto = require('crypto'); -let CACHE_DIR = path.join(settings.root, 'var/'); +let CACHE_DIR = path.join(root, 'var/'); CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined; type Headers = { diff --git a/src/node/utils/run_cmd.ts b/src/node/utils/run_cmd.ts index 463b0f076..80dc84b72 100644 --- a/src/node/utils/run_cmd.ts +++ b/src/node/utils/run_cmd.ts @@ -5,11 +5,11 @@ import {ChildProcess} from "node:child_process"; import {PromiseWithStd} from "../types/PromiseWithStd"; import {Readable} from "node:stream"; +import {root, settings} from "./Settings"; + const spawn = require('cross-spawn'); const log4js = require('log4js'); const path = require('path'); -const settings = require('./Settings'); - const logger = log4js.getLogger('runCmd'); const logLines = (readable: undefined | Readable | null, logLineFn: (arg0: (string | undefined)) => void) => { @@ -77,7 +77,7 @@ const logLines = (readable: undefined | Readable | null, logLineFn: (arg0: (stri module.exports = exports = (args: string[], opts:RunCMDOptions = {}) => { logger.debug(`Executing command: ${args.join(' ')}`); - opts = {cwd: settings.root, ...opts}; + opts = {cwd: root, ...opts}; logger.debug(`cwd: ${opts.cwd}`); // Log stdout and stderr by default. @@ -112,8 +112,8 @@ module.exports = exports = (args: string[], opts:RunCMDOptions = {}) => { opts.env = { ...env, // Copy env to avoid modifying process.env or the caller's supplied env. [pathVarName]: [ - path.join(settings.root, 'src', 'node_modules', '.bin'), - path.join(settings.root, 'node_modules', '.bin'), + path.join(root, 'src', 'node_modules', '.bin'), + path.join(root, 'node_modules', '.bin'), ...(PATH ? PATH.split(path.delimiter) : []), ].join(path.delimiter), }; diff --git a/src/package.json b/src/package.json index bb00ee08a..c275bb3dd 100644 --- a/src/package.json +++ b/src/package.json @@ -81,6 +81,7 @@ "devDependencies": { "@types/async": "^3.2.24", "@types/express": "^4.17.21", + "@types/formidable": "^3.4.5", "@types/jsonminify": "^0.4.3", "@types/node": "^20.11.19", "@types/underscore": "^1.11.15", diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index ed7b328e3..9dd22d89e 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -4,7 +4,7 @@ const log4js = require('log4js'); const plugins = require('./plugins'); const hooks = require('./hooks'); const runCmd = require('../../../node/utils/run_cmd'); -const settings = require('../../../node/utils/Settings'); +import {getEpVersion, reloadSettings, root, settings} from '../../../node/utils/Settings'; const axios = require('axios'); const {PluginManager} = require('live-plugin-manager-pnpm'); const {promises: fs} = require('fs'); @@ -14,18 +14,18 @@ const logger = log4js.getLogger('plugins'); exports.manager = new PluginManager(); -const installedPluginsPath = path.join(settings.root, 'var/installed_plugins.json'); +const installedPluginsPath = path.join(root, 'var/installed_plugins.json'); const onAllTasksFinished = async () => { await plugins.update(); await persistInstalledPlugins(); - settings.reloadSettings(); + reloadSettings(); await hooks.aCallAll('loadSettings', {settings}); await hooks.aCallAll('restartServer'); }; const headers = { - 'User-Agent': `Etherpad/${settings.getEpVersion()}`, + 'User-Agent': `Etherpad/${getEpVersion()}`, }; let tasks = 0; diff --git a/src/tests/backend/common.js b/src/tests/backend/common.js index 8172e1e87..54637db01 100644 --- a/src/tests/backend/common.js +++ b/src/tests/backend/common.js @@ -9,7 +9,7 @@ const {padutils} = require('../../static/js/pad_utils'); const process = require('process'); const server = require('../../node/server'); const setCookieParser = require('set-cookie-parser'); -const settings = require('../../node/utils/Settings'); +import {settings} from '../../node/utils/Settings'; const supertest = require('supertest'); const webaccess = require('../../node/hooks/express/webaccess'); diff --git a/src/tests/backend/specs/Pad.js b/src/tests/backend/specs/Pad.js index a5e01b655..7e2da8a86 100644 --- a/src/tests/backend/specs/Pad.js +++ b/src/tests/backend/specs/Pad.js @@ -6,7 +6,7 @@ const authorManager = require('../../../node/db/AuthorManager'); const common = require('../common'); const padManager = require('../../../node/db/PadManager'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); -import * as settings from '../../../node/utils/Settings'; +import {settings} from '../../../node/utils/Settings'; describe(__filename, function () { const backups = {}; diff --git a/src/tests/backend/specs/api/importexportGetPost.js b/src/tests/backend/specs/api/importexportGetPost.js index 4d21cbe38..968c71316 100644 --- a/src/tests/backend/specs/api/importexportGetPost.js +++ b/src/tests/backend/specs/api/importexportGetPost.js @@ -7,7 +7,7 @@ const assert = require('assert').strict; const common = require('../../common'); const fs = require('fs'); -import * as settings from '../../../../node/utils/Settings'; +import {settings} from '../../../../node/utils/Settings'; const superagent = require('superagent'); const padManager = require('../../../../node/db/PadManager'); const plugins = require('../../../../static/js/pluginfw/plugin_defs'); diff --git a/src/tests/backend/specs/caching_middleware.js b/src/tests/backend/specs/caching_middleware.js index c5ef605fc..9c175a9ec 100644 --- a/src/tests/backend/specs/caching_middleware.js +++ b/src/tests/backend/specs/caching_middleware.js @@ -9,7 +9,7 @@ const common = require('../common'); const assert = require('../assert-legacy').strict; const queryString = require('querystring'); -import * as settings from '../../../node/utils/Settings'; +import {settings} from '../../../node/utils/Settings'; let agent; diff --git a/src/tests/backend/specs/export.js b/src/tests/backend/specs/export.js index e202eab6d..a89eb807f 100644 --- a/src/tests/backend/specs/export.js +++ b/src/tests/backend/specs/export.js @@ -2,7 +2,7 @@ const common = require('../common'); const padManager = require('../../../node/db/PadManager'); -import * as settings from '../../../node/utils/Settings'; +import {settings} from '../../../node/utils/Settings'; describe(__filename, function () { let agent; diff --git a/src/tests/backend/specs/favicon.js b/src/tests/backend/specs/favicon.js index 0a2594a28..4823c35c0 100644 --- a/src/tests/backend/specs/favicon.js +++ b/src/tests/backend/specs/favicon.js @@ -5,7 +5,7 @@ const common = require('../common'); const fs = require('fs'); const fsp = fs.promises; const path = require('path'); -import * as settings from '../../../node/utils/Settings' +import {settings} from '../../../node/utils/Settings' const superagent = require('superagent'); describe(__filename, function () { diff --git a/src/tests/backend/specs/lowerCasePadIds.js b/src/tests/backend/specs/lowerCasePadIds.js index 489b0eda9..ccc83512d 100644 --- a/src/tests/backend/specs/lowerCasePadIds.js +++ b/src/tests/backend/specs/lowerCasePadIds.js @@ -3,7 +3,7 @@ const assert = require('assert').strict; const common = require('../common'); const padManager = require('../../../node/db/PadManager'); -const settings = require('../../../node/utils/Settings'); +import {settings} from '../../../node/utils/Settings'; describe(__filename, function () { let agent; diff --git a/src/tests/backend/specs/settings.js b/src/tests/backend/specs/settings.js index e737f4f34..5a00459e4 100644 --- a/src/tests/backend/specs/settings.js +++ b/src/tests/backend/specs/settings.js @@ -1,7 +1,7 @@ 'use strict'; const assert = require('assert').strict; -const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly; +import {parseSettings} from '../../../node/utils/Settings'; const path = require('path'); const process = require('process'); diff --git a/src/tests/backend/specs/socketio.js b/src/tests/backend/specs/socketio.js index 8a65213f8..7c9503000 100644 --- a/src/tests/backend/specs/socketio.js +++ b/src/tests/backend/specs/socketio.js @@ -5,7 +5,7 @@ const common = require('../common'); const padManager = require('../../../node/db/PadManager'); const plugins = require('../../../static/js/pluginfw/plugin_defs'); const readOnlyManager = require('../../../node/db/ReadOnlyManager'); -const settings = require('../../../node/utils/Settings'); +import {settings} from '../../../node/utils/Settings'; const socketIoRouter = require('../../../node/handler/SocketIORouter'); describe(__filename, function () {