2013-12-06 18:13:15 +04:00
// General entry point for all configuration data
//
// This file itself is a wrapper for the root level config.js file.
// All other files that need to reference config.js should use this file.
2013-11-20 17:58:52 +04:00
2014-01-05 10:40:53 +04:00
var path = require ( 'path' ) ,
2014-08-17 10:17:23 +04:00
Promise = require ( 'bluebird' ) ,
2014-10-10 18:54:07 +04:00
crypto = require ( 'crypto' ) ,
2014-08-23 20:19:13 +04:00
fs = require ( 'fs' ) ,
2014-01-05 10:40:53 +04:00
url = require ( 'url' ) ,
_ = require ( 'lodash' ) ,
2014-05-15 07:28:10 +04:00
knex = require ( 'knex' ) ,
2014-08-23 20:19:13 +04:00
validator = require ( 'validator' ) ,
2014-02-28 23:52:32 +04:00
requireTree = require ( '../require-tree' ) . readAll ,
2014-08-23 20:19:13 +04:00
errors = require ( '../errors' ) ,
2014-01-05 10:40:53 +04:00
configUrl = require ( './url' ) ,
2014-10-10 18:54:07 +04:00
packageInfo = require ( '../../../package.json' ) ,
2014-01-05 10:40:53 +04:00
appRoot = path . resolve ( _ _dirname , '../../../' ) ,
2014-05-15 07:28:10 +04:00
corePath = path . resolve ( appRoot , 'core/' ) ,
2014-07-17 18:33:21 +04:00
testingEnvs = [ 'testing' , 'testing-mysql' , 'testing-pg' ] ,
defaultConfig = { } ,
2014-05-15 07:28:10 +04:00
knexInstance ;
2014-01-05 10:40:53 +04:00
2014-08-23 20:19:13 +04:00
function ConfigManager ( config ) {
/ * *
* Our internal true representation of our current config object .
* @ private
* @ type { Object }
* /
this . _config = { } ;
// Allow other modules to be externally accessible.
this . urlFor = configUrl . urlFor ;
this . urlForPost = configUrl . urlForPost ;
// If we're given an initial config object then we can set it.
if ( config && _ . isObject ( config ) ) {
this . set ( config ) ;
}
}
2014-02-20 07:22:02 +04:00
// Are we using sockets? Custom socket or the default?
2014-08-23 20:19:13 +04:00
ConfigManager . prototype . getSocket = function ( ) {
if ( this . _config . server . hasOwnProperty ( 'socket' ) ) {
return _ . isString ( this . _config . server . socket ) ?
this . _config . server . socket :
path . join ( this . _config . paths . contentPath , process . env . NODE _ENV + '.socket' ) ;
2014-02-20 07:22:02 +04:00
}
return false ;
2014-08-23 20:19:13 +04:00
} ;
ConfigManager . prototype . init = function ( rawConfig ) {
var self = this ;
2014-02-20 07:22:02 +04:00
2014-08-23 20:19:13 +04:00
// Cache the config.js object's environment
// object so we can later refer to it.
// Note: this is not the entirety of config.js,
// just the object appropriate for this NODE_ENV
self . set ( rawConfig ) ;
return Promise . all ( [ requireTree ( self . _config . paths . themePath ) , requireTree ( self . _config . paths . appPath ) ] ) . then ( function ( paths ) {
self . _config . paths . availableThemes = paths [ 0 ] ;
self . _config . paths . availableApps = paths [ 1 ] ;
return self . _config ;
} ) ;
} ;
/ * *
* Allows you to set the config object .
* @ param { Object } config Only accepts an object at the moment .
* /
ConfigManager . prototype . set = function ( config ) {
2014-07-17 18:33:21 +04:00
var localPath = '' ,
2014-01-05 10:40:53 +04:00
contentPath ,
2014-10-10 18:54:07 +04:00
subdir ,
assetHash ;
2014-01-05 10:40:53 +04:00
2014-08-23 20:19:13 +04:00
// Merge passed in config object onto our existing config object.
// We're using merge here as it doesn't assign `undefined` properties
// onto our cached config object. This allows us to only update our
// local copy with properties that have been explicitly set.
_ . merge ( this . _config , config ) ;
2014-01-05 10:40:53 +04:00
// Protect against accessing a non-existant object.
// This ensures there's always at least a paths object
// because it's referenced in multiple places.
2014-08-23 20:19:13 +04:00
this . _config . paths = this . _config . paths || { } ;
2014-01-05 10:40:53 +04:00
// Parse local path location
2014-08-23 20:19:13 +04:00
if ( this . _config . url ) {
localPath = url . parse ( this . _config . url ) . path ;
2014-01-05 10:40:53 +04:00
// Remove trailing slash
if ( localPath !== '/' ) {
localPath = localPath . replace ( /\/$/ , '' ) ;
}
}
subdir = localPath === '/' ? '' : localPath ;
// Allow contentPath to be over-written by passed in config object
// Otherwise default to default content path location
2014-08-23 20:19:13 +04:00
contentPath = this . _config . paths . contentPath || path . resolve ( appRoot , 'content' ) ;
2014-01-05 10:40:53 +04:00
2014-10-10 18:54:07 +04:00
assetHash = this . _config . assetHash ||
( crypto . createHash ( 'md5' ) . update ( packageInfo . version + Date . now ( ) ) . digest ( 'hex' ) ) . substring ( 0 , 10 ) ;
2014-08-23 20:19:13 +04:00
if ( ! knexInstance && this . _config . database && this . _config . database . client ) {
knexInstance = knex ( this . _config . database ) ;
2014-05-15 07:28:10 +04:00
}
2014-08-23 20:19:13 +04:00
_ . merge ( this . _config , {
2014-05-15 07:28:10 +04:00
database : {
knex : knexInstance
} ,
2014-11-28 20:47:32 +03:00
ghostVersion : packageInfo . version ,
2014-01-05 10:40:53 +04:00
paths : {
2014-09-10 08:06:24 +04:00
appRoot : appRoot ,
subdir : subdir ,
config : this . _config . paths . config || path . join ( appRoot , 'config.js' ) ,
configExample : path . join ( appRoot , 'config.example.js' ) ,
corePath : corePath ,
contentPath : contentPath ,
themePath : path . resolve ( contentPath , 'themes' ) ,
appPath : path . resolve ( contentPath , 'apps' ) ,
imagesPath : path . resolve ( contentPath , 'images' ) ,
imagesRelPath : 'content/images' ,
adminViews : path . join ( corePath , '/server/views/' ) ,
helperTemplates : path . join ( corePath , '/server/helpers/tpl/' ) ,
exportPath : path . join ( corePath , '/server/data/export/' ) ,
lang : path . join ( corePath , '/shared/lang/' ) ,
availableThemes : this . _config . paths . availableThemes || { } ,
availableApps : this . _config . paths . availableApps || { } ,
builtScriptPath : path . join ( corePath , 'built/scripts/' )
2014-09-03 07:15:15 +04:00
} ,
theme : {
// normalise the URL by removing any trailing slash
url : this . _config . url ? this . _config . url . replace ( /\/$/ , '' ) : ''
2014-09-07 00:16:14 +04:00
} ,
slugs : {
// Used by generateSlug to generate slugs for posts, tags, users, ..
// reserved slugs are reserved but can be extended/removed by apps
// protected slugs cannot be changed or removed
reserved : [ 'admin' , 'app' , 'apps' , 'archive' , 'archives' , 'categories' , 'category' , 'dashboard' , 'feed' , 'ghost-admin' , 'login' , 'logout' , 'page' , 'pages' , 'post' , 'posts' , 'public' , 'register' , 'setup' , 'signin' , 'signout' , 'signup' , 'tag' , 'tags' , 'user' , 'users' , 'wp-admin' , 'wp-login' ] ,
protected : [ 'ghost' , 'rss' ]
2014-09-26 17:31:23 +04:00
} ,
uploads : {
// Used by the upload API to limit uploads to images
extensions : [ '.jpg' , '.jpeg' , '.gif' , '.png' , '.svg' , '.svgz' ] ,
contentTypes : [ 'image/jpeg' , 'image/png' , 'image/gif' , 'image/svg+xml' ]
2014-10-12 22:46:53 +04:00
} ,
2014-10-10 18:54:07 +04:00
deprecatedItems : [ 'updateCheck' , 'mail.fromaddress' ] ,
// create a hash for cache busting assets
assetHash : assetHash
2014-01-05 10:40:53 +04:00
} ) ;
// Also pass config object to
// configUrl object to maintain
2014-10-10 18:54:07 +04:00
// clean dependency tree
2014-08-23 20:19:13 +04:00
configUrl . setConfig ( this . _config ) ;
2014-01-05 10:40:53 +04:00
2014-08-23 20:19:13 +04:00
// For now we're going to copy the current state of this._config
// so it's directly accessible on the instance.
// @TODO: perhaps not do this? Put access of the config object behind
// a function?
_ . extend ( this , this . _config ) ;
} ;
/ * *
* Allows you to read the config object .
* @ return { Object } The config object .
* /
ConfigManager . prototype . get = function ( ) {
return this . _config ;
} ;
ConfigManager . prototype . load = function ( configFilePath ) {
var self = this ;
self . _config . paths . config = process . env . GHOST _CONFIG || configFilePath || self . _config . paths . config ;
/ * C h e c k f o r c o n f i g f i l e a n d c o p y f r o m c o n f i g . e x a m p l e . j s
if one doesn ' t exist . After that , start the server . * /
return new Promise ( function ( resolve , reject ) {
fs . exists ( self . _config . paths . config , function ( exists ) {
var pendingConfig ;
2014-01-05 10:40:53 +04:00
2014-08-23 20:19:13 +04:00
if ( ! exists ) {
pendingConfig = self . writeFile ( ) ;
}
Promise . resolve ( pendingConfig ) . then ( function ( ) {
return self . validate ( ) ;
} ) . then ( function ( rawConfig ) {
resolve ( self . init ( rawConfig ) ) ;
} ) . catch ( reject ) ;
} ) ;
2014-01-05 10:40:53 +04:00
} ) ;
2014-08-23 20:19:13 +04:00
} ;
/ * C h e c k f o r c o n f i g f i l e a n d c o p y f r o m c o n f i g . e x a m p l e . j s
if one doesn ' t exist . After that , start the server . * /
ConfigManager . prototype . writeFile = function ( ) {
var configPath = this . _config . paths . config ,
configExamplePath = this . _config . paths . configExample ;
return new Promise ( function ( resolve , reject ) {
fs . exists ( configExamplePath , function checkTemplate ( templateExists ) {
var read ,
write ,
error ;
if ( ! templateExists ) {
error = new Error ( 'Could not locate a configuration file.' ) ;
error . context = appRoot ;
error . help = 'Please check your deployment for config.js or config.example.js.' ;
return reject ( error ) ;
}
// Copy config.example.js => config.js
read = fs . createReadStream ( configExamplePath ) ;
read . on ( 'error' , function ( err ) {
errors . logError ( new Error ( 'Could not open config.example.js for read.' ) , appRoot , 'Please check your deployment for config.js or config.example.js.' ) ;
reject ( err ) ;
} ) ;
write = fs . createWriteStream ( configPath ) ;
write . on ( 'error' , function ( err ) {
errors . logError ( new Error ( 'Could not open config.js for write.' ) , appRoot , 'Please check your deployment for config.js or config.example.js.' ) ;
reject ( err ) ;
} ) ;
write . on ( 'finish' , resolve ) ;
read . pipe ( write ) ;
} ) ;
} ) ;
} ;
/ * *
* Read config . js file from file system using node ' s require
* @ param { String } envVal Which environment we ' re in .
* @ return { Object } The config object .
* /
ConfigManager . prototype . readFile = function ( envVal ) {
return require ( this . _config . paths . config ) [ envVal ] ;
} ;
/ * *
* Validates the config object has everything we want and in the form we want .
* @ return { Promise . < Object > } Returns a promise that resolves to the config object .
* /
ConfigManager . prototype . validate = function ( ) {
var envVal = process . env . NODE _ENV || undefined ,
hasHostAndPort ,
hasSocket ,
config ,
parsedUrl ;
try {
config = this . readFile ( envVal ) ;
}
catch ( e ) {
return Promise . reject ( e ) ;
}
// Check if we don't even have a config
if ( ! config ) {
errors . logError ( new Error ( 'Cannot find the configuration for the current NODE_ENV' ) , 'NODE_ENV=' + envVal ,
'Ensure your config.js has a section for the current NODE_ENV value and is formatted properly.' ) ;
return Promise . reject ( new Error ( 'Unable to load config for NODE_ENV=' + envVal ) ) ;
}
// Check that our url is valid
2014-09-10 08:06:24 +04:00
if ( ! validator . isURL ( config . url , { protocols : [ 'http' , 'https' ] , require _protocol : true } ) ) {
2014-08-23 20:19:13 +04:00
errors . logError ( new Error ( 'Your site url in config.js is invalid.' ) , config . url , 'Please make sure this is a valid url before restarting' ) ;
return Promise . reject ( new Error ( 'invalid site url' ) ) ;
}
parsedUrl = url . parse ( config . url || 'invalid' , false , true ) ;
if ( /\/ghost(\/|$)/ . test ( parsedUrl . pathname ) ) {
errors . logError ( new Error ( 'Your site url in config.js cannot contain a subdirectory called ghost.' ) , config . url , 'Please rename the subdirectory before restarting' ) ;
return Promise . reject ( new Error ( 'ghost subdirectory not allowed' ) ) ;
}
// Check that we have database values
if ( ! config . database || ! config . database . client ) {
errors . logError ( new Error ( 'Your database configuration in config.js is invalid.' ) , JSON . stringify ( config . database ) , 'Please make sure this is a valid Bookshelf database configuration' ) ;
return Promise . reject ( new Error ( 'invalid database configuration' ) ) ;
}
hasHostAndPort = config . server && ! ! config . server . host && ! ! config . server . port ;
hasSocket = config . server && ! ! config . server . socket ;
// Check for valid server host and port values
if ( ! config . server || ! ( hasHostAndPort || hasSocket ) ) {
errors . logError ( new Error ( 'Your server values (socket, or host and port) in config.js are invalid.' ) , JSON . stringify ( config . server ) , 'Please provide them before restarting.' ) ;
return Promise . reject ( new Error ( 'invalid server configuration' ) ) ;
}
return Promise . resolve ( config ) ;
} ;
2013-11-20 17:58:52 +04:00
2014-09-03 08:27:37 +04:00
/ * *
* Helper method for checking the state of a particular privacy flag
2014-09-10 08:06:24 +04:00
* @ param { String } privacyFlag The flag to check
2014-09-03 08:27:37 +04:00
* @ returns { boolean }
* /
ConfigManager . prototype . isPrivacyDisabled = function ( privacyFlag ) {
if ( ! this . privacy ) {
return false ;
}
if ( this . privacy . useTinfoil === true ) {
return true ;
}
return this . privacy [ privacyFlag ] === false ;
} ;
/ * *
* Check if any of the currently set config items are deprecated , and issues a warning .
* /
ConfigManager . prototype . checkDeprecated = function ( ) {
2014-10-12 22:46:53 +04:00
var self = this ;
_ . each ( this . deprecatedItems , function ( property ) {
2014-09-20 22:14:16 +04:00
self . displayDeprecated ( self , property . split ( '.' ) , [ ] ) ;
} ) ;
} ;
ConfigManager . prototype . displayDeprecated = function ( item , properties , address ) {
var self = this ,
property = properties . shift ( ) ,
errorText ,
explanationText ,
helpText ;
2014-09-03 08:27:37 +04:00
2014-09-20 22:14:16 +04:00
address . push ( property ) ;
if ( item . hasOwnProperty ( property ) ) {
if ( properties . length ) {
return self . displayDeprecated ( item [ property ] , properties , address ) ;
2014-09-03 08:27:37 +04:00
}
2014-09-20 22:14:16 +04:00
errorText = 'The configuration property [' + address . join ( '.' ) . bold + '] has been deprecated.' ;
explanationText = 'This will be removed in a future version, please update your config.js file.' ;
helpText = 'Please check http://support.ghost.org/config for the most up-to-date example.' ;
errors . logWarn ( errorText , explanationText , helpText ) ;
}
2014-09-03 08:27:37 +04:00
} ;
2014-07-17 18:33:21 +04:00
if ( testingEnvs . indexOf ( process . env . NODE _ENV ) > - 1 ) {
defaultConfig = require ( '../../../config.example' ) [ process . env . NODE _ENV ] ;
2013-11-26 07:22:59 +04:00
}
2013-11-20 17:58:52 +04:00
2014-08-23 20:19:13 +04:00
module . exports = new ConfigManager ( defaultConfig ) ;