diff --git a/src/node/utils/Settings.ts b/src/node/utils/Settings.ts index ebc564944..78e592826 100644 --- a/src/node/utils/Settings.ts +++ b/src/node/utils/Settings.ts @@ -29,6 +29,7 @@ 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'); @@ -47,28 +48,30 @@ 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 exports.logconfig so check for equality first. - log4js.configure(config); - log4js.getLogger('console'); +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); + // 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 @@ -132,15 +135,15 @@ exports.ssl = false; exports.socketTransportProtocols = ['websocket', 'polling']; exports.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, + /** + * 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, }; /* @@ -156,77 +159,77 @@ exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')}; * The default Text of a new pad */ exports.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', + 'Welcome to Etherpad!', + '', + 'This pad text is synchronized as you type, so that everyone viewing this page sees the same ' + + 'text. This allows you to collaborate seamlessly on documents!', + '', + 'Etherpad on Github: https://github.com/ether/etherpad-lite', ].join('\n'); /** * The default Pad Settings for a user (Can be overridden by changing the setting */ exports.padOptions = { - noColors: false, - showControls: true, - showChat: true, - showLineNumbers: true, - useMonospaceFont: false, - userName: null, - userColor: null, - rtl: false, - alwaysShowChat: false, - chatAndUsers: false, - lang: null, + 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 */ exports.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, + 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. */ exports.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'], - ], + 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'], + ], }; /** @@ -313,21 +316,21 @@ exports.trustProxy = false; * Settings controlling the session cookie issued by Etherpad. */ exports.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, + 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, }; /* @@ -349,31 +352,31 @@ exports.showSettingsInAdminPage = true; * height needed to make this line visible. */ exports.scrollWhenFocusLineIsOutOfViewport = { - /* - * Percentage of viewport height to be additionally scrolled. - */ - percentage: { - editionAboveViewport: 0, - editionBelowViewport: 0, - }, + /* + * 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, + /* + * 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, + /* + * 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, + /* + * Flag to control if it should scroll when user places the caret in the last + * line of the viewport + */ + scrollWhenCaretIsInTheLastLineOfViewport: false, }; /* @@ -398,11 +401,11 @@ exports.customLocaleStrings = {}; * See https://github.com/nfriedly/express-rate-limit for more options */ exports.importExportRateLimiting = { - // duration of the rate limit window (milliseconds) - windowMs: 90000, + // duration of the rate limit window (milliseconds) + windowMs: 90000, - // maximum number of requests per IP to allow during the rate limit window - max: 10, + // maximum number of requests per IP to allow during the rate limit window + max: 10, }; /* @@ -414,11 +417,11 @@ exports.importExportRateLimiting = { * See https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options */ exports.commitRateLimiting = { - // duration of the rate limit window (seconds) - duration: 1, + // duration of the rate limit window (seconds) + duration: 1, - // maximum number of chanes per IP to allow during the rate limit window - points: 10, + // maximum number of chanes per IP to allow during the rate limit window + points: 10, }; /* @@ -442,58 +445,58 @@ exports.lowerCasePadIds = false; // checks if abiword is avaiable exports.abiwordAvailable = () => { - if (exports.abiword != null) { - return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; - } else { - return 'no'; - } + if (exports.abiword != null) { + return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; + } else { + return 'no'; + } }; exports.sofficeAvailable = () => { - if (exports.soffice != null) { - return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; - } else { - return 'no'; - } + if (exports.soffice != null) { + return os.type().indexOf('Windows') !== -1 ? 'withoutPDF' : 'yes'; + } else { + return 'no'; + } }; exports.exportAvailable = () => { - const abiword = exports.abiwordAvailable(); - const soffice = exports.sofficeAvailable(); + const abiword = exports.abiwordAvailable(); + const soffice = exports.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 exports.getGitCommit = () => { - let version = ''; - try { - let rootPath = exports.root; - if (fs.lstatSync(`${rootPath}/.git`).isFile()) { - rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); - rootPath = rootPath.split(' ').pop().trim(); - } else { - rootPath += '/.git'; + let version = ''; + try { + let rootPath = exports.root; + if (fs.lstatSync(`${rootPath}/.git`).isFile()) { + rootPath = fs.readFileSync(`${rootPath}/.git`, 'utf8'); + rootPath = rootPath.split(' ').pop().trim(); + } else { + rootPath += '/.git'; + } + const ref = fs.readFileSync(`${rootPath}/HEAD`, 'utf-8'); + if (ref.startsWith('ref: ')) { + const refPath = `${rootPath}/${ref.substring(5, ref.indexOf('\n'))}`; + version = fs.readFileSync(refPath, 'utf-8'); + } else { + version = ref; + } + version = version.substring(0, 7); + } catch (e: any) { + logger.warn(`Can't get git version for server header\n${e.message}`); } - 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 @@ -506,31 +509,31 @@ exports.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:any) => { - for (const i of Object.keys(settingsObj || {})) { - if (nonSettings.includes(i)) { - logger.warn(`Ignoring setting: '${i}'`); - continue; - } +const 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}'`); - } + // 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]; - } - } 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 + 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`); + } } - } }; /* @@ -545,24 +548,29 @@ 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) => { - // 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)); +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)); - 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; + } }; /** @@ -602,134 +610,125 @@ 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: 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 - } + 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 ((typeof value !== 'string' && typeof value !== 'object') || value === null) { - 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 ((typeof value !== 'string' && typeof value !== 'object') || value === null) { + obj[key] = value; + continue + } - if (typeof obj[key] === "object") { - replaceEnvs(obj[key]); - 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)*))?\}$/); + /* + * 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 - } + 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]; + /* + * 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. - */ - obj[key] = null; - continue - } + /* + * 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.`); + 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 - } + obj[key] = coerceValue(defaultValue); + continue + } - // 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}"`); - obj[key] = coerceValue(envVarValue!); + obj[key] = coerceValue(envVarValue!); + } + return obj } - return obj - } - replaceEnvs(obj); + replaceEnvs(obj); - const envVars:MapArrayType = {} + const envVars: MapArrayType = {} - // Add plugin ENV variables + // Add plugin ENV variables - /** - * If the key contains a double underscore, it's a plugin variable - * E.g. - */ - let treeEntries = new Map - const root = new SettingsNode("EP") + /** + * If the key contains a double underscore, it's a plugin variable + * E.g. + */ + let treeEntries = new Map + const root = new SettingsNode("EP") - for (let [env,envVal] of Object.entries(process.env)) { - if(!env.startsWith("EP")) continue + for (let [env, envVal] of Object.entries(process.env)) { + if (!env.startsWith("EP")) continue treeEntries.set(env, envVal) - } + } - treeEntries.forEach((value, key) => { + treeEntries.forEach((value, key) => { let pathToKey = key.split("__") let currentNode = root let depth = 0 - depth++ - currentNode.addChild(pathToKey, value!) - }) + depth++ + currentNode.addChild(pathToKey, value!) + }) - console.log("Root is", JSON.stringify(root, (k,v)=>{ - if(v instanceof Map) { - return { - dataType: 'Map', - value: Array.from(v.entries()), // or with spread: value: [...value] - }; - } else { - return v; - } - })) - - obj = Object.assign(obj, envVars) - return obj; + console.log(root.collectFromLeafsUpwards()) + obj = Object.assign(obj, envVars) + return obj; }; + /** * - reads the JSON configuration file settingsFilename from disk * - strips the comments @@ -738,184 +737,185 @@ const lookupEnvironmentVariables = (obj: MapArrayType) => { * * The isSettings variable only controls the error logging. */ -const parseSettings = (settingsFilename:string, isSettings:boolean) => { - let settingsStr = ''; +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); + } }; exports.reloadSettings = () => { - const settings = parseSettings(exports.settingsFilename, true); - const credentials = parseSettings(exports.credentialsFilename, false); - storeSettings(settings); - storeSettings(credentials); + const settings = parseSettings(exports.settingsFilename, true); + const credentials = parseSettings(exports.credentialsFilename, false); + storeSettings(settings); + storeSettings(credentials); - // Init logging config - exports.logconfig = defaultLogConfig(exports.loglevel ? exports.loglevel : defaultLogLevel); - initLogging(exports.logconfig); + // Init logging config + exports.logconfig = defaultLogConfig(exports.loglevel ? exports.loglevel : defaultLogLevel); + 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'; - } - - // 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'; + 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'; } - // informative variable, just for the log messages - let skinPath = path.join(skinBasePath, exports.skinName); + // 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; - // 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: "${exports.skinName}". 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 (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; + 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 (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.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 (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; + } + }); + } + + 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'); + } + } 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 (exports.sessionKey) { + logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + + 'use automatic key rotation instead (see the cookie.keyRotationInterval setting).'); + } + + if (exports.dbType === 'dirty') { + const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nError: ${sofficeError}${suppressDisableMsg}`; + exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; } - logger.error(`${sofficeError} File location: ${exports.soffice}`); - exports.soffice = null; - } - }); - } - 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'); - } - } 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 (exports.sessionKey) { - logger.warn(`The sessionKey setting and ${sessionkeyFilename} file are deprecated; ` + - 'use automatic key rotation instead (see the cookie.keyRotationInterval setting).'); - } - - if (exports.dbType === 'dirty') { - const dirtyWarning = 'DirtyDB is used. This is not recommended for production.'; - if (!exports.suppressErrorsInPadText) { - exports.defaultPadText += `\nWarning: ${dirtyWarning}${suppressDisableMsg}`; + exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); + logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`); } - exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename); - logger.warn(`${dirtyWarning} File location: ${exports.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 (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.'); - } - - /* - * 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 - */ - exports.randomVersionString = randomString(4); - logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); + /* + * 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 + */ + exports.randomVersionString = randomString(4); + logger.info(`Random string used for versioning assets: ${exports.randomVersionString}`); }; exports.exportedForTestingOnly = { - parseSettings, + parseSettings, }; // initially load settings diff --git a/src/node/utils/SettingsTree.ts b/src/node/utils/SettingsTree.ts index b5b533e5d..c505f2ebb 100644 --- a/src/node/utils/SettingsTree.ts +++ b/src/node/utils/SettingsTree.ts @@ -1,3 +1,5 @@ +import {MapArrayType} from "../types/MapType"; + export class SettingsTree { private children: Map; constructor() { @@ -24,46 +26,87 @@ export class SettingsTree { export class SettingsNode { private readonly key: string; - private value: string|undefined; - private children: Map; + private value: string | number | boolean | null | undefined; + private children: MapArrayType; - constructor(key: string, value?: string) { + constructor(key: string, value?: string | number | boolean | null | undefined) { this.key = key; this.value = value; - this.children = new Map(); + this.children = {} } - public addChild(key: string[], value?: string) { - let depth = 0 - - while (depth < key.length) { - const k = key[depth]; - const slicedKey = key.slice(depth + 1) - depth++; - if(this.key === k) { - console.log("same key") + public addChild(path: string[], value: string) { + let currentNode:SettingsNode = this; + for (let i = 0; i < path.length; i++) { + const key = path[i]; + /* + Skip the current node if the key is the same as the current node's key + */ + if (key === this.key ) { continue } - if (this.children.has(k)) { - console.log("has child", k) - this.children.get(k)!.addChild(slicedKey, value); + /* + If the current node does not have a child with the key, create a new node with the key + */ + if (!currentNode.hasChild(key)) { + currentNode = currentNode.children[key] = new SettingsNode(key, this.coerceValue(value)); } else { - const newNode = new SettingsNode(k); - this.children.set(k, newNode); - if(slicedKey.length > 0) - newNode.addChild(slicedKey, value); - else - newNode.value = value; - this.children.get(k)!.addChild(slicedKey, undefined); + /* + Else move to the child node + */ + currentNode = currentNode.getChild(key); } } } + + public collectFromLeafsUpwards() { + let collected:MapArrayType = {}; + for (const key in this.children) { + const child = this.children[key]; + if (child.hasChildren()) { + collected[key] = child.collectFromLeafsUpwards(); + } else { + collected[key] = child.value; + } + } + return collected; + } + + 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 + + return +stringValue; + } + + switch (stringValue) { + case 'true': + return true; + case 'false': + return false; + case 'undefined': + return undefined; + case 'null': + return null; + default: + return stringValue; + } + }; + + public hasChildren() { + return Object.keys(this.children).length > 0; + } + public getChild(key: string) { - return this.children.get(key); + return this.children[key]; } public hasChild(key: string) { - return this.children.has(key); + return this.children[key] !== undefined; } }