'use strict';
var path = require('path');
var _ = require('lodash');
var logger = require('yocto-logger');
var joi = require('joi');
var fs = require('fs');
var glob = require('glob');
var utils = require('yocto-utils');
var Q = require('q');
var schema = require('./schema');
/**
* Yocto config manager.
* Manage your configuration file (all / common / env & specific file)
*
* Config file has priority. And priority is defined like a php ini system.
* `(Other file).json` < `all.json` < `common.json` < `development.json` < `stagging.json` < `production.json`
*
* All specific data must be configured on a each correct file.
*
* all.json : contains general data
* common.json : must contains all common data between each env
* development.json : must contains development data for development environnement
* staging.json : must contains stagging data for staging environnement
* production.json : must contains production data for production environnement
*
* This Module use some security format rules based on Lusca NPM module : {@link https://www.npmjs.com/package/lusca}
* - Cross Site Request Forgery (CSRF) headers : {@link https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)}
* - Content Security Policy (CSP) headers : {@link https://www.owasp.org/index.php/Content_Security_Policy}
* - MDN CSP usage : {@link https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Using_Content_Security_Policy}
* - X-FRAME-OPTIONS : {@link https://www.owasp.org/index.php/Clickjacking}
* - Platform for Privacy Preferences Project (P3P) headers : {@link http://support.microsoft.com/kb/290333}
* - HTTP Strict Transport Security (HSTS & Chrome HSTS preload) : {@link https://www.owasp.org/index.php/HTTP_Strict_Transport_Security} & {@link https://hstspreload.appspot.com/}
* - XssProtection : {@link http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-iv-the-xss-filter.aspx}
*
* @date : 12/05/2015
* @author : ROBERT Mathieu <mathieu@yocto.re>
* @copyright : Yocto SAS, All right reserved
* @class Config
*/
function Config (logger) {
/**
* Default config value
*
* @public
* @memberof Config
* @member {Object} config
* @default {}
*/
this.config = {};
/**
* Default state value, true if config state is or false otherwise
*
* @public
* @memberof Config
* @member {Boolean} state
* @default false
*/
this.state = false;
/**
* Default env value
*
* @public
* @memberof Config
* @member {String} env
* @default development
*/
this.env = process.env.NODE_ENV || 'development';
/**
* Default base path
*
* @public
* @memberof Config
* @member {String} base
*/
this.base = process.cwd();
/**
* Default logger instance. can be override by set function
*
* @public
* @memberof Config
* @member {Instance} logger
*/
this.logger = logger;
/**
* Prefix to use in case of multiple configuration
*
* @public
* @memberof Config
* @member {String} prefix
*/
this.suffix = process.env.CONFIG_SUFFIX_PATH || '';
/**
* Default schema validation for config validator
*
* @public
* @memberof Config
* @member {Object}schema
* @default {
* express : schema.getExpress(),
* mongoose : schema.getMongoose(),
* passportJs : schema.getPassportJs(),
* render : schema.getRender(),
* router : schema.getRouter()
* }
*/
this.schemaList = {
express : schema.getExpress(),
mongoose : schema.getMongoose(),
passportJs : schema.getPassportJs(),
render : schema.getRender(),
router : schema.getRouter()
};
/**
* Current schema to use for validation
*
* @public
* @memberof Config
* @property {Object} schema
* @default {}
*/
this.schema = {};
}
/**
* Default find function, retreive a config from given name
*
* @param {String} name wanted config
* @param {String} complete true if we need to complete existing config with new
* @return {Boolean} true if all is ok falser otherwise
*/
Config.prototype.find = function (name, complete) {
// message
this.logger.debug([ '[ Config.find ] - Try to enable config for', name].join(' '));
// search item
var item = _.has(this.schemaList, name);
// has item ?
if (item) {
// get value
item = this.schemaList[name];
// create default object
var obj = _.set({}, name, item);
// express item ??
if (name === 'express' && _.has(obj, 'express')) {
// change depth assign
obj = obj.express;
}
// mongoose or sequelize item ?? transform to db
if (name === 'mongoose' || name === 'sequelize') {
// change object name for db
obj = {
db : obj[name]
};
}
// add item at the end of list ?
if (_.isBoolean(complete) && complete) {
// not extend but merge current object
_.merge(this.schema, obj);
} else {
// simple assignation
this.schema = obj;
}
// message
this.logger.info(['[ Config.find ] -', name, 'config was correcty activated.'].join(' '));
// valid statement
return true;
}
// default statement
return false;
};
/**
* Enable Express config
*
* @param {Boolean} complete true if we need to append new config after existing
* @return {Boolean} true if all is ok falser otherwise
*/
Config.prototype.enableExpress = function (complete) {
// process
return this.enableSchema('express', complete);
};
/**
* Enable Yocto Render config
*
* @param {Boolean} complete true if we need to append new config after existing
* @return {Boolean} true if all is ok falser otherwise
*/
Config.prototype.enableRender = function (complete) {
// process
return this.enableSchema('render', complete);
};
/**
* Enable Yocto Router config
*
* @param {Boolean} complete true if we need to append new config after existing
* @return {Boolean} true if all is ok falser otherwise
*/
Config.prototype.enableRouter = function (complete) {
// process
return this.enableSchema('router', complete);
};
/**
* Enable Mongoose config
*
* @param {Boolean} complete true if we need to append new config after existing
* @return {Boolean} true if all is ok falser otherwise
*/
Config.prototype.enableMongoose = function (complete) {
// process
return this.enableSchema('mongoose', complete);
};
/**
* Enable PassportJs config
*
* @param {Boolean} complete true if we need to append new config after existing
* @return {Boolean} true if all is ok falser otherwise
*/
Config.prototype.enablePassportJs = function (complete) {
// process
return this.enableSchema('passportJs', complete);
};
/**
* Enable Specific schema config
*
* @param {String} name default name to find in schema
* @param {Boolean} complete true if we need to add new config after existing
* @return {Boolean} true if all is ok falser otherwise
*/
Config.prototype.enableSchema = function (name, complete) {
// force complete to be a boolean
complete = _.isBoolean(complete) ? complete : false;
// process
return this.find(name, complete);
};
/**
* Add a custom schema on config
*
* @param {String} name config name to add in schema
* @param {Object} value config value to add in schema
* @param {Boolean} enable true if we need to add auto enable of config
* @param {Boolean} complete true if we need to add new config after existing
*/
Config.prototype.addCustomSchema = function (name, value, enable, complete) {
// schema already exists ?
if (_.has(this.schemaList, name) || !_.isObject(value)) {
// error schema already exists
this.logger.error([ '[ Config.addCustomerSchema ] - Cannot add new custom schema. Schema :',
name, 'already exist, or given value is invalid.' ].join(' '));
// invalid schema
return false;
}
// add new schema
_.extend(this.schemaList, _.set({}, name, value));
// auto enable ?
if (_.isBoolean(enable) && enable) {
// enable schema
return this.enableSchema(name, complete);
}
// valid statement
return _.has(this.schemaList, name);
};
/**
* Default set function, a value to a specific params
*
* @param {String} name current name to use
* @param {String} value current value to assign on params name
* @return {Boolean} true if all is ok false otherwise
*/
Config.prototype.set = function (name, value) {
// check requirements
if (!_.isUndefined(name) && _.isString(name) && !_.isEmpty(name)) {
// need to normalize path ?
if (name === 'base') {
// is relative path ?
if (!path.isAbsolute(value)) {
// normalize
value = path.normalize([ process.cwd(), value ].join('/'));
}
}
// assign value
this[name] = value;
// valid statement
return true;
} else {
// warn message
this.logger.warning([ '[ Core.set ] - Invalid value given.',
'name must be a string and not empty. Operation aborted !' ].join(' '));
}
// invalid statement
return false;
};
/**
* Retreive default configuration
*
* @return {Object} loaded object
*/
Config.prototype.getConfig = function () {
// default statement
return this.get('config');
};
/**
* Set config path
*
* @param {String} path default path to use
* @return {Boolean} true if all is ok false otherwise
*/
Config.prototype.setConfigPath = function (path) {
// default statement
return this.set('base', path);
};
/**
* Return correct property from given name
*
* @param {String} name the property name
* @return {Mixed} needed data
*/
Config.prototype.get = function (name) {
// default instance
return this[name];
};
/**
* Reload config from path
*
* @param {String} base if base exists and is valid reassign base and reload
* @return {Boolean} true if load succeed false otherwise
*/
Config.prototype.reload = function (base) {
// check base before load for conditional assignation
if (_.isString(base) && !_.isEmpty(base)) {
// change base
this.base = base;
}
// return load statement
return this.load();
};
/**
* Load password schema for current configuration
*
* @return {Object} return current promise
*/
Config.prototype.loadPassport = function () {
// retreive passport schema
// this.schema = this.passport.get();
// default statement
return this.load();
};
/**
* Auto enable validators schema for given list
*
* @param {Array} items array of items to enable
* @return {Boolean} true if all is ok false otherwise
*/
Config.prototype.autoEnableValidators = function (items) {
// is valid format ?
if (!_.isArray(items) || _.isEmpty(items)) {
// warn message invalid data
this.logger.warning([ '[ Config.autoEnableValidator ] - Cannot check items.',
'Is not an array or is empty.' ].join(' '));
// invalid statement
return false;
}
// parse item and check
_.each(items, function (item) {
// schema exists ?
if (_.has(this.schemaList, item)) {
// enable succeed ?
if (!this.enableSchema(item, true)) {
// success or error ?
this.logger.error([ '[ Config.autoEnableValidator ] - Auto enable [',
item, '] failed' ].join(' '));
}
} else {
// warning
this.logger.warning([ '[ Config.autoEnableValidator ] - Cannot enable [',
item, '] schema does not exists' ].join(' '));
}
}.bind(this));
// default statement
return true;
};
/**
* Default load function, load data from all.js constant file
*
* @return {Object} return current promise
*/
Config.prototype.load = function () {
// create async process here
var deferred = Q.defer();
// any errors ?? try/catch it
try {
// default config object
var config = {};
// build pattern
var pattern = path.normalize([ this.base, this.suffix, '*.json' ].join('/'));
var penv = path.normalize([ this.base, this.suffix, [
this.env, '.json' ].join('')
].join('/'));
// get file (sync mode)
var paths = glob.sync(pattern);
// has a valid path ? no ? stop process
if (_.isEmpty(paths)) {
throw 'No config files was found. Operation aborted !';
}
// sort path
paths = _.sortBy(paths, function (p) {
var all = path.normalize([ this.base, this.suffix, 'all.json' ].join('/'));
var common = path.normalize([ this.base, this.suffix, 'common.json' ].join('/'));
var development = path.normalize([ this.base, this.suffix, 'development.json' ].join('/'));
var staging = path.normalize([ this.base, this.suffix, 'staging.json' ].join('/'));
var production = path.normalize([ this.base, this.suffix, 'production.json' ].join('/'));
// return data order
return [ p === production, p === staging,
p === development, p === common, p === all, p === p ].join('|');
}.bind(this));
// parse all and merge if no error
_.each(paths, function (path) {
// parse all file contains
var item = JSON.parse(fs.readFileSync(path, 'utf-8'));
// merge data
_.merge(config, item);
// has current env ? if test return false stop env was founded
return penv !== path;
});
// build schema
this.schema = joi.object().required().keys(this.schema).unknown(true);
// validate !!!
var result = joi.validate(config, this.schema, { abortEarly : false });
// has error
if (!_.isNull(result.error)) {
// parse details
_.each(result.error.details, function (error) {
this.logger.warning([ '[ Config.load ] - Cannot update config an error occured. Error is :',
utils.obj.inspect(error) ].join(' '));
}.bind(this));
// throw exception error occured
throw 'Config validation failed';
}
// build directory value to UPPERCASE constants
_.each(config.directory, function (dir) {
// build path
var p = _.first(_.values(dir));
// is relative path ?
if (!path.isAbsolute(p)) {
p = path.normalize([ process.cwd(), p ].join('/'));
}
// build key
var k = [ _.first(_.keys(dir)), 'directory' ].join('_').toUpperCase();
// assign
this[k] = p;
}.bind(this));
// change state
this.state = true;
// valid so assign config
this.config = result.value;
// log message
this.logger.info([ '[ Config.load ] - Success - Config file was changed with files based on :',
[ this.base, this.suffix ].join('/') ].join(' '));
// resolve with valid value
deferred.resolve(this.config);
} catch (e) {
// error message
this.logger.error([ '[ Config.load ] - an error occured during load config file. Error is :',
e ].join(' '));
// reject
deferred.reject(e);
}
// return true if all is ok
return deferred.promise;
};
// Default export
module.exports = function (l) {
// is a valid logger ?
if (_.isUndefined(l) || _.isNull(l)) {
logger.warning('[ Config.constructor ] - Invalid logger given. Use internal logger');
// assign
l = logger;
}
// default statement
return new (Config)(l);
};