'use strict';
var path = require('path'),
// request = require('superagent'),
async = require('async'),
fs = require('fs-extra'),
// npm = require('npm'),
os = require('os'),
// semver = require('semver'),
str2json = require('string-to-json'),
merge = require('utils-merge'),
CronJob = require('cron').CronJob,
admin_ext_settings,
CoreUtilities,
CoreController,
CoreExtension,
CoreMailer,
appSettings,
dbSettings,
mongoose,
AppDBSetting,
appenvironment,
logger,
restartfile = path.join(process.cwd(), '/content/config/restart.json'),
changedemailtemplate,
io = global.io;
/**
* send setting update email
* @param {object} options - contains email options and nodemailer transport
* @param {Function} callbackk async callback
*/
var sendSettingEmail = function (options, callback) {
if (appSettings.adminnotificationemail_bcc) {
options.adminnotificationemail_bcc = appSettings.adminnotificationemail_bcc;
}
console.log({
to: appSettings.adminnotificationemail,
cc: options.user.email,
bcc: options.adminnotificationemail_bcc,
from: appSettings.serverfromemail,
});
CoreMailer.sendEmail({
appenvironment: appenvironment,
to: appSettings.adminnotificationemail,
cc: options.user.email,
bcc: options.adminnotificationemail_bcc,
replyTo: options.replyTo,
generatetextemail: true,
// replyTo: 'Promise Financial [Do Not Reply] <no-reply@promisefin.com>',
from: appSettings.serverfromemail,
subject: (options.subject) ? options.subject : appSettings.name + ' -Admin Email Notification',
// bcc: ilsConfig.emailtosalesforce,
emailtemplatefilepath: options.emailtemplate,
emailtemplatedata: options.emaildata
}, function (err, emailstatus) {
if (err) {
CoreController.logError({
err: err,
req: options.req
});
}
else {
logger.debug('settings change email sent', emailstatus);
}
if (callback) {
callback(err, emailstatus);
}
});
};
/**
* restarts application response handler and send notification email
* @param {object} req
* @param {object} res
* @return {object} reponds with an error page or sends user to authenicated in resource
*/
var restart_app = function (req, res) {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/settings',
responseData: {
result: 'success',
data: 'restarted'
}
});
var d = new Date();
sendSettingEmail({
user: req.user,
emaildata: {
user: req.user,
hostname: req.headers.host,
appname: appSettings.name,
appenvironment: appSettings.application.environment,
appport: appSettings.application.port,
settingmessage: 'Your application was restarted from the admin interface - ' + d,
},
subject: appSettings.name + '[env:' + appSettings.application.environment + '] Application Restart Notification',
emailtemplate: changedemailtemplate,
}, function (err, status) {
CoreUtilities.restart_app({
restartfile: restartfile
});
if (err) {
logger.error(err);
}
else {
console.info('email status', status);
}
});
};
/**
* placeholder response for updating application
* @param {object} req
* @param {object} res
* @return {object} reponds with an error page or sends user to authenicated in resource
*/
var update_app = function (req, res) {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/settings',
responseData: {
result: 'success',
data: 'restarted'
}
});
};
/**
* load the extensions configuration files from the installed config folder in content/config/extensions/[extension]/[config files]
* @param {object} req
* @param {object} res
* @param {Function} next
*/
var load_extension_settings = function (req, res, next) {
var extname = req.params.id,
ext_default_config_file_path = path.resolve(process.cwd(), 'node_modules/', extname, 'config'),
ext_installed_config_file_path = path.resolve(process.cwd(), 'content/config/extensions/', extname);
/**
* load config files into array of filejson
* @param {Function} callback async callbackk
* @return {array} array of file data objects
*/
var loadconfigfiles = function (callback) {
var configfilesJSON = [];
fs.readdir(ext_installed_config_file_path, function (err, configfiles) {
if (configfiles && configfiles.length > 0) {
async.each(configfiles, function (configFile, cb) {
if (path.extname(configFile) === '.json') {
fs.readJson(path.resolve(ext_installed_config_file_path, configFile), function (err, data) {
if (err) {
cb(err);
}
else {
configfilesJSON.push({
name: configFile,
filedata: data
});
cb(null);
}
});
}
else {
fs.readFile(path.resolve(ext_installed_config_file_path, configFile), 'utf8', function (err, data) {
if (err) {
cb(err);
}
else {
configfilesJSON.push({
name: configFile,
filedata: data
});
cb(null);
}
});
}
}, function (err) {
if (err) {
callback(err);
}
else {
callback(null, configfilesJSON);
}
});
}
else {
callback(null, configfilesJSON);
}
});
};
req.controllerData = (req.controllerData) ? req.controllerData : {};
async.waterfall([
function (cb) {
cb(null, {
ext_default_config_file_path: ext_default_config_file_path,
ext_installed_config_file_path: ext_installed_config_file_path
});
},
CoreExtension.getExtensionConfigFiles,
CoreExtension.copyMissingConfigFiles,
loadconfigfiles
],
function (err, result) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else {
// console.log('err', err, 'result', result);
req.controllerData.extconfigfiles = result;
next();
}
});
};
/**
* save data from theme page post
* @param {object} req
* @param {object} res
*/
var update_theme_filedata = function (req, res) {
var updateThemeFileData = CoreUtilities.removeEmptyObjectValues(req.body),
themeconffile = path.resolve(process.cwd(), 'content/themes/', updateThemeFileData.themename, updateThemeFileData.filename),
jsonParseError;
delete updateThemeFileData._csrf;
try {
updateThemeFileData.filedata = (path.extname(updateThemeFileData.filename) === '.json') ? JSON.parse(updateThemeFileData.filedata) : updateThemeFileData.filedata;
}
catch (e) {
jsonParseError = e;
}
if (path.extname(updateThemeFileData.filename) === '.json') {
logger.warn('write json');
fs.writeJson(themeconffile, updateThemeFileData.filedata, function (err) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else if (jsonParseError) {
CoreController.handleDocumentQueryErrorResponse({
err: 'JSON Parse Error: ' + jsonParseError,
res: res,
req: req
});
}
else {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/theme/' + updateThemeFileData.themename,
responseData: {
result: 'success',
data: 'updated theme file and restarted application'
},
callback: function () {
CoreUtilities.restart_app({
restartfile: restartfile
});
}
});
}
});
}
else {
logger.warn('write file');
fs.outputFile(themeconffile, updateThemeFileData.filedata, function (err) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else if (jsonParseError) {
CoreController.handleDocumentQueryErrorResponse({
err: 'JSON Parse Error: ' + jsonParseError,
res: res,
req: req
});
}
else {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/theme/' + updateThemeFileData.themename,
responseData: {
result: 'success',
data: 'updated theme file and restarted application'
},
callback: function () {
CoreUtilities.restart_app({
restartfile: restartfile
});
}
});
}
});
}
};
/**
* save data from config page post
* @param {object} req
* @param {object} res
*/
var update_ext_filedata = function (req, res) {
var updateConfigFileData = CoreUtilities.removeEmptyObjectValues(req.body),
extconffile = path.resolve(process.cwd(), 'content/config/extensions/', updateConfigFileData.extname, updateConfigFileData.filename),
jsonParseError;
delete updateConfigFileData._csrf;
try {
updateConfigFileData.filedata = (path.extname(updateConfigFileData.filename) === '.json') ? JSON.parse(updateConfigFileData.filedata) : updateConfigFileData.filedata;
}
catch (e) {
jsonParseError = e;
}
if (path.extname(updateConfigFileData.filename) === '.json') {
logger.warn('write json');
fs.writeJson(extconffile, updateConfigFileData.filedata, function (err) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else if (jsonParseError) {
CoreController.handleDocumentQueryErrorResponse({
err: 'JSON Parse Error: ' + jsonParseError,
res: res,
req: req
});
}
else {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/extension/' + updateConfigFileData.extname,
responseData: {
result: 'success',
data: 'updated config file and restarted application'
},
callback: function () {
CoreUtilities.restart_app({
restartfile: restartfile
});
}
});
}
});
}
else {
logger.warn('write file');
fs.outputFile(extconffile, updateConfigFileData.filedata, function (err) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else if (jsonParseError) {
CoreController.handleDocumentQueryErrorResponse({
err: 'JSON Parse Error: ' + jsonParseError,
res: res,
req: req
});
}
else {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/extension/' + updateConfigFileData.extname,
responseData: {
result: 'success',
data: 'updated config file and restarted application'
},
callback: function () {
CoreUtilities.restart_app({
restartfile: restartfile
});
}
});
}
});
}
};
/**
* load app configuration information
* @param {object} req
* @param {object} res
* @param {object} next async callback
* @return {object} reponds with an error page or sends user to authenicated in resource
*/
var load_app_settings = function (req, res, next) {
req.controllerData = (req.controllerData) ? req.controllerData : {};
var appsettings = {
readonly: {
application: appSettings.application,
cookies: appSettings.cookies,
crsf: appSettings.crsf,
database: dbSettings.url,
expressCompression: appSettings.expressCompression,
theme: appSettings.theme,
templatefileextension: appSettings.templatefileextension,
templateengine: appSettings.templateengine,
sessions: appSettings.sessions,
node_modules: appSettings.node_modules,
version: appSettings.version,
},
environment: appSettings.application.environment,
configuration: {
adminnotificationemail: appSettings.adminnotificationemail,
serverfromemail: appSettings.serverfromemail,
debug: appSettings.debug,
periodic_cache_status: appSettings.periodic_cache_status,
homepage: appSettings.homepage,
name: appSettings.name,
}
};
async.parallel({
env_config: function (asynccb) {
fs.readJson(path.join(process.cwd(), 'content/config/environment', appenvironment + '.json'), asynccb);
},
default_config: function (asynccb) {
fs.readJson(path.join(process.cwd(), 'content/config/environment/default.json'), asynccb);
},
global_config: function (asynccb) {
fs.readJson(path.join(process.cwd(), 'content/config/config.json'), asynccb);
}
}, function (err, results) {
if (err) {
next(err);
}
else {
var configReturn = {};
for (var key in appSettings) {
configReturn[key] = appSettings[key];
}
req.controllerData.config = results;
req.controllerData.config.app_config = configReturn;
req.controllerData.config.app_config.themeSettings = '[theme config]';
req.controllerData.config.app_config.extconf = '[extension config]';
req.controllerData.appsettings = appsettings;
next();
}
});
};
/**
* load theme configuration information
* @param {object} req
* @param {object} res
* @param {object} next async callback
* @return {object} reponds with an error page or sends user to authenicated in resource
*/
var load_theme_settings = function (req, res, next) {
var themesettings = {
readonly: {},
environment: appSettings.application.environment,
configuration: {}
};
if (appSettings.themeSettings && appSettings.themeSettings.settings) {
themesettings.configuration = appSettings.themeSettings.settings[appSettings.application.environment];
themesettings.readonly = {
name: appSettings.themeSettings.name,
periodicCompatibility: appSettings.themeSettings.periodicCompatibility,
author: appSettings.themeSettings.author,
url: appSettings.themeSettings.url,
templatefileextension: appSettings.templatefileextension,
templateengine: appSettings.templateengine,
themepath: appSettings.themepath
};
}
req.controllerData = (req.controllerData) ? req.controllerData : {};
req.controllerData.themesettings = themesettings;
next();
};
/**
* form upload handler to update app settings, and sends notification email
* @param {object} req
* @param {object} res
* @param {object} next async callback
* @return {object} reponds with an error page or sends user to authenicated in resource
*/
var update_app_settings = function (req, res) {
var updatedAppSettings = CoreUtilities.removeEmptyObjectValues(req.body),
appsettingsfile = path.join(process.cwd(), 'content/config/environment/' + appSettings.application.environment + '.json');
updatedAppSettings = CoreUtilities.replaceBooleanStringObjectValues(updatedAppSettings);
delete updatedAppSettings._csrf;
fs.ensureFileSync(appsettingsfile);
fs.readJson(appsettingsfile, function (err, appconfig) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else {
updatedAppSettings = str2json.convert(updatedAppSettings);
var originalconfig = appconfig || {},
mergedconfig = merge(originalconfig, updatedAppSettings);
async.series({
update_app_setting: function (asynccb) {
fs.writeJson(appsettingsfile, mergedconfig, asynccb);
},
send_email_notification: function (asynccb) {
sendSettingEmail({
user: req.user,
req: req,
emaildata: {
user: req.user,
hostname: req.headers.host,
appname: appSettings.name,
appenvironment: appSettings.application.environment,
appport: appSettings.application.port,
settingmessage: '<p>Your application was configuration was changed from the admin interface - ' + new Date() + '</p><p><pre>' + JSON.stringify(mergedconfig, null, '\t') + '</pre></p>',
},
subject: appSettings.name + '[env:' + appSettings.application.environment + '] Application Configuration Change Notification',
emailtemplate: changedemailtemplate,
}, asynccb);
},
send_server_response: function (asynccb) {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/settings',
responseData: {
result: 'success',
data: 'app config updated'
},
callback: asynccb
});
}
}, function (err, results) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else {
logger.debug('update_app_settings async series results', results);
CoreUtilities.restart_app({
restartfile: restartfile
});
}
});
}
});
};
/**
* form upload handler to update theme settings, and sends notification email
* @param {object} req
* @param {object} res
* @param {object} next async callback
* @return {object} reponds with an error page or sends user to authenicated in resource
*/
var update_theme_settings = function (req, res) {
var updatedThemeSettings = JSON.parse(req.body['themesettings-codemirror']), //CoreUtilities.removeEmptyObjectValues(req.body),
themesettingsfile = path.join(process.cwd(), 'content/config/themes', appSettings.theme, 'periodicjs.theme.json'),
originalsettings,
mergedsettings,
newthemeconfig;
updatedThemeSettings = CoreUtilities.replaceBooleanStringObjectValues(updatedThemeSettings);
delete updatedThemeSettings._csrf;
async.series({
read_theme_files: function (asynccb) {
fs.readJson(themesettingsfile, function (err, themeconfig) {
if (err) {
asynccb(err);
}
else {
// updatedThemeSettings = str2json.convert(updatedThemeSettings);
originalsettings = themeconfig.settings[appenvironment];
mergedsettings = merge(originalsettings, updatedThemeSettings);
newthemeconfig = themeconfig;
newthemeconfig.settings[appSettings.application.environment] = mergedsettings;
asynccb(null, 'updated with new theme settings');
}
});
},
write_new_settings: function (asynccb) {
fs.writeJson(themesettingsfile, newthemeconfig, {
spaces: 2
}, function (err) {
asynccb(err, 'saved new theme settings');
});
},
send_server_response: function (asynccb) {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/settings',
responseData: {
result: 'success',
data: 'theme config updated'
},
callback: asynccb
});
},
send_email_notification: function (asynccb) {
sendSettingEmail({
req: req,
user: req.user,
emaildata: {
user: req.user,
hostname: req.headers.host,
appname: appSettings.name,
appenvironment: appenvironment,
appport: appSettings.application.port,
settingmessage: '<p>Your theme [' + appenvironment + '] configuration was changed from the admin interface - ' + new Date() + '</p><p><pre>' + JSON.stringify(newthemeconfig.settings[appenvironment], null, '\t') + '</pre></p>',
},
subject: appSettings.name + '[env:' + appenvironment + '] Application Theme Setting Change Notification',
emailtemplate: changedemailtemplate,
}, asynccb);
}
},
function (err, results) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else {
logger.debug('update_theme_settings async results', results);
CoreUtilities.restart_app({
restartfile: restartfile
});
}
});
};
/**
* form upload handler to update theme settings, and sends notification email
* @param {object} req
* @param {object} res
* @param {object} next async callback
* @return {object} reponds with an error page or sends user to authenicated in resource
*/
var update_config_json_files = function (req, res) {
var bodyjson,
configfilejsonpath,
configname;
if (req.body['defaultconfig-codemirror']) {
bodyjson = JSON.parse(req.body['defaultconfig-codemirror']);
configfilejsonpath = path.join(process.cwd(), '/content/config/environment/default.json');
configname = 'default.json';
}
else if (req.body['envconfig-codemirror']) {
bodyjson = JSON.parse(req.body['envconfig-codemirror']);
configfilejsonpath = path.join(process.cwd(), '/content/config/environment/' + appenvironment + '.json');
configname = appenvironment + '.json';
}
else if (req.body['globalconfig-codemirror']) {
bodyjson = JSON.parse(req.body['globalconfig-codemirror']);
configfilejsonpath = path.join(process.cwd(), '/content/config/config.json');
configname = 'config.json';
}
async.series({
write_new_settings: function (asynccb) {
fs.writeJson(configfilejsonpath, bodyjson, {
spaces: 2
}, function (err) {
asynccb(err, 'saved new theme settings');
});
},
send_server_response: function (asynccb) {
CoreController.handleDocumentQueryRender({
req: req,
res: res,
redirecturl: '/p-admin/settings',
responseData: {
result: 'success',
data: configname + ' updated'
},
callback: asynccb
});
},
send_email_notification: function (asynccb) {
if (bodyjson.cookies && bodyjson.cookies.cookieParser) {
bodyjson.cookies.cookieParser = '[redacted]';
}
if (bodyjson.session_secret) {
bodyjson.session_secret = '[redacted]';
}
sendSettingEmail({
req: req,
user: req.user,
emaildata: {
user: req.user,
hostname: req.headers.host,
appname: appSettings.name,
appenvironment: appenvironment,
appport: appSettings.application.port,
settingmessage: '<p>Your ' + configname + ' configuration was changed from the admin interface - ' + new Date() + '</p><p><pre>' + JSON.stringify(bodyjson, null, '\t') + '</pre></p>',
},
subject: appSettings.name + '[env:' + appenvironment + '] ' + configname + ' Change Notification',
emailtemplate: changedemailtemplate,
}, asynccb);
}
},
function (err, results) {
if (err) {
CoreController.handleDocumentQueryErrorResponse({
err: err,
res: res,
req: req
});
}
else {
logger.debug('update_theme_settings async results', results);
CoreUtilities.restart_app({
restartfile: restartfile
});
}
});
};
var send_setting_server_callback = function (options) {
try {
if (io && io.engine) {
io.sockets.emit('server_callback', {
functionName: options.functionName,
functionData: options.functionData
});
}
}
catch (e) {
logger.error('asyncadmin - send_server_callback e', e);
}
};
var checkOutdatedModulesAndPeriodic = function (options, callback) {
var send_outdated_emails = (options) ? options.send_outdated_emails : (admin_ext_settings && admin_ext_settings.settings) ? admin_ext_settings.settings.send_cron_check_email : true;
var asyncadmin_outdated_log_file_path = path.resolve(process.cwd(), 'content/config/outdated_log.json');
if (send_outdated_emails === undefined) {
send_outdated_emails = true;
}
fs.readJSON(asyncadmin_outdated_log_file_path, function (err, result) {
if (err) {
logger.error(err);
if (callback) {
callback(err);
}
}
else if (result.calculate_outdated_versions && Object.keys(result.calculate_outdated_versions).length > 0) {
if (callback) {
callback(null, result);
}
logger.warn('asyncadmin - WARNING: Your Instance is out of date', result.calculate_outdated_versions);
var alerthtml = '<ul>';
for (var key in result.calculate_outdated_versions) {
var ext = result.calculate_outdated_versions[key];
alerthtml += '<li>' + ext.name + ': current(' + ext.latest_version + '), installed(' + ext.installed_version + ')</li>';
}
alerthtml += '<ul>';
if (send_outdated_emails) {
sendSettingEmail({
// req: {},
user: {
username: 'application-cron',
email: appSettings.adminnotificationemail
},
emaildata: {
user: {
username: 'application-cron',
email: appSettings.adminnotificationemail
},
hostname: appSettings.homepage,
appname: appSettings.name,
appenvironment: appenvironment,
appport: appSettings.application.port,
settingmessage: '<p>Your ' + appSettings.name + ' application dependencies are outdated - ' + new Date() + '</p><div>' + alerthtml + '</div>',
},
subject: appSettings.name + '[env:' + appenvironment + ' - ' + os.hostname() + '] Dependency warning notification',
emailtemplate: changedemailtemplate,
}, function () {});
}
send_setting_server_callback({
functionName: 'showServerModal',
functionData: '<div class="ts-text-xl"><span class="ts-text-error-color">Node (' + os.hostname() + ') dependencies outdated warning</span></div><div>' + alerthtml + '</div>'
});
}
});
};
var get_outdated_modules = function (req, res, next) {
req.controllerData = (req.controllerData) ? req.controllerData : {};
checkOutdatedModulesAndPeriodic({}, function (err, outdated_modules) {
if (err) {
next(err);
}
else {
req.controllerData.outdated_modules = outdated_modules;
next();
}
});
};
var useCronTasks = function () {
try {
var crontime_to_use = (admin_ext_settings && admin_ext_settings.settings && admin_ext_settings.settings.check_dependency_cron) ? admin_ext_settings.settings.check_dependency_cron : '00 00 06 * * 1-5',
check_outdated_dependencies = new CronJob({
cronTime: crontime_to_use,
onTick: checkOutdatedModulesAndPeriodic,
onComplete: function () {} //,
// start: true
});
check_outdated_dependencies.start();
}
catch (e) {
logger.error('setupLoanCronTasks e', e);
}
};
/**
* settings controller
* @module settingsController
* @{@link https://github.com/typesettin/periodicjs.ext.admin}
* @author Yaw Joseph Etse
* @copyright Copyright (c) 2014 Typesettin. All rights reserved.
* @license MIT
* @requires module:async
* @requires module:path
* @requires module:string-to-json
* @requires module:utils-merge
* @requires module:ejs
* @requires module:periodicjs.core.utilities
* @requires module:periodicjs.core.controller
* @requires module:periodicjs.core.mailer
* @param {object} resources variable injection from current periodic instance with references to the active logger and mongo session
* @return {object} settings
*/
var controller = function (resources) {
logger = resources.logger;
mongoose = resources.mongoose;
appSettings = resources.settings;
dbSettings = resources.db;
CoreController = resources.core.controller;
CoreUtilities = resources.core.utilities;
CoreExtension = resources.core.extension;
CoreMailer = resources.core.mailer;
AppDBSetting = mongoose.model('Setting');
appenvironment = appSettings.application.environment;
admin_ext_settings = resources.app.controller.extension.asyncadmin.adminExtSettings;
CoreController.getPluginViewDefaultTemplate({
viewname: 'p-admin/email/settings/notification',
themefileext: appSettings.templatefileextension
},
function (err, templatepath) {
if (templatepath === 'p-admin/email/settings/notification') {
templatepath = path.resolve(process.cwd(), 'node_modules/periodicjs.ext.asyncadmin/views', templatepath + '.' + appSettings.templatefileextension);
}
changedemailtemplate = templatepath;
}
);
checkOutdatedModulesAndPeriodic();
useCronTasks();
return {
load_extension_settings: load_extension_settings,
update_config_json_files: update_config_json_files,
update_ext_filedata: update_ext_filedata,
update_theme_filedata: update_theme_filedata,
load_app_settings: load_app_settings,
load_theme_settings: load_theme_settings,
restart_app: restart_app,
update_app: update_app,
update_theme_settings: update_theme_settings,
update_app_settings: update_app_settings,
get_outdated_modules: get_outdated_modules
};
};
module.exports = controller;