2021-01-21 21:06:52 +00:00
'use strict' ;
2011-05-14 17:42:04 +01:00
/ * *
2020-04-14 01:10:19 +02:00
* The Settings module reads the settings out of settings . json and provides
2011-05-30 15:53:11 +01:00
* this information to the other modules
2020-04-14 01:10:19 +02:00
*
* TODO muxator 2020 - 04 - 14 :
*
* 1 ) get rid of the reloadSettings ( ) call at module loading ;
* 2 ) provide a factory method that configures the settings module at runtime ,
* reading the file name either from command line parameters , from a function
* argument , or falling back to a default .
2011-05-30 15:53:11 +01:00
* /
/ *
2011-08-11 15:26:41 +01:00
* 2011 Peter 'Pita' Martischka ( Primary Technology Ltd )
2011-05-14 17:42:04 +01:00
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS-IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2024-03-21 18:50:02 +01:00
import { MapArrayType } from "../types/MapType" ;
import { SettingsNode , SettingsTree } from "./SettingsTree" ;
import { coerce } from "semver" ;
2020-11-23 13:24:19 -05:00
const absolutePaths = require ( './AbsolutePaths' ) ;
2021-09-16 00:24:40 -04:00
const deepEqual = require ( 'fast-deep-equal/es6' ) ;
2020-11-23 13:24:19 -05:00
const fs = require ( 'fs' ) ;
const os = require ( 'os' ) ;
const path = require ( 'path' ) ;
const argv = require ( './Cli' ) . argv ;
const jsonminify = require ( 'jsonminify' ) ;
const log4js = require ( 'log4js' ) ;
const randomString = require ( './randomstring' ) ;
2021-02-21 20:28:19 +00:00
const suppressDisableMsg = ' -- To suppress these warning messages change ' +
'suppressErrorsInPadText to true in your settings.json\n' ;
2020-11-23 13:24:19 -05:00
const _ = require ( 'underscore' ) ;
2012-02-26 13:07:51 +01:00
2021-09-15 23:59:17 -04:00
const logger = log4js . getLogger ( 'settings' ) ;
2021-11-10 18:41:53 -05:00
// Exported values that settings.json and credentials.json cannot override.
const nonSettings = [
2024-03-21 18:50:02 +01:00
'credentialsFilename' ,
'settingsFilename' ,
2021-11-10 18:41:53 -05:00
] ;
2021-09-23 03:05:42 -04:00
// 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. :(
2024-08-05 15:28:48 -04:00
const defaultLogConfig = ( level : string , layoutType : string ) = > ( {
appenders : { console : { type : 'console' , layout : { type : layoutType } } } ,
2024-03-21 18:50:02 +01:00
categories : {
default : { appenders : [ 'console' ] , level } ,
}
} ) ;
2021-09-23 03:05:42 -04:00
const defaultLogLevel = 'INFO' ;
2024-08-05 15:28:48 -04:00
const defaultLogLayoutType = 'colored' ;
2021-09-23 03:05:42 -04:00
2024-03-21 18:50:02 +01:00
const initLogging = ( config : any ) = > {
// log4js.configure() modifies exports.logconfig so check for equality first.
log4js . configure ( config ) ;
log4js . getLogger ( 'console' ) ;
2023-10-22 18:26:58 +02:00
2024-03-21 18:50:02 +01:00
// 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 ) ;
2021-09-23 03:05:42 -04:00
} ;
// 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.
2024-08-05 15:28:48 -04:00
initLogging ( defaultLogConfig ( defaultLogLevel , defaultLogLayoutType ) ) ;
2021-09-23 03:05:42 -04:00
2012-02-26 13:07:51 +01:00
/* Root path of the installation */
2018-08-22 00:47:47 +02:00
exports . root = absolutePaths . findEtherpadRoot ( ) ;
2021-09-15 23:59:17 -04:00
logger . info ( 'All relative paths will be interpreted relative to the identified ' +
2023-12-17 22:13:13 +01:00
` Etherpad base dir: ${ exports . root } ` ) ;
2021-11-10 18:41:53 -05:00
exports . settingsFilename = absolutePaths . makeAbsolute ( argv . settings || 'settings.json' ) ;
exports . credentialsFilename = absolutePaths . makeAbsolute ( argv . credentials || 'credentials.json' ) ;
2011-05-14 17:22:25 +01:00
2012-11-02 12:30:57 +01:00
/ * *
* The app title , visible e . g . in the browser window
* /
2020-11-23 13:24:19 -05:00
exports . title = 'Etherpad' ;
2012-11-02 12:30:57 +01:00
2012-11-04 11:26:17 +01:00
/ * *
2021-04-20 00:53:22 -04:00
* 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 .
2012-11-04 11:26:17 +01:00
* /
2021-04-20 00:53:22 -04:00
exports . favicon = null ;
2012-11-04 11:26:17 +01:00
2024-04-29 17:04:00 +02:00
exports . ttl = {
AccessToken : 1 * 60 * 60 , // 1 hour in seconds
AuthorizationCode : 10 * 60 , // 10 minutes in seconds
ClientCredentials : 1 * 60 * 60 , // 1 hour in seconds
IdToken : 1 * 60 * 60 , // 1 hour in seconds
RefreshToken : 1 * 24 * 60 * 60 , // 1 day in seconds
}
2024-09-09 20:47:45 +02:00
exports . updateServer = "https://static.etherpad.org"
2024-04-29 17:04:00 +02:00
2018-08-19 03:36:18 +02:00
/ *
* Skin name .
*
* Initialized to null , so we can spot an old configuration file and invite the
* user to update it before falling back to the default .
* /
exports . skinName = null ;
2020-11-23 13:24:19 -05:00
exports . skinVariants = 'super-light-toolbar super-light-editor light-background' ;
2020-04-07 17:33:40 +02:00
2011-07-30 16:39:53 +01:00
/ * *
* The IP ep - lite should listen to
* /
2020-11-23 13:24:19 -05:00
exports . ip = '0.0.0.0' ;
2013-11-26 10:20:59 +02:00
2011-05-30 15:53:11 +01:00
/ * *
* The Port ep - lite should listen to
* /
2012-10-25 10:21:34 -07:00
exports . port = process . env . PORT || 9001 ;
2012-11-22 10:12:58 +01:00
2015-04-06 00:13:38 +01:00
/ * *
* Should we suppress Error messages from being in Pad Contents
* /
exports . suppressErrorsInPadText = false ;
2012-11-22 10:12:58 +01:00
/ * *
* The SSL signed server key and the Certificate Authority ' s own certificate
* default case : ep - lite does * not * use SSL . A signed server key is not required in this case .
* /
exports . ssl = false ;
2012-12-02 18:28:28 +01:00
/ * *
2012-12-02 18:44:39 +01:00
* socket . io transport methods
2012-12-02 18:28:28 +01:00
* * /
2024-02-18 00:06:26 +03:30
exports . socketTransportProtocols = [ 'websocket' , 'polling' ] ;
2012-12-02 18:28:28 +01:00
2021-02-14 19:04:29 +00:00
exports . socketIo = {
2024-03-21 18:50:02 +01:00
/ * *
* 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 ) .
* /
2024-06-01 07:17:02 -04:00
maxHttpBufferSize : 50000 ,
2021-02-14 19:04:29 +00:00
} ;
2024-05-14 22:36:16 +02:00
/ *
The authentication method used by the server .
The default value is sso
If you want to use the old authentication system , change this to apikey
* /
exports . authenticationMethod = 'sso'
2011-05-30 15:53:11 +01:00
/ *
* The Type of the database
* /
2024-09-05 16:06:16 +02:00
exports . dbType = 'rustydb' ;
2011-05-30 15:53:11 +01:00
/ * *
* This setting is passed with dbType to ueberDB to set up the database
* /
2024-09-05 16:06:16 +02:00
exports . dbSettings = { filename : path.join ( exports . root , 'var/rusty.db' ) } ;
2012-07-08 18:59:46 +02:00
2011-05-30 15:53:11 +01:00
/ * *
* The default Text of a new pad
* /
2021-02-21 20:28:19 +00:00
exports . defaultPadText = [
2024-03-21 18:50:02 +01:00
'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' ,
2021-02-21 20:28:19 +00:00
] . join ( '\n' ) ;
2011-11-21 01:45:37 -05:00
2015-04-11 21:22:00 +01:00
/ * *
* The default Pad Settings for a user ( Can be overridden by changing the setting
* /
exports . padOptions = {
2024-03-21 18:50:02 +01:00
noColors : false ,
showControls : true ,
showChat : true ,
showLineNumbers : true ,
useMonospaceFont : false ,
userName : null ,
userColor : null ,
rtl : false ,
alwaysShowChat : false ,
chatAndUsers : false ,
lang : null ,
2021-05-11 16:56:06 -04:00
} ;
2016-01-21 07:38:41 -05:00
/ * *
* Whether certain shortcut keys are enabled for a user in the pad
* /
exports . padShortcutEnabled = {
2024-03-21 18:50:02 +01:00
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 ,
2021-05-11 16:56:06 -04:00
} ;
2015-04-11 21:22:00 +01:00
2013-03-09 14:57:42 -08:00
/ * *
* The toolbar buttons and order .
* /
exports . toolbar = {
2024-03-21 18:50:02 +01:00
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' ] ,
] ,
2020-11-23 13:24:19 -05:00
} ;
2013-03-09 14:57:42 -08:00
2011-11-21 01:45:37 -05:00
/ * *
* A flag that requires any user to have a valid session ( via the api ) before accessing a pad
* /
exports . requireSession = false ;
2011-11-21 12:44:33 -05:00
/ * *
* A flag that prevents users from creating new pads
* /
exports . editOnly = false ;
2012-02-06 23:04:02 -08:00
/ * *
* Max age that responses will have ( affects caching layer ) .
* /
2020-11-23 13:24:19 -05:00
exports . maxAge = 1000 * 60 * 60 * 6 ; // 6 hours
2012-02-06 23:04:02 -08:00
2011-05-30 15:53:11 +01:00
/ * *
* A flag that shows if minification is enabled or not
* /
2011-05-28 18:09:17 +01:00
exports . minify = true ;
2011-05-14 17:22:25 +01:00
2011-07-19 19:48:11 +01:00
/ * *
* The path of the abiword executable
* /
exports . abiword = null ;
2015-10-20 19:46:08 +01:00
/ * *
* The path of the libreoffice executable
* /
exports . soffice = null ;
2014-07-22 15:46:31 +01:00
/ * *
* Should we support none natively supported file types on import ?
* /
exports . allowUnknownFileEnds = true ;
2011-08-17 17:45:47 +01:00
/ * *
* The log level of log4js
* /
2021-09-23 03:05:42 -04:00
exports . loglevel = defaultLogLevel ;
2011-08-17 17:45:47 +01:00
2024-08-05 15:28:48 -04:00
/ * *
* The log layout type of log4js
* /
exports . logLayoutType = defaultLogLayoutType ;
2013-10-19 21:37:11 +02:00
/ * *
* Disable IP logging
* /
exports . disableIPlogging = false ;
2017-04-04 11:09:24 -03:00
/ * *
* Number of seconds to automatically reconnect pad
* /
exports . automaticReconnectionTimeout = 0 ;
2015-05-18 16:24:41 +01:00
/ * *
2015-02-16 23:02:19 +00:00
* Disable Load Testing
* /
exports . loadTest = false ;
2021-04-13 10:10:56 +02:00
/ * *
* Disable dump of objects preventing a clean exit
* /
exports . dumpOnUncleanExit = false ;
2015-10-13 18:39:23 -03:00
/ * *
* Enable indentation on new lines
* /
exports . indentationOnNewLine = true ;
2013-01-13 12:20:49 +01:00
/ *
2019-04-16 00:17:56 +02:00
* log4js appender configuration
* /
2023-12-17 22:13:13 +01:00
exports . logconfig = null ;
2013-01-13 12:20:49 +01:00
2013-02-13 21:51:09 +00:00
/ *
2023-07-03 16:58:49 -04:00
* Deprecated cookie signing key .
2019-04-16 00:17:56 +02:00
* /
2023-07-03 16:58:49 -04:00
exports . sessionKey = null ;
2013-02-13 21:51:09 +00:00
2013-04-24 12:19:41 +02:00
/ *
2019-04-16 00:17:56 +02:00
* Trust Proxy , whether or not trust the x - forwarded - for header .
* /
2013-04-24 12:19:41 +02:00
exports . trustProxy = false ;
2020-10-02 23:53:05 -04:00
/ *
* Settings controlling the session cookie issued by Etherpad .
* /
exports . cookie = {
2024-03-21 18:50:02 +01:00
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 ,
2020-10-02 23:53:05 -04:00
} ;
2019-04-16 00:17:56 +02:00
/ *
* This setting is used if you need authentication and / or
2012-04-19 14:25:12 +02:00
* authorization . Note : / a d m i n a l w a y s r e q u i r e s a u t h e n t i c a t i o n , a n d
2019-04-16 00:17:56 +02:00
* either authorization by a module , or a user with is_admin set
* /
2012-04-19 14:25:12 +02:00
exports . requireAuthentication = false ;
exports . requireAuthorization = false ;
exports . users = { } ;
2012-04-02 18:45:37 +02:00
2024-03-26 17:11:24 +01:00
/ *
* This setting is used for configuring sso
* /
2024-04-03 20:19:16 +02:00
exports . sso = {
issuer : "http://localhost:9001"
}
2024-03-26 17:11:24 +01:00
2016-05-22 20:58:51 +05:30
/ *
2019-04-16 00:17:56 +02:00
* Show settings in admin page , by default it is true
* /
2016-05-22 20:58:51 +05:30
exports . showSettingsInAdminPage = true ;
2018-01-03 18:57:28 -03:00
/ *
2019-04-16 00:17:56 +02:00
* By default , when caret is moved out of viewport , it scrolls the minimum
* height needed to make this line visible .
* /
2018-01-03 18:57:28 -03:00
exports . scrollWhenFocusLineIsOutOfViewport = {
2024-03-21 18:50:02 +01:00
/ *
* 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 ,
2018-01-03 18:57:28 -03:00
} ;
2019-04-15 16:02:46 +02:00
/ *
2019-04-15 17:03:06 +02:00
* Expose Etherpad version in the web interface and in the Server http header .
2019-04-15 16:02:46 +02:00
*
* Do not enable on production machines .
* /
exports . exposeVersion = false ;
2020-05-19 08:21:31 -04:00
/ *
* Override any strings found in locale directories
* /
exports . customLocaleStrings = { } ;
2020-04-04 20:39:33 +00:00
/ *
* From Etherpad 1.8 . 3 onwards , import and export of pads is always rate
* limited .
*
* The default is to allow at most 10 requests per IP in a 90 seconds window .
* After that the import / export request is rejected .
*
* See https : //github.com/nfriedly/express-rate-limit for more options
* /
exports . importExportRateLimiting = {
2024-03-21 18:50:02 +01:00
// duration of the rate limit window (milliseconds)
windowMs : 90000 ,
2020-04-04 20:39:33 +00:00
2024-03-21 18:50:02 +01:00
// maximum number of requests per IP to allow during the rate limit window
max : 10 ,
2020-04-04 20:39:33 +00:00
} ;
2020-07-19 22:44:24 +01:00
/ *
* From Etherpad 1.9 . 0 onwards , commits from individual users are rate limited
*
* The default is to allow at most 10 changes per IP in a 1 second window .
* After that the change is rejected .
*
* See https : //github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#websocket-single-connection-prevent-flooding for more options
* /
exports . commitRateLimiting = {
2024-03-21 18:50:02 +01:00
// duration of the rate limit window (seconds)
duration : 1 ,
2020-07-19 22:44:24 +01:00
2024-03-21 18:50:02 +01:00
// maximum number of chanes per IP to allow during the rate limit window
points : 10 ,
2020-07-19 22:44:24 +01:00
} ;
2020-04-07 01:07:10 +02:00
/ *
* From Etherpad 1.8 . 3 onwards , the maximum allowed size for a single imported
* file is always bounded .
*
* File size is specified in bytes . Default is 50 MB .
* /
exports . importMaxFileSize = 50 * 1024 * 1024 ;
2021-02-07 11:32:57 +00:00
/ *
2021-02-13 00:31:36 -05:00
* Disable Admin UI tests
2021-02-07 11:32:57 +00:00
* /
exports . enableAdminUITests = false ;
2023-07-03 20:52:49 +02:00
/ *
* Enable auto conversion of pad Ids to lowercase .
* e . g . / p / EtHeRpAd to / p / etherpad
* /
exports . lowerCasePadIds = false ;
2021-02-07 11:32:57 +00:00
2019-04-16 00:17:56 +02:00
// checks if abiword is avaiable
2021-01-21 21:06:52 +00:00
exports . abiwordAvailable = ( ) = > {
2024-03-21 18:50:02 +01:00
if ( exports . abiword != null ) {
return os . type ( ) . indexOf ( 'Windows' ) !== - 1 ? 'withoutPDF' : 'yes' ;
} else {
return 'no' ;
}
2013-03-24 01:18:44 +01:00
} ;
2011-12-18 00:18:35 -05:00
2021-01-21 21:06:52 +00:00
exports . sofficeAvailable = ( ) = > {
2024-03-21 18:50:02 +01:00
if ( exports . soffice != null ) {
return os . type ( ) . indexOf ( 'Windows' ) !== - 1 ? 'withoutPDF' : 'yes' ;
} else {
return 'no' ;
}
2015-12-17 21:54:04 -06:00
} ;
2021-01-21 21:06:52 +00:00
exports . exportAvailable = ( ) = > {
2024-03-21 18:50:02 +01:00
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' ;
}
2015-12-17 21:54:04 -06:00
} ;
2015-02-11 17:59:05 +00:00
// Provide git version if available
2021-01-21 21:06:52 +00:00
exports . getGitCommit = ( ) = > {
2024-03-21 18:50:02 +01:00
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 } ` ) ;
2020-10-22 15:05:12 -07:00
}
2024-03-21 18:50:02 +01:00
return version ;
2020-11-23 13:24:19 -05:00
} ;
2015-02-11 17:59:05 +00:00
2015-04-11 00:13:04 +02:00
// Return etherpad version from package.json
2021-01-21 21:06:52 +00:00
exports . getEpVersion = ( ) = > require ( '../../package.json' ) . version ;
2015-04-11 00:13:04 +02:00
2024-05-14 22:36:16 +02:00
2019-03-09 10:06:51 +01:00
/ * *
* Receives a settingsObj and , if the property name is a valid configuration
* item , stores it in the module ' s exported properties via a side effect .
*
* This code refactors a previous version that copied & pasted the same code for
* both "settings.json" and "credentials.json" .
* /
2024-03-21 18:50:02 +01:00
const storeSettings = ( settingsObj : any ) = > {
for ( const i of Object . keys ( settingsObj || { } ) ) {
if ( nonSettings . includes ( i ) ) {
logger . warn ( ` Ignoring setting: ' ${ i } ' ` ) ;
continue ;
}
2021-11-10 18:41:53 -05:00
2024-03-21 18:50:02 +01:00
// 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 } ' ` ) ;
}
2019-03-09 10:06:51 +01:00
2024-03-21 18:50:02 +01:00
// 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 ` ) ;
}
2019-03-09 10:06:51 +01:00
}
2021-01-21 21:06:52 +00:00
} ;
2019-03-09 10:06:51 +01:00
2019-03-21 21:32:39 +01:00
/ *
* If stringValue is a numeric string , or its value is "true" or "false" , coerce
* them to appropriate JS types . Otherwise return stringValue as - is .
2020-04-21 03:40:49 +02:00
*
* Please note that this function is used for converting types for default
* values in the settings file ( for example : "${PORT:9001}" ) , and that there is
* no coercition for "null" values .
*
* If the user wants a variable to be null by default , he ' ll have to use the
* short syntax "${ABIWORD}" , and not "${ABIWORD:null}" : the latter would result
* in the literal string "null" , instead .
2019-03-21 21:32:39 +01:00
* /
2024-03-21 18:50:02 +01:00
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 ) ) ;
2019-03-21 21:32:39 +01:00
2024-03-21 18:50:02 +01:00
if ( isNumeric ) {
// detected numeric string. Coerce to a number
2019-03-21 21:32:39 +01:00
2024-03-21 18:50:02 +01:00
return + stringValue ;
}
2019-03-21 21:32:39 +01:00
2024-03-21 18:50:02 +01:00
switch ( stringValue ) {
case 'true' :
return true ;
case 'false' :
return false ;
case 'undefined' :
return undefined ;
case 'null' :
return null ;
default :
return stringValue ;
}
2021-01-21 21:06:52 +00:00
} ;
2019-03-21 21:32:39 +01:00
2019-03-09 23:01:21 +01:00
/ * *
* Takes a javascript object containing Etherpad ' s configuration , and returns
2019-03-21 22:18:59 +01:00
* another object , in which all the string properties whose value is of the form
2019-03-21 01:37:19 +01:00
* "${ENV_VAR}" or "${ENV_VAR:default_value}" got their value replaced with the
* contents of the given environment variable , or with a default value .
2019-03-09 23:01:21 +01:00
*
2019-03-21 22:18:59 +01:00
* By definition , an environment variable ' s value is always a string . However ,
* the code base makes use of the various json types . To maintain compatiblity ,
* some heuristics is applied :
2019-03-09 23:01:21 +01:00
*
* - if ENV_VAR does not exist in the environment , null is returned ;
* - if ENV_VAR ' s value is "true" or "false" , it is converted to the js boolean
* values true or false ;
* - if ENV_VAR ' s value looks like a number , it is converted to a js number
* ( details in the code ) .
*
2019-03-21 22:18:59 +01:00
* The following is a scheme of the behaviour of this function :
*
* + -- -- -- -- -- -- -- -- -- -- -- -- -- - + -- -- -- -- -- -- -- - + -- -- -- -- -- -- -- -- -- +
* | Configuration string in | Value of | Resulting confi - |
* | settings . json | ENV_VAR | guration value |
* | -- -- -- -- -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- |
* | "${ENV_VAR}" | "some_string" | "some_string" |
* | "${ENV_VAR}" | "9001" | 9001 |
* | "${ENV_VAR}" | undefined | null |
2019-03-21 01:37:19 +01:00
* | "${ENV_VAR:some_default}" | "some_string" | "some_string" |
* | "${ENV_VAR:some_default}" | undefined | "some_default" |
2019-03-21 22:18:59 +01:00
* + -- -- -- -- -- -- -- -- -- -- -- -- -- - + -- -- -- -- -- -- -- - + -- -- -- -- -- -- -- -- -- +
*
* IMPLEMENTATION NOTE : variable substitution is performed doing a round trip
* conversion to / from json , using a custom replacer parameter in
* JSON . stringify ( ) , and parsing the JSON back again . This ensures that
* environment variable replacement is performed even on nested objects .
2019-03-09 23:01:21 +01:00
*
* see : https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter
* /
2024-03-21 18:50:02 +01:00
const lookupEnvironmentVariables = ( obj : MapArrayType < any > ) = > {
const replaceEnvs = ( obj : MapArrayType < any > ) = > {
for ( let [ key , value ] of Object . entries ( obj ) ) {
/ *
* the first invocation of replacer ( ) is with an empty key . Just go on , or
* we would zap the entire object .
* /
if ( key === '' ) {
obj [ key ] = value ;
continue
}
/ *
* If we received from the configuration file a number , a boolean or
* something that is not a string , we can be sure that it was a literal
* value . No need to perform any variable substitution .
*
* The environment variable expansion syntax "${ENV_VAR}" is just a string
* of specific form , after all .
* /
if ( key === 'undefined' || value === undefined ) {
delete obj [ key ]
continue
}
if ( ( typeof value !== 'string' && typeof value !== 'object' ) || value === null ) {
obj [ key ] = value ;
continue
}
if ( typeof obj [ key ] === "object" ) {
replaceEnvs ( obj [ key ] ) ;
continue
}
/ *
* Let ' s check if the string value looks like a variable expansion ( e . g . :
* "${ENV_VAR}" or "${ENV_VAR:default_value}" )
* /
// MUXATOR 2019-03-21: we could use named capture groups here once we migrate to nodejs v10
const match = value . match ( /^\$\{([^:]*)(:((.|\n)*))?\}$/ ) ;
if ( match == null ) {
// no match: use the value literally, without any substitution
obj [ key ] = value ;
continue
}
/ *
* We found the name of an environment variable . Let ' s read its actual value
* and its default value , if given
* /
const envVarName = match [ 1 ] ;
const envVarValue = process . env [ envVarName ] ;
const defaultValue = match [ 3 ] ;
if ( ( envVarValue === undefined ) && ( defaultValue === undefined ) ) {
logger . warn ( ` Environment variable " ${ envVarName } " does not contain any value for ` +
` configuration key " ${ key } ", and no default was given. Using null. ` +
'THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF ETHERPAD; you should ' +
'explicitly use "null" as the default if you want to continue to use null.' ) ;
/ *
* We have to return null , because if we just returned undefined , the
* configuration item "key" would be stripped from the returned object .
* /
obj [ key ] = null ;
continue
}
if ( ( envVarValue === undefined ) && ( defaultValue !== undefined ) ) {
logger . debug ( ` Environment variable " ${ envVarName } " not found for ` +
` configuration key " ${ key } ". Falling back to default value. ` ) ;
obj [ key ] = coerceValue ( defaultValue ) ;
continue
}
// envVarName contained some value.
/ *
* For numeric and boolean strings let ' s convert it to proper types before
* returning it , in order to maintain backward compatibility .
* /
logger . debug (
` Configuration key " ${ key } " will be read from environment variable " ${ envVarName } " ` ) ;
obj [ key ] = coerceValue ( envVarValue ! ) ;
}
return obj
2019-03-09 23:01:21 +01:00
}
2024-03-21 18:50:02 +01:00
replaceEnvs ( obj ) ;
2019-03-09 23:01:21 +01:00
2024-03-21 18:50:02 +01:00
// Add plugin ENV variables
2019-03-09 23:01:21 +01:00
2024-03-21 18:50:02 +01:00
/ * *
* If the key contains a double underscore , it ' s a plugin variable
* E . g .
2019-03-21 01:37:19 +01:00
* /
2024-03-21 18:50:02 +01:00
let treeEntries = new Map < string , string | undefined >
const root = new SettingsNode ( "EP" )
2019-03-21 01:37:19 +01:00
2024-03-21 18:50:02 +01:00
for ( let [ env , envVal ] of Object . entries ( process . env ) ) {
if ( ! env . startsWith ( "EP" ) ) continue
treeEntries . set ( env , envVal )
2019-03-21 01:37:19 +01:00
}
2024-03-21 18:50:02 +01:00
treeEntries . forEach ( ( value , key ) = > {
let pathToKey = key . split ( "__" )
let currentNode = root
let depth = 0
depth ++
currentNode . addChild ( pathToKey , value ! )
} )
//console.log(root.collectFromLeafsUpwards())
const rooting = root . collectFromLeafsUpwards ( )
obj = Object . assign ( obj , rooting )
return obj ;
2021-01-21 21:06:52 +00:00
} ;
2019-03-09 23:01:21 +01:00
2024-03-21 18:50:02 +01:00
2019-03-10 00:36:53 +01:00
/ * *
* - reads the JSON configuration file settingsFilename from disk
* - strips the comments
2019-03-09 23:01:21 +01:00
* - replaces environment variables calling lookupEnvironmentVariables ( )
2019-03-10 00:36:53 +01:00
* - returns a parsed Javascript object
*
* The isSettings variable only controls the error logging .
* /
2024-03-21 18:50:02 +01:00
const parseSettings = ( settingsFilename : string , isSettings : boolean ) = > {
let settingsStr = '' ;
2015-12-02 11:54:37 +00:00
2024-03-21 18:50:02 +01:00
let settingsType , notFoundMessage , notFoundFunction ;
2011-05-14 17:22:25 +01:00
2024-03-21 18:50:02 +01:00
if ( isSettings ) {
settingsType = 'settings' ;
notFoundMessage = 'Continuing using defaults!' ;
notFoundFunction = logger . warn . bind ( logger ) ;
} else {
settingsType = 'credentials' ;
notFoundMessage = 'Ignoring.' ;
notFoundFunction = logger . info . bind ( logger ) ;
}
2015-12-02 11:54:37 +00:00
2024-03-21 18:50:02 +01:00
try {
// read the settings file
settingsStr = fs . readFileSync ( settingsFilename ) . toString ( ) ;
} catch ( e ) {
notFoundFunction ( ` No ${ settingsType } file found in ${ settingsFilename } . ${ notFoundMessage } ` ) ;
2011-05-14 17:22:25 +01:00
2024-03-21 18:50:02 +01:00
// or maybe undefined!
return null ;
}
2019-03-10 00:36:53 +01:00
2024-03-21 18:50:02 +01:00
try {
settingsStr = jsonminify ( settingsStr ) . replace ( ',]' , ']' ) . replace ( ',}' , '}' ) ;
2019-03-10 00:36:53 +01:00
2024-03-21 18:50:02 +01:00
const settings = JSON . parse ( settingsStr ) ;
2019-03-10 00:36:53 +01:00
2024-03-21 18:50:02 +01:00
logger . info ( ` ${ settingsType } loaded from: ${ settingsFilename } ` ) ;
2019-03-09 23:01:21 +01:00
2024-03-21 18:50:02 +01:00
return lookupEnvironmentVariables ( settings ) ;
} catch ( e : any ) {
logger . error ( ` There was an error processing your ${ settingsType } ` +
` file from ${ settingsFilename } : ${ e . message } ` ) ;
2019-03-10 00:36:53 +01:00
2024-03-21 18:50:02 +01:00
process . exit ( 1 ) ;
}
2021-01-21 21:06:52 +00:00
} ;
2019-03-10 00:36:53 +01:00
2021-01-21 21:06:52 +00:00
exports . reloadSettings = ( ) = > {
2024-03-21 18:50:02 +01:00
const settings = parseSettings ( exports . settingsFilename , true ) ;
const credentials = parseSettings ( exports . credentialsFilename , false ) ;
storeSettings ( settings ) ;
storeSettings ( credentials ) ;
// Init logging config
2024-08-05 15:28:48 -04:00
exports . logconfig = defaultLogConfig (
exports . loglevel ? exports.loglevel : defaultLogLevel ,
exports . logLayoutType ? exports.logLayoutType : defaultLogLayoutType
) ;
logger . warn ( "loglevel: " + exports . loglevel ) ;
logger . warn ( "logLayoutType: " + exports . logLayoutType ) ;
2024-03-21 18:50:02 +01:00
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' ;
2018-08-19 03:36:18 +02:00
}
2024-08-13 02:07:02 +08:00
if ( ! exports . socketTransportProtocols . includes ( "websocket" ) || ! exports . socketTransportProtocols . includes ( "polling" ) ) {
2024-07-07 19:25:14 +02:00
logger . warn ( "Invalid socketTransportProtocols setting. Please check out settings.json.template and update your settings.json. Falling back to the default ['websocket', 'polling']." ) ;
exports . socketTransportProtocols = [ 'websocket' , 'polling' ] ;
}
2024-03-21 18:50:02 +01:00
// 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 ;
2018-08-19 03:36:18 +02:00
2024-03-21 18:50:02 +01:00
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". ` ) ;
2018-08-19 03:36:18 +02:00
2024-03-21 18:50:02 +01:00
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 } ` ) ;
2018-08-19 03:36:18 +02:00
}
2024-03-21 18:50:02 +01:00
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 ;
}
} ) ;
}
2018-08-19 03:36:18 +02:00
}
2024-03-21 18:50:02 +01:00
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 ;
}
} ) ;
}
2018-08-19 03:36:18 +02:00
2024-03-21 18:50:02 +01:00
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' ) ;
2015-01-04 14:47:08 +00:00
}
2024-03-21 18:50:02 +01:00
} 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).' ) ;
2015-01-04 14:47:08 +00:00
}
2015-12-17 21:54:04 -06:00
2024-03-21 18:50:02 +01:00
if ( exports . dbType === 'dirty' ) {
const dirtyWarning = 'DirtyDB is used. This is not recommended for production.' ;
2019-03-10 01:46:55 +01:00
if ( ! exports . suppressErrorsInPadText ) {
2024-03-21 18:50:02 +01:00
exports . defaultPadText += ` \ nWarning: ${ dirtyWarning } ${ suppressDisableMsg } ` ;
2015-12-17 21:54:04 -06:00
}
2024-03-21 18:50:02 +01:00
exports . dbSettings . filename = absolutePaths . makeAbsolute ( exports . dbSettings . filename ) ;
logger . warn ( ` ${ dirtyWarning } File location: ${ exports . dbSettings . filename } ` ) ;
2015-04-06 00:13:38 +01:00
}
2024-03-21 18:50:02 +01:00
2024-09-07 14:59:05 +02:00
if ( exports . dbType === 'rustydb' || exports . dbType === "sqlite" ) {
2024-09-05 16:06:16 +02:00
exports . dbSettings . filename = absolutePaths . makeAbsolute ( exports . dbSettings . filename ) ;
logger . warn ( ` File location: ${ exports . dbSettings . filename } ` ) ;
}
2024-09-07 14:59:05 +02:00
2024-03-21 18:50:02 +01:00
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.' ) ;
2015-04-06 00:13:38 +01:00
}
2018-08-22 01:16:33 +02:00
2024-03-21 18:50:02 +01:00
/ *
* 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 } ` ) ;
2013-03-24 01:18:44 +01:00
} ;
2012-04-20 17:03:37 +02:00
2021-06-06 05:26:52 -04:00
exports . exportedForTestingOnly = {
2024-03-21 18:50:02 +01:00
parseSettings ,
2021-06-06 05:26:52 -04:00
} ;
2012-11-06 17:35:05 +01:00
// initially load settings
exports . reloadSettings ( ) ;