# Source: lib/init/express.js

    'use strict';

    require('isomorphic-fetch');
    const os = require('os');
    const path = require('path');
    const fs = require('fs-extra');
    const semver = require('semver');
    const express = require('express');
    const Sequelize = require('sequelize');
    const EJS = require('ejs');
    const LRU = require('lru-cache');
    const responseTime = require('response-time');
    const flash = require('connect-flash');
    const bodyParser = require('body-parser');
    const methodOverride = require('method-override');
    const cookieParser = require('cookie-parser');
    const favicon = require('serve-favicon');
    const compress = require('compression');
    const morgan = require('morgan');
    const csrf = require('csurf');
    const connectLoki = require('connect-loki');
    const connectMongo = require('connect-mongo');
    const connectSql = require('connect-sequelize');
    const connectRedis = require('connect-redis');
    const session = require('express-session');
    const crypto = require('crypto');

    /**
     * configure express view rendering options
     *
     * @returns {Promise}
     */
    function configureViews() {
      return new Promise((resolve, reject) => {
        try {
          if (this.settings.express.config.trust_proxy) {
            this.app.enable('trust proxy');
          }
          this.app.set('view engine', this.settings.express.views.engine || 'ejs');
          this.app.set('views', path.resolve(this.config.app_root, 'app/views'));
          if (this.settings.express.views.lru_cache) {
            EJS.cache = LRU(this.settings.express.views.lru);
          }
          this.app.engine('html', EJS.renderFile);
          this.app.engine('ejs', EJS.renderFile);
          if (this.settings.express.views.engine !== 'ejs') {
            this.app.engine(this.settings.express.views.engine, require(this.settings.express.views.package).renderFile);
          }
          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    function configureExpress() {
      return new Promise((resolve, reject) => {
        try {
          this.app.use(responseTime(this.settings.express.response_time));
          if (this.settings.express.use_flash) {
            this.app.use(flash());
          }
          this.app.use(bodyParser.urlencoded(this.settings.express.body_parser.urlencoded));
          this.app.use(bodyParser.json(this.settings.express.body_parser.json));
          this.app.use(methodOverride());
          this.app.use(methodOverride('_method'));
          this.app.use(methodOverride('X-HTTP-Method')); // Microsoft
          this.app.use(methodOverride('X-HTTP-Method-Override')); // Google/GData
          this.app.use(methodOverride('X-Method-Override')); // IBM
          this.app.use(methodOverride(function(req, res) {
            if (req.body && typeof req.body === 'object' && '_method' in req.body) {
              // look in urlencoded POST bodies and delete it
              const method = req.body._method;
              delete req.body._method;
              return method;
            }
          }));
          this.app.use(cookieParser(this.settings.express.cookies.cookie_parser));
          this.app.use(favicon(path.resolve(this.config.app_root, 'public/favicon.png')));

          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    function customizeExpress() {
      return new Promise((resolve, reject) => {
        try {
          const startup = require(path.join(this.config.app_root, 'content/config/startup.js'));
          resolve(startup.customExpressConfiguration.call(this));
        } catch (e) {
          reject(e);
        }
      });
    }

    function staticCacheExpress() {
      return new Promise((resolve, reject) => {
        try {
          const expressStaticOptions = (this.settings.express.config.use_static_caching) ? {
            maxAge: 86400000,
          } : {};
          this.app.use(express.static(path.resolve(this.config.app_root, 'public'), expressStaticOptions));
          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    function compressExpress() {
      return new Promise((resolve, reject) => {
        try {
          if (this.settings.express.config.use_compression) {
            this.app.use(compress());
          }
          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    function morganColors(req, res) {
      const status = res.statusCode;
      let color = 32; // green

      if (status >= 500) {
        color = 31;
      } // red
      else if (status >= 400) {
        color = 33;
      } // yellow
      else if (status >= 300) {
        color = 36;
      } // cyan
      return '\x1b[' + color + 'm' + status + '\x1b[90m';
    }

    function morganLogger(req, res, next) {
      return (req, res, next) => {
        if (this.settings.express.config.skip_logging_paths.indexOf(req.path)>-1) {
          return next();
        } else {
          return morgan('app', {
            format: '\x1b[90m:remote-addr :method \x1b[37m:url\x1b[90m :colorstatus \x1b[97m:response-time ms\x1b[90m :date :referrer :user-agent\x1b[0m',
          })(req, res, next);
        }
      };
    }

    function logExpress() {
      return new Promise((resolve, reject) => {
        try {
          if (this.settings.express.config.debug) {
            morgan.token('colorstatus', morganColors);
            morgan.format('app', '\x1b[90m:remote-addr :method \x1b[37m:url\x1b[90m :colorstatus \x1b[97m:response-time ms\x1b[90m :date :referrer :user-agent\x1b[0m');
            // if (this.settings.application.status !== 'install') {
            // this.app.use(morgan('app', {
            //   format: '\x1b[90m:remote-addr :method \x1b[37m:url\x1b[90m :colorstatus \x1b[97m:response-time ms\x1b[90m :date :referrer :user-agent\x1b[0m',
            // }));
            this.app.use(morganLogger.call(this));
            // } else {
            //   app.use(morgan('app', {
            //     format: '\x1b[90m:remote-addr :method \x1b[37m:url\x1b[90m :colorstatus \x1b[97m:response-time ms\x1b[90m :date :referrer :user-agent\x1b[0m'
            //   }));
            // }
          }
          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    function skipSessions(options) {
      const { req, } = options;
      if (req.headers.authorization) {
        return true;
      } else if (req.query && req.query.skip_session && (req.query.skip_session === 'true' || req.query.skip_session === true)) {
        return true;
      } else if (req.controllerData && req.controllerData.skip_session && (req.controllerData.skip_session === 'true' || req.controllerData.skip_session === true)) {
        return true;
      } else {
        return false;
      }
    }

    function enabledSessionMiddleware(sessionMiddleware) {
      return (req, res, next) => {
        // console.log('skipSessions({ req })',skipSessions({ req }))
        if (skipSessions({ req, })) {
          return next();
        } else {
          return sessionMiddleware(req, res, next);
        }
      };
    }

    function enabledCSRFMiddleware(csrfMiddleware) {
      return (req, res, next) => {
        let skipSessionCheck = skipSessions({ req, });
        if (skipSessionCheck) {
          return next();
        } else if (Array.isArray(this.settings.express.config.skip_csrf_header)) {
          let skipCSRF = false;
          this.settings.express.config.skip_csrf_header.forEach(skipHeader => {
            if (req.headers[skipHeader]) {
              skipCSRF = true;
            }
          });
          if (skipCSRF || skipSessionCheck) {
            next();
          } else {
            return csrfMiddleware(req, res, next);
          }
        } else if (req.headers[this.settings.express.config.skip_csrf_header]) {
          return next();
        } else {
          return csrfMiddleware(req, res, next);
        }
      };
    }

    function expressSessions() {
      return new Promise((resolve, reject) => {
        try {
          if (this.settings.express.sessions.enabled) {
            const express_session_config = Object.assign({
              proxy: true,
              // name:'connect.id',
              resave: false, //true, //default
              // rolling:false, //default
              saveUninitialized: false, //true, //default
              // secret:'somescreted', //default
              // store:MemoryStore //default
              // unset:'keep' //default
              cookie: {
                expires: 604800000, //one week
                maxAge: 604800000, //one week
                secure: 'auto',
              },
            }, this.settings.express.sessions.config);
            // let store;
            switch (this.settings.express.sessions.type) {
            case 'loki':
              var LokiStore = connectLoki(session);
              express_session_config.store = new LokiStore(this.settings.express.sessions.store_settings);
              break;
            case 'mongo':
              var MongoStore = connectMongo(session);
              express_session_config.store = new MongoStore(this.settings.express.sessions.store_settings);
              break;
            case 'sql':
              var SequeliezeStore = connectSql(session);
              var { db, options, modelName, } = this.settings.express.sessions.store_settings;
              var sqldb = new Sequelize(...db);
              express_session_config.store = new SequeliezeStore(sqldb, options, modelName);
              break;
            case 'redis':
              var RedisStore = connectRedis(session);
              express_session_config.store = new RedisStore(this.settings.express.sessions.store_settings);
              break;
            default:
              reject(new Error('Invalid express session type'));
              break;
            }
            const sessionMiddleware = session(express_session_config);
            this.app.use(enabledSessionMiddleware(sessionMiddleware));
            const csrfMiddleWare = enabledCSRFMiddleware.bind(this);
            if (this.settings.express.config.csrf) {
              this.app.use(csrfMiddleWare(csrf()));
            }
            resolve(true);
          } else {
            resolve(true);
          }
        } catch (e) {
          reject(e);
        }
      });
    }

    function useCSRFMiddleware(req, res, next) {
      res.locals.token = (this.settings.express.config.csrf && req.csrfToken) ?
        req.csrfToken() :
        '';
      next();
    }

    function useLocalsMiddleware(req, res, next) {
      res.locals.periodic_addons = {
        additionalHeadHTML: {},
        additionalFooterHTML: {},
        additionalPreContentHTML: {},
        additionalPostContentHTML: {},
        ['x-forwarded-for']: req.headers['x-forwarded-for'],
        remoteAddress: req.connection.remoteAddress,
      };
      res.locals.isLoggedIn = (req.user && Object.keys(req.user).length) ? true : false;
      next();
    }

    function worker_index(ip, len) {
      const hash = crypto.createHash('sha256');
      hash.update(ip);
      let hex = hash.digest('hex').substring(0, 8);
      return parseInt(hex, 16).toString(10) % len;
    }

    function assignWorkersMiddleware(req, res, next) {
      if (this.settings.application.cluster_process && (this.settings.application.server.socketio ||this.settings.application.use_sticky_sessions)) {
        let worker = this.workers[ worker_index(req._remoteAddress, this.numWorkers)];
        worker.send('sticky-session:connection', req.connection.server);
      }
      next();
    }

    function expressLocals() {
      return new Promise((resolve, reject) => {
        try {
          this.app.locals.viewHelper = this.utilities.viewHelper;
          const useCSRFMiddlewareFunc = useCSRFMiddleware.bind(this);
          const useLocalsMiddlewareFunc = useLocalsMiddleware.bind(this);
          const assignWorkersMiddlewareFunc = assignWorkersMiddleware.bind(this);
          let core_data_list = [];
          for (let key of this.datas.keys()) {
            if (key !== 'configuration' && key !== 'extension') {
              core_data_list.push(key);
            }
          }
          this.app.locals.core_data_list = core_data_list.reduce(this.utilities.routing.splitModelNameReducer, {});
          this.app.locals.appenvironment = this.environment;
          this.app.locals.appname = this.settings.name;
          this.app.locals.additionalHTMLFunctions = [];
          this.app.use(useCSRFMiddlewareFunc);
          this.app.use(useLocalsMiddlewareFunc);
          this.app.use(assignWorkersMiddlewareFunc); // for clustering and sockets to work together
          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    function expressRouting() {
      return new Promise((resolve, reject) => {
        try {
          this.app.locals.page_data = Object.assign({}, this.settings.express.views.page_data, this.app.locals.page_data);
          //assign container routers
          //assign extension routers
          //assign data routers
          this.routers.forEach((value /*, key*/ ) => {
            if (value.type === 'container') {
              this.app.use(value.router);
            } else if (value.type === 'extension') {
              this.app.use(value.router);
            } else if (value.type === 'data') {
              this.app.use(getDataRoute.call(this, value.router_base),
                value.router);
            }
            // else
          });
          this.app.get('*', (req, res, next) => {
            const skippable_routes = this.settings.express.views.skippable_routes;
            const skip404 = skippable_routes.reduce((result, route) => {
              if (req.path.indexOf(route) >= 0) {
                result.push(route);
              }
              return result;
            }, []);

            if (skip404.length) {
              next();
            } else {
              const viewtemplate = 'home/error404';
              const viewdata = Object.assign({}, {
                periodic: {
                  appname: this.settings.name,
                  page_data: {
                    version: this.settings.application.version,
                  },
                },
                error: new Error('page not found'),
              }, this.app.locals);
              res.status(404);
              this.core.controller.renderView(req, res, viewtemplate, viewdata);
            }
          });
          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    function getDataRoute(data_route) {
      return this.utilities.routing.route_prefix(path.join(this.settings.express.routing.data, data_route.replace(/_/gi, '/')));
    }

    function displayOutOfDatePeriodic(options) {
      const { latestPeriodicVersion, } = options;
      console.log('\u0007');
      this.logger.debug(`
      ======================================================
      |                                                    |
      |     Your instance of Periodic is out of date.      |
      |   Your Version: ${this.settings.application.version}, Current Version: ${latestPeriodicVersion}    |
      |                                                    |
      ======================================================
      `);
    }

    function handleLatestPeriodicVersionCheckError(error) {
      this.logger.warn('Could not check latest version of Periodic', error);
    }

    function extVersionReducer(result, key) {
      result[key.name] = key.version;
      return result;
    }

    function checkOutdatedExtensionVersions() {
      const extensionNamesToFetch = Array.from(this.extensions.keys()).filter(ext => ext.indexOf('@') < 0);
      const extensions = Array.from(this.extensions.values()).filter(ext => ext.name.indexOf('@') < 0);
      const semverRegEx = RegExp(/[^0-9.]/g);

      const registryExtensionFetchs = extensionNamesToFetch.map(extname => {
        return new Promise((resolve, reject) => {
          fetch(`https://registry.npmjs.org/${extname}`, {
            headers: {
              Accept: 'application/json',
            },
          })
            .then(this.utilities.fetchUtils.checkStatus)
            .then(res => res.json())
            .then(resolve)
            .catch(reject);
        });
      });
      const outOfDateExtensions = [];
      fs.readJson(path.join(this.config.app_root, 'package.json')).then(packageJSON => {
        const extensionVersionObj = Array.from(this.extensions.values()).reduce(extVersionReducer, {});
        Object.keys(extensionVersionObj).forEach(extVer => {
          if (packageJSON.dependencies[extVer]) {
            const packageJsonExtVer = packageJSON.dependencies[extVer].replace(semverRegEx, '').split('.').slice(0, 3).join('.');
            const extJsonVer = extensionVersionObj[extVer];
            if (this.utilities.fetchUtils.isVersionUpToDate(packageJsonExtVer, extJsonVer) === false) {
              this.logger.error(`${extVer}@${packageJsonExtVer} in package.json is out of date with version ${extJsonVer} in the configuration database`);
            }
          } else {
            this.logger.error(`${extVer}@${extensionVersionObj[extVer]} from the configuration database in missing in your package.json file`);
          }
        });
        return Promise.all(registryExtensionFetchs);
      })
        .then(extensionsFromNPM => {
          extensionsFromNPM.forEach((extFromNPM, i) => {
            const latestExtVersion = extFromNPM['dist-tags'].latest;
            if (this.utilities.fetchUtils.isVersionUpToDate(extensions[i].version, latestExtVersion) === false) {
              outOfDateExtensions.push({
                name: extensions[i].name,
                installed: extensions[i].version,
                latest: latestExtVersion,
              });
            }
          });
          if (outOfDateExtensions.length) {
            this.logger.verbose('Your current extensions are out of date: ', outOfDateExtensions);
          }
        })
        .catch(this.logger.error);
    }

    function checkLatestPeriodicVersion() {
      fetch('https://registry.npmjs.org/periodicjs', {
        headers: {
          Accept: 'application/json',
        },
      })
        .then(this.utilities.fetchUtils.checkStatus)
        .then(res => res.json())
        .then(res => {
          const latestPeriodicVersion = res['dist-tags'].latest;
          if (this.utilities.fetchUtils.isVersionUpToDate(this.settings.application.version, latestPeriodicVersion)) {
            if (this.config.debug) {
              this.logger.debug(`Your version of Periodic[${this.settings.application.version}] is up to date with the current version [${latestPeriodicVersion}]`);
            }
          } else {
            displayOutOfDatePeriodic.call(this, { latestPeriodicVersion, });
          }
        })
        .catch(handleLatestPeriodicVersionCheckError.bind(this));
    }

    function expressStatus() {
      return new Promise((resolve, reject) => {
        try {
          if (this.config.debug) {
            this.logger.debug(`Running in environment: ${this.environment}`);
          }
          if (this.settings.application.check_for_updates) {
            checkLatestPeriodicVersion.call(this);
          }
          if (this.settings.application.check_for_outdated_extensions) {
            checkOutdatedExtensionVersions.call(this);
          }
          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    const errorLogMiddleware = function errorLogMiddleware(err, req, res, next) {
      // console.log('err', err, 'next', next);
      var userdata = {};
      if (req && req.user && req.user.email) {
        userdata = {
          email: req.user.email,
          username: req.user.username,
          firstname: req.user.firstname,
          lastname: req.user.lastname,
        };
      }
      this.logger.error(err.message, err.stack, {
        err: err,
        ipinfo: {
          date: new Date(),
          'x-forwarded-for': req.headers['x-forwarded-for'],
          remoteAddress: req.connection.remoteAddress,
          originalUrl: req.originalUrl,
          headerHost: req.headers.host,
          userAgent: req.headers['user-agent'],
          referer: req.headers.referer,
          user: userdata,
          osHostname: os.hostname(),
        },
      });
      // logger.error(err.stack);
      next(err);
    };

    const catchAllErrorMiddleware = function catchAllErrorMiddleware(err, req, res, next) {
      if (!err) {
        next();
      } else if (req.query.format === 'json' || req.is('json') || req.is('application/json')) {
        res.status(500);
        res.send({
          result: 'error',
          data: {
            error:err.toString(),
          },
          error: err,
        });
      } else {
        res.status(500);
        res.render(this.settings.express.views.custom_error_view || 'home/error500', {
          user: req.user,
          message: err.message,
          error: err,
        });
      }
    };

    function expressErrors() {
      return new Promise((resolve, reject) => {
        try {
          const catchAllErrorMiddlewareFunc = catchAllErrorMiddleware.bind(this);
          const errorLogMiddlewareFunc = errorLogMiddleware.bind(this);
          this.app.use(errorLogMiddlewareFunc);
          this.app.use(catchAllErrorMiddlewareFunc);
          resolve(true);
        } catch (e) {
          reject(e);
        }
      });
    }

    /**
     * sets the runtime environment correctly
     *
     * @returns {Promise} configRuntimeEnvironment sets up application config db
     */
    function initializeExpress() {
      return Promise.all([
        configureViews.call(this),
        configureExpress.call(this),
        customizeExpress.call(this),
        staticCacheExpress.call(this),
        compressExpress.call(this),
        logExpress.call(this),
        expressSessions.call(this),
        expressLocals.call(this),
        expressRouting.call(this),
        expressStatus.call(this),
        expressErrors.call(this),
      ]);
    }
    module.exports = {
      initializeExpress,
      configureViews,
      configureExpress,
      customizeExpress,
      staticCacheExpress,
      compressExpress,
      logExpress,
      expressSessions,
      expressLocals,
      expressRouting,
      expressStatus,
      expressErrors,
      morganColors,
      morganLogger,
      errorLogMiddleware,
      catchAllErrorMiddleware,
      checkLatestPeriodicVersion,
      useLocalsMiddleware,
      useCSRFMiddleware,
      skipSessions,
      enabledCSRFMiddleware,
      enabledSessionMiddleware,
      handleLatestPeriodicVersionCheckError,
      displayOutOfDatePeriodic,
    };