UNPKG

periodicjs.core.controller

Version:
1,456 lines (1,383 loc) 118 kB
/* * periodic * http://github.com/typesettin/periodic * * Copyright (c) 2014 Yaw Joseph Etse. All rights reserved. */ 'use strict'; const xss = require('xss'); var fs = require('fs-extra'), path = require('path'), os = require('os'), async = require('async'), bcrypt = require('bcrypt'), str2json = require('string-to-json'), Utilities = require('periodicjs.core.utilities'), CoreMailer = require('periodicjs.core.mailer'), objectComparison = require('objectcompare'), str2json = require('string-to-json'), pluralize = require('pluralize'), moment = require('moment'), merge = require('utils-merge'), Bluebird = require('bluebird'), mongoId = require('valid-objectid'), appenvironment, CoreUtilities, Item, Category, Tag, User, Userrole, Contenttypes, useCache = function() { // console.log('useCache global.CoreCache.status',global.CoreCache.status); return (global.CoreCache && global.CoreCache.status === 'active'); }, useViewCache = function() { // console.log('useViewCache global.CoreCache.options.view_cache_status',global.CoreCache.options.view_cache_status); return (global.CoreCache && global.CoreCache.status === 'active' && global.CoreCache.options.view_cache_status === 'active'); }, useDataCache = function() { // console.log('useDataCache global.CoreCache.options.data_cache_status',global.CoreCache.options.data_cache_status); return (global.CoreCache && global.CoreCache.status === 'active' && global.CoreCache.options.data_cache_status === 'active'); }, html_xss_default_whitelist = Object.assign(xss.whiteList, { onIgnoreTagAttr: function(tag, name, value, isWhiteAttr) { if ((name.substr(0, 5) === 'data-')) { // escape its value using built-in escapeAttrValue function return name + '="' + xss.escapeAttrValue(value) + '"'; } } }, { onTagAttr: function(tag, name, value, isWhiteAttr) { if (['img', 'audio', 'video'].indexOf(tag) !== -1 && name.toLowerCase() === 'src') { return name + '="' + (value) + '"'; } } }), logger, appSettings, theme, applocals, controllerOptions, mongoose, periodicResources; /** * A core constructor that provides numerous controller helper functions. * @{@link https://github.com/typesettin/periodicjs.core.controller} * @author Yaw Joseph Etse * @constructor * @copyright Copyright (c) 2014 Typesettin. All rights reserved. * @license MIT * @requires module:fs-extra * @requires module:path * @requires module:periodicjs.core.utilities * @param {object} resources variable injection from resources from instantiated periodic express app */ var Controller = function(resources, options) { periodicResources = resources; logger = resources.logger; appSettings = resources.settings; theme = resources.settings.theme; controllerOptions = options; mongoose = resources.mongoose; appenvironment = appSettings.application.environment; Item = mongoose.model('Item'); Contenttypes = mongoose.model('Contenttype'); Category = mongoose.model('Category'); Tag = mongoose.model('Tag'); User = mongoose.model('User'); Userrole = mongoose.model('Userrole'); pluralize.addIrregularRule('data', 'datas'); if (resources.app && resources.app.locals) { applocals = resources.app.locals; } CoreUtilities = new Utilities(resources); }; String.prototype.capitalizeFirstLetter = function() { return this.charAt(0).toUpperCase() + this.slice(1); }; var jsonCheck = function(req) { return (req.query.format === 'json' || req.params.ext === 'json' || path.extname(req.originalUrl) === '.json' || req.is('json') || req.params.callback || req.is('application/json')); }; var formatJsonResponse = function(data) { return { result: 'success', data: { doc: data } }; }; /** * Gets the path to the view file specified, first look at custom theme views, then extension views, then default views * @param {object} options extname, themename, themefileext - support custom theme files * @param {Function} callback async callback * @return {Function} async callback(err,viewname) */ Controller.prototype.getPluginViewDefaultTemplate = function(options, callback) { var extname = options.extname || '', themename = theme, viewname = options.viewname, themefileext = options.themefileext || options.viewfileext, themetemplatefile, exttemplatefile; themetemplatefile = (themename && themefileext) ? path.join(path.resolve(process.cwd(), './content/themes'), themename, 'views', viewname + '.' + themefileext) : false; exttemplatefile = (extname && themefileext) ? path.join(path.resolve(process.cwd(), './node_modules', extname), 'views', viewname + '.' + themefileext) : false; var getExtensionView = function(viewname, callback) { if (extname) { fs.open(exttemplatefile, 'r', function(err, fd) { if (fd) { fs.close(fd); } if (err) { callback(err, viewname, null); } else { callback(null, viewname, exttemplatefile); } }); } else { callback('no extname', viewname, viewname); } }; var getThemeView = function(viewname, callback) { if (theme) { fs.open(themetemplatefile, 'r', function(err, fd) { if (fd) { fs.close(fd); } if (err) { callback(err, viewname, null); } else { callback(null, viewname, themetemplatefile); } }); } else { callback('no theme', viewname, viewname); } }; getThemeView(viewname, function(err, defaultview, themeview) { if (err) { getExtensionView(defaultview, function(err, defaultview, extname) { if (err) { callback(null, defaultview); } else { callback(null, extname); } }); } else { callback(null, themeview); } }); }; Controller.prototype.respondInKind = function(options, asyncCallback) { var req = options.req, res = options.res, err = options.err, responseData = options.responseData || {}, callback = options.callback || asyncCallback; responseData.periodic = responseData.periodic || {}; responseData.periodic.version = appSettings.version; responseData.periodic.name = appSettings.name; responseData.request = { query: req.query, params: req.params, baseurl: req.baseUrl, originalurl: req.originalUrl, parsed: req._parsedUrl, 'x-forwarded-for': req.headers['x-forwarded-for'], remoteAddress: req.connection.remoteAddress, referer: req.headers.referer, originalUrl: req.originalUrl, headerHost: req.headers.host }; if (err) { responseData.status = 'error'; responseData.result = 'error'; responseData.data = { error: err }; } if (path.extname(req.originalUrl) === '.html' || req.is('html') || req.is('text/html') || path.extname(req.originalUrl) === '.htm') { // console.log('responding because format is html'); callback(req, res, responseData); } else if (jsonCheck(req)) { if (periodicResources.settings.sessions.enabled && req.session) { responseData.flash_messages = req.flash(); } if (req.query.callback) { res.jsonp(responseData); } else { res.send(responseData); } } else if (options.redirecturl) { res.redirect(options.redirecturl); // console.log('req.flash()',req.flash()); } else { callback(req, res, responseData); } }; Controller.prototype.logError = function(options) { var err = options.err, req = options.req, errormeta = { err: err }, userobject = {}; if (req) { errormeta.ipinfo = { 'x-forwarded-for': req.headers['x-forwarded-for'], remoteAddress: req.connection.remoteAddress, referer: req.headers.referer, originalUrl: req.originalUrl, headerHost: req.headers.host, osHostname: os.hostname() }; userobject = req.user || req.body; } if (userobject.email || userobject.username) { errormeta.ipinfo.user = userobject.email || userobject.username; } logger.error(err.message, err.stack, errormeta); }; Controller.prototype.logWarning = function(options) { var err = options.err, req = options.req, errormeta = { err: err }, userobject = {}; if (req) { errormeta.ipinfo = { 'x-forwarded-for': req.headers['x-forwarded-for'], remoteAddress: req.connection.remoteAddress, referer: req.headers.referer, originalUrl: req.originalUrl, headerHost: req.headers.host, osHostname: os.hostname() }; userobject = req.user || req.body; } if (userobject.email || userobject.username) { errormeta.ipinfo.user = userobject.email || userobject.username; } logger.warn(err.message, err.stack, errormeta); }; /** * default response handler for express views, or will redirect to another request * @param {object} options res,req,redirecturl,err,callback,responseData - this is what's sent to the rendered template view, also appends http request information like base url, query string parameters, etc * @return {object} response object render or callback */ Controller.prototype.handleDocumentQueryRender = function(options, asyncCallback) { var res = options.res, req = options.req, redirecturl = options.redirecturl, err = options.err, callback = options.callback || asyncCallback, renderview = options.renderView, responseData = options.responseData, cachetype, cachekey, cachedView, useCacheTest = (useViewCache() && req.headers.periodicCache !== 'no-periodic-cache'), renderResponseData = function(rendererror) { // console.log('this is rendererror',rendererror); if (rendererror) { err = rendererror; } if (err) { this.handleDocumentQueryErrorResponse({ res: res, req: req, err: err, callback: callback, redirecturl: redirecturl }); } else { Controller.prototype.respondInKind({ req: req, res: res, responseData: responseData, callback: function(req, res, responseData) { if (!responseData.flash_messages && periodicResources.settings.sessions.enabled && req.session) { responseData.flash_messages = req.flash(); } res.render(renderview, responseData, function(err, renderedview) { if (err) { if (callback) { callback(err); } else { Controller.prototype.logError({ err: err, req: req }); res.status(500); res.render(appSettings.customThemeView, { message: err.message, error: err }); res.end(); } } else if (useCacheTest && global.CoreCache) { global.CoreCache.ViewCache.set({ key: cachekey, val: renderedview }, function(err) { if (err) { Controller.prototype.logError({ err: err, req: req }); res.status(500); res.render(appSettings.customThemeView, { message: err.message, error: err }); res.end(); } else { logger.silly('saved cached ', cachekey); if (callback) { callback(null, renderedview); } else { res.send(renderedview); } } }); } else if (callback) { callback(null, renderedview); } else { res.send(renderedview); } }); } }); } }.bind(this), getPageView = function(err) { if (err) { renderResponseData(err); } else if (applocals) { async.applyEach( applocals.additionalHTMLFunctions, { req: req, res: res }, function(asyncerr) { if (asyncerr) { this.handleExceptionError(asyncerr, req, res); } else { renderResponseData(); } }.bind(this)); } else { renderResponseData(); } }.bind(this); if (req.session && req.session.themename && renderview && theme) { renderview = renderview.replace(theme, req.session.themename); } if (useCacheTest && global.CoreCache) { cachetype = global.CoreCache.ViewCache.type; cachekey = global.CoreCache.generateKeyFromRequestUrl(cachetype, req.originalUrl); cachedView = global.CoreCache.ViewCache.get({ key: cachekey }, function(err, cachedViewData) { if (cachedViewData) { logger.silly('cache hit', cachekey); res.set('X-Periodic-Cache', 'hit'); res.send(cachedViewData); } else { logger.silly('cache miss', cachekey); res.set('X-Periodic-Cache', 'miss'); getPageView(); } }); } else { getPageView(err); } }; Controller.prototype.handleExceptionError = function(err, req, res) { if (req.xhr) { res.send(500, { error: 'Something blew up!' }); } else { res.status(500); // if(appconfig.settings().theme) res.render('home/error500', { message: err.message, error: err }); } }; /** * default response handler for error, or will redirect with flash error set * @param {object} options err,req,res,callback * @return {object} response object render or callback */ Controller.prototype.handleDocumentQueryErrorResponse = function(options) { var err = options.err, errormessage, redirecturl = options.redirecturl, req = options.req, res = options.res, loggerFunction = logger.error, callback = options.callback; //, // errorFlashMessage = (options.errorflash) ? options.errorflash : errormessage; if (typeof options.use_warning === 'boolean' && options.use_warning === true) { loggerFunction = logger.warn; } else if (typeof options.use_warning === 'string') { loggerFunction = logger[options.use_warning]; } if (typeof options.err === 'string') { errormessage = options.err; } else if (typeof options.err.toString() === 'string') { errormessage = options.err.toString(); } else { errormessage = options.err.message; } // else if (typeof options.err === 'string' || typeof options.err.toString() === 'string') ? options.err.toString() : options.err.message if (res.statusCode !== 200 && res.statusCode !== 400) { res.status(res.statusCode); } else { res.status(400); } // console.log('errormessage',errormessage,typeof errormessage); if (res.statusCode === 404) { logger.debug(errormessage, req.url); } else { if (err.stack) { var userobject = {}, userfieldtouse = ''; if (req) { userobject = req.user || req.body; } if (userobject.email || userobject.username) { userfieldtouse = userobject.email || userobject.username; } loggerFunction(errormessage, err, { ipinfo: { 'x-forwarded-for': req.headers['x-forwarded-for'], originalUrl: req.originalUrl, remoteAddress: req.connection.remoteAddress, referer: req.headers.referer, headerHost: req.headers.host, osHostname: os.hostname(), user: userfieldtouse } }); } } var responseErrorData = { 'result': 'error', 'data': { error: errormessage } }; if (jsonCheck(req)) { if (periodicResources.settings.sessions.enabled && req.session) { responseErrorData.flash_messages = req.flash(); } if (req.query.callback) { res.jsonp(responseErrorData); } else { res.send(responseErrorData); } } else { if (options.errorflash !== false && periodicResources.settings.sessions.enabled && req.session) { req.flash('error', errormessage); } if (callback) { callback(); } else if (redirecturl) { res.redirect(redirecturl); } else { var self = this; self.getPluginViewDefaultTemplate({ viewname: 'home/error404', themefileext: appSettings.templatefileextension }, function(err, templatepath) { self.handleDocumentQueryRender({ res: res, req: req, renderView: templatepath, responseData: { pagedata: { title: 'Not Found' }, user: req.user, url: req.url } }); } ); } } }; /** * short hand method for rendering views * @param {object} req [description] * @param {object} res [description] * @param {object} viewtemplate [description] * @param {object} viewdata [description] * @return {object} [description] */ Controller.prototype.renderView = function(req, res, viewtemplate, viewdata) { this.getPluginViewDefaultTemplate(viewtemplate, function(err, templatepath) { this.handleDocumentQueryRender({ res: res, req: req, err: err, renderView: templatepath, responseData: viewdata }); }.bind(this) ); }; var clearModelDocCache = function(options, asynccallback) { var cachetype, req = options.req, doc = options.doc, model = options.model, datacachetype = global.CoreCache.DataCache.type, datacachekey_byid, datacachekey_byname, asyncDeleteFunctions = [], deleteKeysArray = [], deleteViewKeyFunction = function(delkey) { return function(cb) { global.CoreCache.ViewCache.del({ key: delkey }, cb); }; }, getKeyURLFromCacheModelOptions = function(modelroute, id) { var delkey = '/' + path.join(modelroute.replace(':id', id)); return global.CoreCache.generateKeyFromRequestUrl(cachetype, delkey); }, deleteDataCache = function() { if (doc.id) { datacachekey_byid = global.CoreCache.generateKeyFromRequestUrl(datacachetype, model.modelName + '+' + doc.id); global.CoreCache.DataCache.del({ key: datacachekey_byid }, function(err, status) { logger.silly('deleted memory cache', datacachekey_byid, status); }); } if (doc.name) { datacachekey_byname = global.CoreCache.generateKeyFromRequestUrl(datacachetype, model.modelName + '+' + doc.name); global.CoreCache.DataCache.del({ key: datacachekey_byname }, function(err, status) { logger.silly('deleted memory cache', datacachekey_byname, status); }); } if (model.modelName && (doc.id || doc.name)) { global.CoreCache.DataCache.del({ model_name: model.modelName.toLowerCase(), model_name_plural: pluralize(model.modelName.toLowerCase()), docid: doc.id, docname: doc.name, }, function(err, status) { logger.silly('viewCache Del', status) }); } }, deleteViewCache = function(options) { // console.log('deleteViewCache options',options); var doccache = [`${model.modelName.toLowerCase()}/:id`], //options.doccache, doclistcache = [`${ pluralize(model.modelName.toLowerCase())}`], //options.doclistcache, docid = options.docid, docname = options.docname; //, // reqdoc = options.reqdoc; if (doccache && doccache.length > 0 /* && reqdoc */ ) { for (var gcid in doccache) { if (docname) { deleteKeysArray.push(getKeyURLFromCacheModelOptions(doccache[gcid], docname)); } deleteKeysArray.push(getKeyURLFromCacheModelOptions(doccache[gcid], docid)); } } if (doclistcache && doclistcache.length > 0 && docname /* && reqdoc */ ) { for (var gcil in doclistcache) { deleteKeysArray.push(getKeyURLFromCacheModelOptions(doclistcache[gcil], docname)); } } if (req) { deleteKeysArray.push(global.CoreCache.generateKeyFromRequestUrl(cachetype, req.originalUrl)); deleteKeysArray.push(global.CoreCache.generateKeyFromRequestUrl(cachetype, req.originalUrl.split('?')[0])); //strip out query string parameters from URL let baseURL = req.originalUrl.split(options.docid)[0]; if (baseURL) { let cleanBaseURL = baseURL.replace(model.modelName.toLowerCase(), pluralize(model.modelName.toLowerCase())); //get listing page deleteKeysArray.push(global.CoreCache.generateKeyFromRequestUrl(cachetype, baseURL)); deleteKeysArray.push(global.CoreCache.generateKeyFromRequestUrl(cachetype, cleanBaseURL)); } } // console.log('model.modelName',model.modelName, 'docid',docid , 'docname',docname); if (model.modelName && (docid || docname)) { global.CoreCache.ViewCache.del({ model_name: model.modelName.toLowerCase(), model_name_plural: pluralize(model.modelName.toLowerCase()), docid: docid, docname: docname, }, function(err, status) { logger.silly('viewCache Del', status) }); } }; cachetype = global.CoreCache.ViewCache.type; if (useDataCache()) { deleteDataCache(); } if (useViewCache()) { deleteViewCache({ doccache: global.CoreCache.options[model.modelName.toLowerCase() + '_doc_cache'], doclistcache: global.CoreCache.options[model.modelName.toLowerCase() + '_list_cache'], docid: doc.id, docname: doc.name //, // reqdoc : req.controllerData[model.modelName.toLowerCase()] }); } if (deleteKeysArray.length > 0) { for (var y in deleteKeysArray) { asyncDeleteFunctions.push(deleteViewKeyFunction(deleteKeysArray[y])); } async.series(asyncDeleteFunctions, function(err) { if (err) { logger.error(err); } logger.silly('cleared cache for ', deleteKeysArray); if (asynccallback && typeof asynccallback === 'function') { asynccallback(); } }); } }; /** * short hand mongoose load document query * @param {object} options model,docid - id or name,callback,population -mongoose population, selection - mongoose selection * @return {Function} callback(err,document) */ Controller.prototype.loadModel = function(options, asyncCallback) { var model = options.model, docid = options.docid, docnamelookup = options.docnamelookup || 'name', sort = options.sort, callback = options.callback || asyncCallback, population = options.population, selection = options.selection, fields = options.fields, query, namequery = {}, cached = (typeof options.cached === 'boolean' && options.cached === true) && true, useCacheTest = (useDataCache() && cached), cachetype = (global.CoreCache) ? global.CoreCache.DataCache.type : null, cachekey = (global.CoreCache) ? global.CoreCache.generateKeyFromRequestUrl(cachetype, model.modelName + '+' + docid) : null, cachedData, cachekeyexpirename = (options && options.model_type === 'sequelize') ? options.model_name.toLowerCase() + '_doc_cache_expires' : model.modelName.toLowerCase() + '_doc_cache_expires', // cachedDataDoc, queryCallback = function(err, doc) { if (useCacheTest && global.CoreCache && doc) { logger.debug('cachekeyexpirename', cachekeyexpirename, global.CoreCache.options[cachekeyexpirename]); global.CoreCache.DataCache.set({ key: cachekey, val: doc, expires: global.CoreCache.options[cachekeyexpirename] }, function(err, expires) { if (err) { logger.error(err); } logger.silly('cached', cachekey, 'expires', expires); }); callback(err, doc); } else { callback(err, doc); } }, getDataQuery = function() { // console.log('query',query); if (options && options.model_type === 'sequelize') { let load_sql_function; let docnamequery = {}; if (options.docnamelookup) { // console.log('population',population) docnamequery[options.docnamelookup] = docid; load_sql_function = options.sequelizeDB.models[options.model_name].findOne({ where: docnamequery, include: population.include }); } else { load_sql_function = options.sequelizeDB.models[options.model_name].findById(docid, { include: population.include }); } load_sql_function.then((doc) => { // console.log('sql doc',doc); queryCallback(null, doc); }) .catch((e) => { queryCallback(e); }); } else if (population) { model.findOne(query, fields).sort(sort).select(selection).populate(population).exec(queryCallback); } else { model.findOne(query, fields).sort(sort).select(selection).exec(queryCallback); } }; namequery[docnamelookup] = docid; // console.log('loadModel cached',cached); if (mongoId.isValid(docid)) { query = { $or: [namequery, { _id: docid }] }; } else if (options.searchusername) { query = { $or: [namequery, { username: docid }] }; } else { query = namequery; } // console.log('options.cached',options.cached); // console.log('useCache',useCache); // console.log('useCacheTest',useCacheTest); if (useCacheTest && global.CoreCache) { cachedData = global.CoreCache.DataCache.get({ key: cachekey }, function(err, cachedDataDoc) { if (cachedDataDoc) { logger.silly('X-Periodic-Data-Cache', 'hit', cachekey); callback(null, cachedDataDoc); } else { logger.silly('X-Periodic-Data-Cache', 'miss', cachekey); getDataQuery(); } }); } else { getDataQuery(); } }; /** * short hand mongoose find by id query promisified * @param {object} options model, docid, sort, population, selection, searchusername * options callback is automatically set to handle fulfillment of the async mongoose query * @return {object} chainable instance of bluebird promise */ Controller.prototype.loadModelPromisified = function(options) { return new Bluebird(function(resolve, reject) { var fulfiller = function(err, data) { if (err) { reject(err); } else { resolve(data); } }; options.callback = fulfiller; Controller.prototype.loadModel(options); }); }; /** * short hand mongoose search documents query * @param {object} options model,query - mongoose query,callback,population -mongoose population, selection - mongoose selection , limit, offset * @return {Function} callback(err,documents) */ Controller.prototype.searchModel = function(options, asyncCallback) { var model = options.model, query = options.query, sort = options.sort, offset = options.offset, fields = options.fields, selection = options.selection, limit = options.limit, callback = options.callback || asyncCallback, population = options.population, cached = (typeof options.cached === 'boolean' && options.cached === true) && true, useCacheTest = (useDataCache() && cached), cachetype = (global.CoreCache) ? global.CoreCache.DataCache.type : null, cachekey, cachedData, cachekeyexpirename = (options && options.model_type === 'sequelize') ? options.model_name.toLowerCase() + '_doc_cache_expires' : model.modelName.toLowerCase() + '_doc_cache_expires', queryDocsCallback = function(err, documents) { // console.log('queryDocsCallback err',err); // // console.log('queryDocsCallback err.stack',err.stack); // console.log('queryDocsCallback documents',documents); // // console.log('cachekeyexpirename',cachekeyexpirename,global.CoreCache.options[cachekeyexpirename]); if (useCacheTest && global.CoreCache && documents) { global.CoreCache.DataCache.set({ key: cachekey, val: documents, expires: global.CoreCache.options[cachekeyexpirename] }, function(err, expires) { if (err) { logger.error(err); } logger.silly('cached', cachekey, 'expires', expires); }); callback(err, documents); } else { callback(err, documents); } }, getDocsDataQuery = function() { if (options && options.model_type && options.model_name && options.model_type === 'sequelize') { // console.log('search model query',query); model.findAll({ where: query, offset: offset, limit: limit, raw: options.return_raw, include: (population.include) ? population.include : {}, }) .then((documents) => { // console.log('sql documents',documents); queryDocsCallback(null, documents); }) .catch((e) => { queryDocsCallback(e); }); // docnamelookup: options.docnamelookup, // model_type: options.model_type, // model_name: options.model_name, // sequelizeDB: options.sequelizeDB, } else if (population) { model.find(query, fields).sort(sort).select(selection).limit(limit).skip(offset).populate(population).exec(queryDocsCallback); } else { model.find(query, fields).sort(sort).select(selection).limit(limit).skip(offset).exec(queryDocsCallback); } }; sort = (sort) ? sort : '-createdat'; offset = (offset) ? offset : 0; limit = (limit) ? limit : 500; if (global.CoreCache) { let lowercase_model_name = (options && options.model_type === 'sequelize') ? options.model_name.toLowerCase() : model.modelName.toLowerCase(); cachekey = global.CoreCache.generateKeyFromRequestUrl(cachetype, pluralize(lowercase_model_name) + '+' + JSON.stringify(query) + '+' + sort + '+' + offset + '+' + selection + '+' + limit + '+' + population); } if (useCacheTest && global.CoreCache) { cachedData = global.CoreCache.DataCache.get({ key: cachekey }, function(err, cachedDataDoc) { if (cachedDataDoc) { logger.silly('X-Periodic-Data-Cache', 'hit', cachekey); callback(null, cachedDataDoc); } else { logger.silly('X-Periodic-Data-Cache', 'miss', cachekey); getDocsDataQuery(); } }); } else { getDocsDataQuery(); } }; /** * short hand mongoose find query promisified * @param {object} options model, query, sort, offset, selection, limit, population * options callback is automatically set to handle fulfillment of the async mongoose query * @return {object} chainable instance of bluebird promise */ Controller.prototype.searchModelPromisified = function(options) { return new Bluebird(function(resolve, reject) { var fulfiller = function(err, data) { if (err) { reject(err); } else { resolve(data); } }; options.callback = fulfiller; Controller.prototype.searchModel(options); }); }; /** * short hand mongoose create document query * @param {object} options model,newdoc - document to insert, req, res,callback, successredirect, appendid - append the id of newly created document on redirect * @return {object} responseData or redirected page */ Controller.prototype.createModel = function(options, asyncCallback) { var model = options.model, newdoc = options.newdoc, req = options.req, res = options.res, callback = options.callback || asyncCallback, successredirect = options.successredirect, appendid = options.appendid, responseData = {}, cached = (typeof options.cached === 'boolean' && options.cached === true) && true, useCacheTest = ((useDataCache() || useViewCache()) && cached); var xss_white_list = options.xss_white_list || html_xss_default_whitelist; // console.log('useCacheTest',useCacheTest); // console.log('useDataCache()',useDataCache()); // console.log('useViewCache()',useViewCache()); if (req && req.controllerData && req.controllerData.html_xss) { newdoc = JSON.parse(xss(JSON.stringify(newdoc), xss_white_list)); } else if (req && req.controllerData && !req.controllerData.skip_xss) { var regex = /(<([^>]+)>)/ig; newdoc = JSON.parse(JSON.stringify(newdoc).replace(regex, '')); } model.create(newdoc, function(err, saveddoc) { if (err) { if (callback) { callback(err, saveddoc); } else { this.handleDocumentQueryErrorResponse({ err: err, errorflash: err.message, res: res, req: req }); } } else { if (callback) { if (arguments.length >= 2) { delete arguments['0']; callback(null, arguments['1']); } else { callback(null, saveddoc); } } else if (jsonCheck(req)) { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } Controller.prototype.respondInKind({ req: req, res: res, responseData: formatJsonResponse(saveddoc) }); } else if (appendid) { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } res.redirect(successredirect + saveddoc._id); } else { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } res.redirect(successredirect); } if (useCacheTest) { // console.log('createModel clearModelDocCache'); clearModelDocCache({ model: model, doc: saveddoc }); } } }.bind(this)); }; /** * short hand mongoose create query promisified * @param {object} options model, newdoc, req, res * options callback is automatically set to handle fulfillment of the async mongoose query * @return {object} chainable instance of bluebird promise */ Controller.prototype.createModelPromisified = function(options) { return new Bluebird(function(resolve, reject) { options.callback = function(err, data) { if (err) { reject(err); } else { resolve(data); } }; Controller.prototype.createModel(options); }); }; var getIdFromPopulatedObject = function(object) { if (object && typeof object === 'object' && object._id) { return object._id.toString(); } else { return object; } }; var depopulateArray = function(array) { var depopulatedArray = []; for (var i = 0; i < array.length; i++) { if (typeof array[i] === 'object' && typeof array[i]._id === 'undefined') { depopulatedArray[i] = Controller.prototype.depopulate(array[i]); } else { depopulatedArray[i] = getIdFromPopulatedObject(array[i]); } } return depopulatedArray; }; var depopulate = function(object) { var depopulatedObject = {}; for (var prop in object) { if (Array.isArray(object[prop])) { depopulatedObject[prop] = depopulateArray(object[prop]); } else if (object[prop] !== null && typeof object[prop] === 'object' && typeof object[prop]._id === 'undefined' && new Date(object[prop]).toString() === 'Invalid Date') { depopulatedObject[prop] = depopulate(object[prop]); } else { depopulatedObject[prop] = getIdFromPopulatedObject(object[prop]); } } return depopulatedObject; }; Controller.prototype.depopulate = depopulate; var compareHasDifferentValues = function(originalValue, revisionValue) { var checkrevdate = new Date(revisionValue).toString(); var checkoridate = new Date(originalValue).toString(); if (originalValue !== null && revisionValue !== null && typeof originalValue !== 'undefined' && typeof revisionValue !== 'undefined' && originalValue.toString() === revisionValue.toString()) { // console.log('string vals are the same',originalValue,revisionValue); return false; } else if (checkrevdate !== 'Invalid Date' && checkoridate !== 'Invalid Date' && checkrevdate === checkoridate && moment(revisionValue).isValid() && moment(originalValue).isValid() && moment(revisionValue).isSame(originalValue)) { // console.log('dates are the same',checkoridate,checkrevdate); return false; } else if (originalValue === null && (revisionValue === '' || revisionValue === null || typeof revisionValue === 'undefined')) { // console.log('originalValue empty vals are the same',originalValue,revisionValue); return false; } else if (revisionValue === null && (originalValue === '' || originalValue === null || typeof originalValue === 'undefined')) { // console.log('revisionValue empty vals are the same',originalValue,revisionValue); return false; } else { return true; } }; var returnObjectDifference = function(original, revision, skipDepopulate) { var changesetdata = {}; if (original.toJSON && typeof original.toJSON === 'function') { original = original.toJSON(); } else if (original.toObject && typeof original.toObject === 'function') { original = original.toObject(); } if (revision.toJSON && typeof revision.toJSON === 'function') { revision = revision.toJSON(); } else if (revision.toObject && typeof revision.toObject === 'function') { revision = revision.toObject(); } if (original.changes) { delete original.changes; } if (revision.changes) { delete revision.changes; } if (typeof original.__v !== 'undefined') { delete original.__v; } if (typeof revision.__v !== 'undefined') { delete revision.__v; } if (original.createdat) { delete original.createdat; } if (revision.createdat) { delete revision.createdat; } if (original._id) { delete original._id; } if (revision._id) { delete revision._id; } if (original.random) { delete original.random; } if (revision.random) { delete revision.random; } if (!skipDepopulate) { revision = depopulate(revision); original = depopulate(original); } var compareresult = objectComparison(original, revision), differences = {}; // console.log('compareresult',compareresult); if (compareresult.equal === false) { for (var props in compareresult.differences) { // console.log('compareresult.differences props',props); if (compareHasDifferentValues(compareresult.differences[props].firstValue, compareresult.differences[props].secondValue)) { differences[props] = compareresult.differences[props].firstValue; } } } // console.log('differences',differences); changesetdata = str2json.convert(differences); // console.log('revision',revision); // console.log('original',original); console.log('changesetdata', changesetdata); return changesetdata; }; Controller.prototype.updateMultipleModel = function(options) { logger.warn('cached documents not clearing and revisions on multple documents not saved'); var model = options.model, // id = options.id, updatequery = options.updatequery, updateattributes = options.updateattributes, req = options.req, res = options.res, callback = options.callback, successredirect = options.successredirect, // originalrevision = options.originalrevision, // appendid = options.appendid, responseData = {}; //, // updateOperation, // cached = (typeof options.cached === 'boolean' && options.cached===true) && true, // useCacheTest = (useCache() && global.CoreCache && cached && req.headers.periodicCache!=='no-periodic-cache'); model.update(updatequery, updateattributes, { multi: true }, function(err, numAffected) { if (err) { if (callback) { callback(err, null); } else { this.handleDocumentQueryErrorResponse({ err: err, errorflash: err.message, res: res, req: req }); } } else { if (callback) { callback(null, numAffected); } else if (jsonCheck(req)) { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } Controller.prototype.respondInKind({ req: req, res: res, responseData: formatJsonResponse(numAffected) }); } else { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } res.redirect(successredirect); } // if(useCacheTest){ // updatedoc.id = id; // clearModelDocCache({model:model,doc:updatedoc,req:req}); // } } }); }; /** * short hand mongoose update document query * @param {object} options model, id - objectid of mongoose document,updatedoc - document to update, req, res,callback, successredirect, appendid - append the id of newly created document on redirect, removefromarray - sets the update operation to manipulate an array of documents with mongo $pull, appendArray - sets the update operation to manipulate an array of documents with mongo $push, saverevision - save revisions * @return {object} responseData or redirected page */ Controller.prototype.updateModel = function(options, asyncCallback) { if (!options.id) { //Added to enforce mongo id throw new Error('Update is missing mongo id'); } var model = options.model, id = options.id, updatedoc = options.updatedoc, req = options.req, res = options.res, callback = options.callback || asyncCallback, successredirect = options.successredirect, originalrevision = options.originalrevision, skipDepopulate = options.skipDepopulate, appendid = options.appendid, responseData = {}, updateOperation, cached = (typeof options.cached === 'boolean' && options.cached === true) && true, useCacheTest = ((useDataCache() || useViewCache()) && global.CoreCache && cached && req.headers.periodicCache !== 'no-periodic-cache'); var xss_white_list = options.xss_white_list || html_xss_default_whitelist; if (req && req.controllerData && req.controllerData.html_xss) { updatedoc = JSON.parse(xss(JSON.stringify(updatedoc), xss_white_list)); } else if (req && req.controllerData && !req.controllerData.skip_xss) { var regex = /(<([^>]+)>)/ig; updatedoc = JSON.parse(JSON.stringify(updatedoc).replace(regex, '')); } if (options.removeFromArray) { logger.silly('removing array in doc'); updateOperation = { $pull: updatedoc }; } else if (options.appendArray) { logger.silly('appending array in doc'); updateOperation = { $push: updatedoc }; } else { updatedoc.updatedat = new Date(); logger.silly('updating entire doc'); updateOperation = { $set: updatedoc }; } if (options.forceupdate) { model.update({ _id: id }, updateOperation, function(err, numAffected) { if (err) { if (callback) { callback(err, null); } else { this.handleDocumentQueryErrorResponse({ err: err, errorflash: err.message, res: res, req: req }); } } else { if (callback) { callback(null, numAffected); } else if (jsonCheck(req)) { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } Controller.prototype.respondInKind({ req: req, res: res, responseData: numAffected }); } else { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } res.redirect(successredirect); } if (useCacheTest) { // console.log('updateModel clearModelDocCache'); updatedoc.id = id; clearModelDocCache({ model: model, doc: updatedoc, req: req }); } } }); } else { // console.log('before tried to save', updateOperation); // console.log('options.saverevision ',options.saverevision ); model.findByIdAndUpdate(id, updateOperation, function(err, saveddoc) { // console.log('tried to save', updateOperation,err, saveddoc); // console.log('saveddoc', saveddoc); if (err) { if (callback) { callback(err, null); } else { this.handleDocumentQueryErrorResponse({ err: err, errorflash: err.message, res: res, req: req }); } } else { if (callback) { callback(null, saveddoc); } else if (jsonCheck(req)) { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } if (options.population) { model.findOne({ _id: saveddoc._id }).populate(options.population).exec(function(err, popdoc) { if (err) { responseData.data.docpopulationerror = err; responseData.data.status = 'couldnt populate'; responseData.data.doc = popdoc; Controller.prototype.respondInKind({ req: req, res: res, responseData: responseData }); } else { Controller.prototype.respondInKind({ req: req, res: res, responseData: formatJsonResponse(saveddoc) }); } }); } else { Controller.prototype.respondInKind({ req: req, res: res, responseData: formatJsonResponse(saveddoc) }); } } else if (appendid) { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } res.redirect(successredirect + saveddoc._id); } else { if (periodicResources.settings.sessions.enabled && req.session) { req.flash('success', 'Saved'); } res.redirect(successredirect); } //refresh cache if (useCacheTest) { updatedoc.id = id; clearModelDocCache({ model: model, doc: updatedoc, req: req }); } //save revision if (options.saverevision && originalrevision) { var changesetdata = updatedoc, changesetdiff; delete changesetdata.docid; delete changesetdata._csrf; delete changesetdata.save_button; delete changesetdata.changes; delete originalrevision.changes; delete changesetdata._wysihtml5_mode; delete changesetdata.doctypename; delete changesetdata.doctypenamelink; delete changesetdata.time; delete changesetdata.searchdocumentsbutton; delete changesetdata.date; delete changesetdata.mediafiles; changesetdiff = returnObjectDifference(originalrevision, changesetdata, skipDepopulate); if (Object.keys(changesetdiff).length > 0) { model.findByIdAndUpdate( id, { $push: { 'changes': { changeset: changesetdiff, editor: (req && req.user && req.user._id) ? req.user._id : 'invalid_user_id', editor_username: (req && req.user && req.user.username) ? req.user.username : 'invalid_user_username' } } }, // // {safe: true, upsert: true}, function(err) { if (err) { logger.error(err); } } ); } else { logger.silly('no changes to save'); } } } }.bind(this)); } }; /** * short hand mongoose update promisified * @param {object} options model, id, updatedoc * options callback is automatically set to handle fulfillment of the async mongoose query * @return {object} chainable instance of bluebird promise */ Controller.prototype.updateModelPromisified = function(options) { return new Bluebird(function(resolve, reject) { options.callback = function(err, data) { if (err) { reject(err); } else { resolve(data); } }; Controller.prototype.updateModel(options); }); }; /** * short hand mongoose delete document query * @param {object} options model,deleteid - id to delete,callback * @return {Function} callback(err) */ Controller.prototype.deleteModel = function(options, asyncCallback) { var model = options.model, deleteid = options.deleteid, req = options.req, // res = options.res, callback = options.callback || asyncCallback, cached = (typeof options.cached === 'boolean' && options.cached === true) && true, useCacheTest = ((useDataCache() || useViewCache()) && cached && global.CoreCache); model.remove({ _id: deleteid }, function(err, status) { if (useCacheTest) { // console.log('deleteModel clearModelDocCache'); // updatedoc.id = id; clearModelDocCache({ model: model, req: req, doc: { id: deleteid } }, callback); } else { if (callback && typeof callback === 'function') { callback(); } } }); }; /** * short hand mongoose remove promisified * @param {object} options model, deleteid, cached * options callback is automatically set to handle fulfillment of the async mongoose query * @return {object} c