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,
};