Skip to content

Commit

Permalink
Merge pull request pencilblue#1135 from pencilblue/feature/reqhandler…
Browse files Browse the repository at this point in the history
…hooks

Request Handler Hook - On Theme Route Retrieved
  • Loading branch information
brianhyder authored Sep 5, 2016
2 parents 17e4459 + 605878b commit 523b0f6
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 48 deletions.
101 changes: 67 additions & 34 deletions include/http/request_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ var _ = require('lodash');

module.exports = function RequestHandlerModule(pb) {

//pb dependencies
var AsyncEventEmitter = pb.AsyncEventEmitter;

/**
* Responsible for processing a single req by delegating it to the correct controllers
* @class RequestHandler
* @extends AsyncEventEmitter
* @constructor
* @param {Server} server The http server that the request came in on
* @param {Request} req The incoming request
Expand All @@ -55,6 +59,7 @@ module.exports = function RequestHandlerModule(pb) {
this.activeTheme = null;
this.errorCount = 0;
}
AsyncEventEmitter.extend(RequestHandler);

/**
* A mapping that provides the interface type to parse the body based on the
Expand Down Expand Up @@ -122,6 +127,15 @@ module.exports = function RequestHandlerModule(pb) {
*/
RequestHandler.CORE_ROUTES = require(path.join(pb.config.docRoot, '/plugins/pencilblue/include/routes.js'))(pb);

/**
* The event emitted when a route and theme is derived for an incoming request
* @static
* @readonly
* @property THEME_ROUTE_RETRIEVED
* @type {string}
*/
RequestHandler.THEME_ROUTE_RETIEVED = 'themeRouteRetrieved';

/**
* Initializes the request handler prototype by registering the core routes for
* the system. This should only be called once at startup.
Expand All @@ -135,15 +149,13 @@ module.exports = function RequestHandlerModule(pb) {
util.forEach(RequestHandler.CORE_ROUTES, function(descriptor) {

//register the route
var result;
try {
var result = RequestHandler.registerRoute(descriptor, RequestHandler.DEFAULT_THEME);
if (!result) {
throw new Error();
}
result = RequestHandler.registerRoute(descriptor, RequestHandler.DEFAULT_THEME);
}
catch(e) {
catch(e) {}
if (!result) {
pb.log.error('RequestHandler: Failed to register PB route: %s %s', descriptor.method, descriptor.path);
pb.log.silly(e.stack);
}
});
};
Expand Down Expand Up @@ -798,7 +810,7 @@ module.exports = function RequestHandlerModule(pb) {
this.siteName = this.siteObj.displayName;
//find the controller to hand off to
var route = this.getRoute(this.url.pathname);
if (route == null) {
if (route === null) {
return this.serve404();
}
this.route = route;
Expand Down Expand Up @@ -960,39 +972,58 @@ module.exports = function RequestHandlerModule(pb) {
pb.log.silly("RequestHandler: Settling on theme [%s] and method [%s] for URL=[%s:%s]", rt.theme, rt.method, this.req.method, this.url.href);
}

//sanity check
if (rt.theme === null || rt.method === null || rt.site === null) {
return this.serve404();
}

var inactiveSiteAccess = route.themes[rt.site][rt.theme][rt.method].inactive_site_access;
if (!this.siteObj.active && !inactiveSiteAccess) {
if (this.siteObj.uid === pb.SiteService.GLOBAL_SITE) {
return this.doRedirect('/admin');
//make sure we let the plugins hook in.
this.emitThemeRouteRetrieved(function(err) {
if (util.isError(err)) {
return self.serveError(err);
}
else {
return this.serve404();

//sanity check
if (rt.theme === null || rt.method === null || rt.site === null) {
return self.serve404();
}
}

//do security checks
this.checkSecurity(rt.theme, rt.method, rt.site, function(err, result) {
if (pb.log.isSilly()) {
pb.log.silly('RequestHandler: Security Result=[%s]', result.success);
for (var key in result.results) {
pb.log.silly('RequestHandler:%s: %s', key, JSON.stringify(result.results[key]));
var inactiveSiteAccess = route.themes[rt.site][rt.theme][rt.method].inactive_site_access;
if (!self.siteObj.active && !inactiveSiteAccess) {
if (self.siteObj.uid === pb.SiteService.GLOBAL_SITE) {
return self.doRedirect('/admin');
}
else {
return self.serve404();
}
}
//all good
if (result.success) {
return self.onSecurityChecksPassed(activeTheme, rt.theme, rt.method, rt.site, route);
}

//handle failures through bypassing other processing and doing output
self.onRenderComplete(err);
//do security checks
self.checkSecurity(rt.theme, rt.method, rt.site, function(err, result) {
if (pb.log.isSilly()) {
pb.log.silly('RequestHandler: Security Result=[%s] - %s', result.success, JSON.stringify(result.results));
}
//all good
if (result.success) {
return self.onSecurityChecksPassed(activeTheme, rt.theme, rt.method, rt.site, route);
}

//handle failures through bypassing other processing and doing output
self.onRenderComplete(err);
});
});
};

/**
* Emits the event to let listeners know that a request has derived the route and theme that matches the incoming
* request
* @method emitThemeRouteRetrieved
* @param {function} cb
*/
RequestHandler.prototype.emitThemeRouteRetrieved = function(cb) {
var context = {
site: this.site,
themeRoute: this.routeTheme,
requestHandler: this
};
RequestHandler.emit(RequestHandler.THEME_ROUTE_RETIEVED, context, cb);
};

/**
*
* @method onSecurityChecksPassed
Expand Down Expand Up @@ -1430,7 +1461,7 @@ module.exports = function RequestHandlerModule(pb) {
};

/**
*
* Parses cookies passed for a request
* @static
* @method parseCookies
* @param {Request} req
Expand All @@ -1452,11 +1483,13 @@ module.exports = function RequestHandlerModule(pb) {
};

/**
*
* Checks to see if the URL exists in the current context of the system
* @static
* @method urlExists
* @param {String} url
* @param {
* @param {string} id
* @param {string} site
* @param {function} cb
*/
RequestHandler.urlExists = function(url, id, site, cb) {
var dao = new pb.DAO();
Expand Down
3 changes: 2 additions & 1 deletion include/requirements.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ module.exports = function PB(config) {

//initialize logging
pb.log = require(path.join(config.docRoot, '/include/utils/logging.js'))(config);
pb.AsyncEventEmitter = require(path.join(config.docRoot, '/include/utils/async_event_emitter.js'))(pb);

//setup the System instance
pb.System = require(path.join(config.docRoot, 'include/system/system.js'));
Expand Down Expand Up @@ -94,7 +95,7 @@ module.exports = function PB(config) {
pb.SiteQueryService = require(path.join(config.docRoot, '/include/service/entities/site_query_service.js'))(pb);


//setup object services
//setup object services
pb.SimpleLayeredService = require(path.join(config.docRoot, '/include/service/simple_layered_service.js'))(pb);
pb.MemoryEntityService = require(path.join(config.docRoot, '/include/service/memory_entity_service.js'))(pb);
pb.CacheEntityService = require(path.join(config.docRoot, '/include/service/cache_entity_service.js'))(pb);
Expand Down
127 changes: 127 additions & 0 deletions include/utils/async_event_emitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright (C) 2016 PencilBlue, LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';

//dependencies
var AsyncEventEmitterLib = require('async-eventemitter');

module.exports = function(pb) {

/**
* @static
* @class AsyncEventEmitter
*/
function AsyncEventEmitter(){}

/**
* @static
* @method extend
* @param {function} prototype
*/
AsyncEventEmitter.extend = function(prototype) {
var events = new AsyncEventEmitterLib();

/**
* Registers a listener for the specified event.
* @static
* @method on
* @param {String} event
* @param {Function} listener
* @return {*}
*/
prototype.on = function(event, listener) {
return events.on(event, listener);
};

/**
* Registers a listener to fire a single time for the specfied event
* @static
* @method once
* @param {String} event
* @param {Function} listener
* @return {*}
*/
prototype.once = function(event, listener) {
return events.once(event, listener);
};

/**
* Removes the listener from the specified event
* @static
* @method removeListener
* @param {String} event
* @param {Function} listener
* @return {*}
*/
prototype.removeListener = function(event, listener) {
return events.removeListener(event, listener);
};

/**
* Removes all listeners for the specified event
* @static
* @method removeAllListeners
* @param {String} event
* @return {*}
*/
prototype.removeAllListeners = function(event) {
return events.removeAllListeners(event);
};

/**
* Sets the maximum number of listeners for the emitter
* @static
* @method setMaxListeners
* @param {Integer} n
* @return {EventEmitter}
*/
prototype.setMaxListeners = function(n) {
return events.setMaxListeners(n);
};

/**
* Returns a list of the listeners for the specified event
* @static
* @method listeners
* @param {String} event
* @return {Array}
*/
prototype.listeners = function(event) {
return events.listeners(event);
};

/**
*
* @static
* @method emit
* @param {String} event
* @param {Object} data
* @param {Function} cb (Error)
*/
prototype.emit = function(event, data, cb) {
var listeners = events.listeners(event);
if (listeners.length === 0) {
return cb();
}

pb.log.silly('AsyncEventEmitter: Emitting events: [%s] to %s listeners', event, listeners.length);
events.emit(event, data, cb);
};
};

return AsyncEventEmitter;
};
15 changes: 15 additions & 0 deletions plugins/sample/sample.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ module.exports = function SamplePluginModule(pb) {
*/
SamplePlugin.onStartupWithContext = function (context, cb) {

/**
* Example for hooking into the RequestHandler for custom control flow. The context will also provide the site.
* This means that for multi-site implementations where the plugin is installed on a per plugin basis the hook
* should only be registered ONCE. Otherwise it will execute multiple times causing performance to degrade
* @param ctx {object}
* @param {RequestHandler} ctx.requestHandler
* @param {object} ctx.themeRoute
* @param {function} (Error)
*/
pb.RequestHandler.on(pb.RequestHandler.THEME_ROUTE_RETIEVED, function (ctx, callback) {
//do what ever needs to be done. Use the callback to continue normal control flow or don't if you need to do redirects
pb.log.debug('SamplePlugin: The request handler hook triggered for request: %s', ctx.requestHandler.url.path);
callback();
});

/**
* Administration Navigation sample
*/
Expand Down
Loading

0 comments on commit 523b0f6

Please sign in to comment.