API Docs for: 0.8.0
Show:

File: include/http/request_handler.js

/*
    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 url     = require('url');
var fs      = require('fs');
var path    = require('path');
var async   = require('async');
var domain  = require('domain');
var Cookies = require('cookies');
var util    = require('../util.js');
var _       = require('lodash');
var mime    = require('mime');
var HttpStatusCodes = require('http-status-codes');

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
     * @param {Response} resp The outgoing response
     */
    function RequestHandler(server, req, resp){

        /**
         * @property startTime
         * @type {number}
         */
        this.startTime = (new Date()).getTime();

        /**
         * @property server
         * @type {Server}
         */
        this.server = server;

        /**
         * @property req
         * @type {Request}
         */
        this.req = req;

        /**
         * @property resp
         * @type {Response}
         */
        this.resp = resp;

        /**
         * @property url
         * @type {Url}
         */
        this.url       = url.parse(req.url, true);

        /**
         * The hostname (host header) of the current request. When no host
         * header is provided the globa context is assumed.  We do this because
         * some load balancers including HAProxy use the root as the heartbeat.
         * If we error then the web server will be taken out of the server pool
         * resulting in a 503 from the load balancer
         * @property hostname
         * @type {String}
         */
        this.hostname  = req.headers.host || pb.SiteService.getGlobalSiteContext().hostname;

        /**
         * @property activeTheme
         * @type {string}
         */
        this.activeTheme = null;

        /**
         * @property routeTheme
         * @type {object}
         */
        this.routeTheme = null;

        /**
         * @property errorCount
         * @type {number}
         */
        this.errorCount = 0;
    }
    AsyncEventEmitter.extend(RequestHandler);

    /**
     * A mapping that provides the interface type to parse the body based on the
     * route specification
     * @private
     * @static
     * @readonly
     * @property BODY_PARSER_MAP
     * @type {Object}
     */
    var BODY_PARSER_MAP = {
        'application/json': pb.JsonBodyParser,
        'application/x-www-form-urlencoded': pb.FormBodyParser,
        'multipart/form-data': pb.FormBodyParser
    };

    /**
     * Provides the list of directories that are publicly available
     * @private
     * @static
     * @readonly
     * @property PUBLIC_ROUTE_PREFIXES
     * @type {Array}
     */
    var PUBLIC_ROUTE_PREFIXES = ['/js/', '/css/', '/fonts/', '/img/', '/localization/', '/favicon.ico', '/docs/', '/bower_components/'];

    /**
     * The fallback theme (pencilblue)
     * @static
     * @property DEFAULT_THEME
     * @type {String}
     */
    RequestHandler.DEFAULT_THEME = pb.config.plugins.default;

    /**
     * The internal storage of routes after they are validated and processed.
     * @protected
     * @static
     * @property storage
     * @type {Array}
     */
    RequestHandler.storage = [];
    RequestHandler.index   = {};
    RequestHandler.sites = {};
    RequestHandler.redirectHosts = {};
    var GLOBAL_SITE = pb.SiteService.GLOBAL_SITE;
    /**
     * The internal storage of static routes after they are validated and processed.
     * @protected
     * @static
     * @property staticRoutes
     * @type {Object}
     */
    RequestHandler.staticRoutes = {};

    /**
     * The list of routes provided by the pencilblue plugin.  These routes are
     * loaded first to ensure defaults are in place before other plugins are
     * initialized.  In the future this will change so that all plugins are treated
     * equally.
     * @private
     * @static
     * @property CORE_ROUTES
     * @type {Array}
     */
    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.
     * @static
     * @method init
     */
    RequestHandler.init = function(){

        //iterate core routes adding them
        pb.log.debug('RequestHandler: Registering System Routes');
        util.forEach(RequestHandler.CORE_ROUTES, function(descriptor) {

            //register the route
            var result;
            try {
                result = RequestHandler.registerRoute(descriptor, RequestHandler.DEFAULT_THEME);
            }
            catch(e) {}
            if (!result) {
                pb.log.error('RequestHandler: Failed to register PB route: %s %s', descriptor.method, descriptor.path);
            }
        });
    };

    /**
     * Generates the controller callback object that will trigger the redirect
     * header to be sent back as part of the response.
     * @static
     * @method generateRedirect
     * @param {String} location The fully qualified or relative URL to be redirected to
     * @return {Object} The object for the controller to call back with.
     */
    RequestHandler.generateRedirect = function (location) {
        return {
            redirect: location
        };
    };

    /**
     * @static
     * @method loadSite
     * @param {Object} site
     */
    RequestHandler.loadSite = function(site) {
        RequestHandler.sites[site.hostname] = {
          active: site.active,
          uid: site.uid,
          displayName: site.displayName,
          hostname: site.hostname,
          defaultLocale: site.defaultLocale,
          supportedLocales: site.supportedLocales,
          prevHostnames: site.prevHostnames
        };
        //Populate RequestHandler.redirectHosts if this site has prevHostnames associated
        if (site.prevHostnames) {
            site.prevHostnames.forEach(function (oldHostname) {
                RequestHandler.redirectHosts[oldHostname] = site.hostname;
            });
        }
    };

    /**
     * @static
     * @method activateSite
     * @param {Object} site
     */
    RequestHandler.activateSite = function(site) {
        RequestHandler.sites[site.hostname].active = true;
    };

    /**
     * @static
     * @method deactivateSite
     * @param {Object} site
     */
    RequestHandler.deactivateSite = function(site) {
        RequestHandler.sites[site.hostname].active = false;
    };

    /**
     * Validates a route descriptor.  The specified object must have a "controller"
     * property that points to a valid file and the "path" property must specify a
     * valid URL path structure.
     * @static
     * @method isValidRoute
     * @param {Object} descriptor The object to validate
     * @param {String} descriptor.controller The file path to the controller file
     * @param {String} descriptor.path The URL path
     */
    RequestHandler.isValidRoute = function(descriptor) {
        return fs.existsSync(descriptor.controller) &&
            !util.isNullOrUndefined(descriptor.path);
    };

    /**
     * Unregisters all routes associated with a theme
     * @static
     * @method unregisterThemeRoutes
     * @param {String} theme The plugin/theme uid
     * @return {Integer} The number of routes removed
     */
    RequestHandler.unregisterThemeRoutes = function(theme, site) {
        //resolve the site
        if(!site)
        {
            site = GLOBAL_SITE;
        }

        var routesRemoved = 0;

        //pattern routes
        for (var i = 0; i < RequestHandler.storage.length; i++) {
            var path   = RequestHandler.storage[i].path;
            var result = RequestHandler.unregisterRoute(path, theme, site);
            if (result) {
                routesRemoved++;
            }
        }

        //static routes
        Object.keys(RequestHandler.staticRoutes).forEach(function(path) {
            var result = RequestHandler.unregisterRoute(path, theme, site);
            if (result) {
                routesRemoved++;
            }
        });
        return routesRemoved;
    };

    /**
     * Removes a route based on a URL path and theme UID
     * @static
     * @method unregisterRoute
     * @param {String} The URL path
     * @param {String} The theme that owns the route
     * @return {Boolean} TRUE if the route was found and removed, FALSE if not
     */
    RequestHandler.unregisterRoute = function(path, theme, site) {
        //resolve the site
        if(!site)
        {
            site = GLOBAL_SITE;
        }


        //get the pattern to check for
        var pattern    = null;
        var patternObj = RequestHandler.getRoutePattern(path);
        if (patternObj) {
            pattern = patternObj.pattern;
        }
        else {//invalid path provided
            return false;
        }

        //check if that pattern is registered for any theme
        var descriptor;
        if (RequestHandler.staticRoutes[path]) {
            descriptor = RequestHandler.staticRoutes[path];
        }
        else if (RequestHandler.index[pattern]) {
            descriptor = RequestHandler.storage[RequestHandler.index[pattern]];
        }
        else {
            //not a static route or pattern route
            return false;
        }

        //return false if specified site has no themes registered on that descriptor
        //return false if theme doesnt exist on descriptor for that site
        if (!descriptor || !descriptor.themes[site] || !descriptor.themes[site][theme]) {
            return false;
        }

        //remove from service
        delete descriptor.themes[site][theme];
        descriptor.themes[site].size--;
        if(descriptor.themes[site].size < 1) {
            delete descriptor.themes[site];
        }
        return true;
    };

    /**
     * Registers a route
     * @static
     * @method registerRoute
     * @param {Object} descriptor The route descriptor
     * @param {String} [descriptor.method='ALL'] The HTTP method associated with
     * the route
     * @param {String} descriptor.path The URL path for the route.  The route
     * supports wild cards a well as path variables (/get/:id)
     * @param {String} descriptor.controller The file path to the controller to
     * execute when the path is matched to an incoming request.
     * @param {Integer} [descriptor.access_level=0] Use global constants:
     * ACCESS_USER,ACCESS_WRITER,ACCESS_EDITOR,ACCESS_MANAGING_EDITOR,ACCESS_ADMINISTRATOR
     * @param {Boolean} [descriptor.setup_required=true] If true the system must have gone
     * through the setup process in order to pass validation
     * @param {Boolean} [descriptor.auth_required=false] If true, the user making the
     * request must have successfully authenticated against the system.
     * @param {String} [descriptor.content_type='text/html'] The content type header sent with the response
     * @param {Boolean} [descriptor.localization=false]
     * @param {String} theme The plugin/theme UID
     * @param {String} site The UID of site that owns the route
     * @return {Boolean} TRUE if the route was registered, FALSE if not
     */
    RequestHandler.registerRoute = function(descriptor, theme, site){
        //resolve empty site to global
        if(!site)
        {
            site = GLOBAL_SITE;
        }

        //validate route
        if (!RequestHandler.isValidRoute(descriptor)) {
            pb.log.error("RequestHandler: Route Validation Failed for: "+JSON.stringify(descriptor));
            return false;
        }

        //standardize http method (if exists) to upper case
        if (descriptor.method) {
            descriptor.method = descriptor.method.toUpperCase();
        }
        else {
            descriptor.method = 'ALL';
        }

        //make sure we get a valid prototype back
        var Controller = require(descriptor.controller)(pb);
        if (!Controller) {
            pb.log.error('RequestHandler: Failed to get a prototype back from the controller module. %s', JSON.stringify(descriptor));
            return false;
        }

        //register main route
        var result = _registerRoute(descriptor, theme, site, Controller);

        //now check if we should localize the route
        if (descriptor.localization) {

            var localizedDescriptor = util.clone(descriptor);
            localizedDescriptor.path = pb.UrlService.urlJoin('/:locale', descriptor.path);
            result = result && _registerRoute(localizedDescriptor, theme, site, Controller);
        }
        return result;
    };

    /**
     *
     * @private
     * @static
     * @method _registerRoute
     * @param {Object} descriptor
     * @param {String} theme
     * @param {String} site
     * @param {Function} Controller
     * @return {Boolean}
     */
    function _registerRoute(descriptor, theme, site, Controller) {
        //get pattern and path variables
        var patternObj = RequestHandler.getRoutePattern(descriptor.path);
        var pathVars   = patternObj.pathVars;
        var pattern    = patternObj.pattern;
        var isStatic   = Object.keys(pathVars).length === 0 && !patternObj.hasWildcard;

        //insert it
        var isNew = false;
        var routeDescriptor = null;
        if (isStatic && !util.isNullOrUndefined(RequestHandler.staticRoutes[descriptor.path])) {
            routeDescriptor = RequestHandler.staticRoutes[descriptor.path];
        }
        else if (!isStatic && !util.isNullOrUndefined(RequestHandler.index[pattern])) {

            //exists so find it
            for (var i = 0; i < RequestHandler.storage.length; i++) {
                var route = RequestHandler.storage[i];
                if (route.pattern === pattern) {
                    routeDescriptor = route;
                    break;
                }
            }
        }
        else{//does not exist so create it
            isNew = true;
            routeDescriptor = {
                path: patternObj.path,
                pattern: pattern,
                path_vars: pathVars,
                expression: new RegExp(pattern),
                themes: {}
            };
        }

        //if the site has no themes on this route, add it
        if(!routeDescriptor.themes[site])
        {
            routeDescriptor.themes[site] = {};
            routeDescriptor.themes[site].size = 0;
        }

        //set the descriptor for the theme and load the controller type
        if (!routeDescriptor.themes[site][theme]) {
            routeDescriptor.themes[site][theme] = {};
            routeDescriptor.themes[site].size++;
        }
        routeDescriptor.themes[site][theme][descriptor.method]            = descriptor;
        routeDescriptor.themes[site][theme][descriptor.method].controller = Controller;

       //only add the descriptor it is new.  We do it here because we need to
       //know that the controller is good.
        if (isNew) {
            //set them in storage
            if (isStatic) {
                RequestHandler.staticRoutes[descriptor.path] = routeDescriptor;
            }
            else {
                RequestHandler.index[pattern] = RequestHandler.storage.length;
                RequestHandler.storage.push(routeDescriptor);
            }
        }

        //log the result
        if (isStatic) {
            pb.log.debug('RequestHandler: Registered Static Route - Theme [%s] Path [%s][%s]', theme, descriptor.method, descriptor.path);
        }
        else {
            pb.log.debug('RequestHandler: Registered Route - Theme [%s] Path [%s][%s] Pattern [%s]', theme, descriptor.method, descriptor.path, pattern);
        }
        return true;
    }

    /**
     * Generates a regular expression based on the specified path.  In addition the
     * algorithm extracts any path variables that are included in the path.  Paths
     * can include two types of wild cards.  The traditional glob pattern style of
     * "/some/api/*" can be used as well as path variables ("/some/api/:action").
     * The path variables will be passed to the controllers.
     * @static
     * @method getRoutePattern
     * @param {String} path The URL path
     * @return {Object|null} An object containing three properties: The specified
     * "path". The generated regular expression "pattern" as a string. Lastly, a
     * hash of the path variables and their position in the path coorelating to its
     * depth in the path.
     */
    RequestHandler.getRoutePattern = function(path) {
        if (!path) {
            return null;
        }

        //clean up path
        if (path.indexOf('/') === 0) {
            path = path.substring(1);
        }
        if (path.lastIndexOf('/') === path.length - 1) {
            path = path.substring(0, path.length - 1);
        }

        //construct the pattern & extract path variables
        var pathVars    = {};
        var pattern     = '^';
        var hasWildcard = false;
        var pathPieces  = path.split('/');
        for (var i = 0; i < pathPieces.length; i++) {
            var piece = pathPieces[i];

            if (piece.indexOf(':') === 0) {
                var fieldName = piece.substring(1);
                pathVars[fieldName] = i + 1;
                pattern += '\/[^/]+';
            }
            else {
                if (piece.indexOf('*') >= 0) {
                    piece = piece.replace(/\*/g, '.*');

                    hasWildcard = true;
                }
                pattern += '\/'+piece;
            }
        }
        pattern += '[/]{0,1}$';

        return {
            path: path,
            pattern: pattern,
            pathVars: pathVars,
            hasWildcard: hasWildcard
        };
    };

    /**
     * Processes a request:
     * <ol>
     * 	<li>Initialize localization</li>
     * 	<li>if Public Route:
     * 		<ol>
     * 			<li>If Valid Content
     * 				<ol><li>Serve Public Content</li></ol>
     * 			</li>
     * 			<li>Else Serve 404</li>
     * 		</ol>
     * 	</li>
     * 	<li>Else Parse Cookies</li>
     * 	<li>Open/Create a session</li>
     * 	<li>Get Route</li>
     * </ol>
     * @method handleRequest
     */
    RequestHandler.prototype.handleRequest = function(){

        //fist things first check for public resource
        if (RequestHandler.isPublicRoute(this.url.pathname)) {
            return this.servePublicContent();
        }

        //check for session cookie
        var cookies = RequestHandler.parseCookies(this.req);
        this.req.headers[pb.SessionHandler.COOKIE_HEADER] = cookies;

        //open session
        var self = this;
        pb.session.open(this.req, function(err, session){
            if (util.isError(err)) {
                return self.serveError(err);
            }
            if (!session) {
                return self.serveError(new Error("The session object was not valid.  Unable to generate a session object based on request."));
            }
            //set the session id when no session has started or the current one has
            //expired.
            var sc = Object.keys(cookies).length === 0;
            var se = !sc && cookies.session_id !== session.uid;
            self.setSessionCookie =  sc || se;
            if (pb.log.isSilly()) {
                pb.log.silly("RequestHandler: Session ID [%s] Cookie SID [%s] Created [%s] Expired [%s]", session.uid, cookies.session_id, sc, se);
            }

            //continue processing
            self.onSessionRetrieved(err, session);
        });
    };

    /**
     * Derives the locale and localization instance.
     * @method deriveLocalization
     * @param {Object} context
     * @param {Object} [context.session]
     * @param {String} [context.routeLocalization]
     */
    RequestHandler.prototype.deriveLocalization = function(context) {
        var opts = {};

        var sources = [
            context.routeLocalization
        ];
        if (context.session) {
            sources.push(context.session.locale);
        }
        sources.push(this.req.headers[pb.Localization.ACCEPT_LANG_HEADER]);
        if (this.siteObj) {
            opts.supported = Object.keys(this.siteObj.supportedLocales);
            sources.push(this.siteObj.defaultLocale);
        }
        var localePrefStr = sources.reduce(function(prev, curr, i) {
            return prev + (curr ? (!!i && !!prev ? ',' : '') + curr : '');
        }, '');

        //get locale preference
        return new pb.Localization(localePrefStr, opts);
    };

    /**
     * Serves up public content from an absolute file path
     * @method servePublicContent
     * @param {String} [absolutePath] An absolute file path to the resource
     */
    RequestHandler.prototype.servePublicContent = function(absolutePath) {

        //check for provided path, then default if necessary
        if (util.isNullOrUndefined(absolutePath)) {
            absolutePath = path.join(pb.config.docRoot, 'public', this.url.pathname);
        }

        var self = this;
        fs.readFile(absolutePath, function(err, content){
            if (err) {
                return self.serve404();
            }

            //build response structure
            var data = {
                content: content
            };

            //guess at content-type
            var mimeType = mime.lookup(absolutePath);
            if (mimeType) {
                data.content_type = mimeType;
            }

            //send response
            self.writeResponse(data);
        });
    };

    /**
     * Attempts to derive the MIME type for a resource path based on the extension
     * of the path.
     * @deprecated since 0.8.0 Use mime.lookup instead
     * @static
     * @method getMimeFromPath
     * @param {string} resourcePath The file path to a resource
     * @return {String|undefined} The MIME type or NULL if could not be derived.
     */
    RequestHandler.getMimeFromPath = function(resourcePath) {
        return mime.lookup(resourcePath);
    };

    /**
     * Determines if the path is mapped to static resources
     * @static
     * @method isPublicRoute
     * @param {String} path URL path to a resource
     * @return {Boolean} TRUE if mapped to a public resource directory, FALSE if not
     */
    RequestHandler.isPublicRoute = function(path) {
        for (var i = 0; i < PUBLIC_ROUTE_PREFIXES.length; i++) {
            if (path.indexOf(PUBLIC_ROUTE_PREFIXES[i]) === 0) {
                return true;
            }
        }
        return false;
    };

    /**
     * Serves up a 404 page when the path specified by the incoming request does
     * not exist. This function <b>WILL</b> close the connection.
     * @method serve404
     */
    RequestHandler.prototype.serve404 = function() {
        var error = new Error('NOT FOUND');
        error.code = 404;
        this.serveError(error);
        if (pb.log.isSilly()) {
            pb.log.silly("RequestHandler: No Route Found, Sending 404 for URL="+this.url.href);
        }
    };

    /**
     * Serves up an error page.  The page is responsible for displaying an error page
     * @method serveError
     * @param {Error} err The failure that was generated by the executed controller
     * @return {Boolean} TRUE when the error is rendered, FALSE if the request had already been handled
     */
    RequestHandler.prototype.serveError = function(err, options) {
        if (this.resp.headerSent) {
            return false;
        }

        //bump the error count so handlers will know if we are recursively trying to handle errors.
        this.errorCount++;

        //retrieve the active theme.  Sometimes we don't have it such as in the case of the 404.
        var self = this;
        var getActiveTheme = function(cb){
            if (self.activeTheme) {
                return cb(null, self.activeTheme);
            }

            self.siteObj = self.siteObj || pb.SiteService.getGlobalSiteContext();
            var settingsService = pb.SettingServiceFactory.getService(pb.config.settings.use_memory, pb.config.settings.use_cache, self.siteObj.uid);
            settingsService.get('active_theme', function(err, activeTheme){
                self.activeTheme = activeTheme || pb.config.plugins.default;
                cb(null, self.activeTheme);
            });
        };

        getActiveTheme(function(error, activeTheme) {

            //build out params for handlers
            self.localizationService = self.localizationService || self.deriveLocalization({});
            var params = {
                mime: self.themeRoute && self.themeRoute.content_type ? self.themeRoute.content_type : 'text/html',
                error: err,
                request: self.req,
                localization: self.localizationService,
                activeTheme: activeTheme,
                reqHandler: self,
                errorCount: self.errorCount
            };

            //hand off to the formatters.  NOTE: the callback may not be called if
            //the handler chooses to fire off a controller.
            var handler = options.handler || function(data) {
                self.onRenderComplete(data);
            };
            pb.ErrorFormatters.formatForMime(params, function(error, result) {
                if (util.isError(error)) {
                    pb.log.error('RequestHandler: An error occurred attempting to render an error: %s', error.stack);
                }

                var data = {
                    reqHandler: self,
                    content: result.content,
                    content_type: result.mime,
                    code: err.code || 500
                };
                handler(data);
            });
        });

        return true;
    };

    /**
     * Called when the session has been retrieved.  Responsible for checking the
     * active theme.  It then retrieves the route object and passes it off to onThemeRetrieved.
     * @method onSessionRetrieved
     * @param {Error} err Any error that occurred while retrieving the session
     * @param {Object} session The session for the requesting entity
     */
    RequestHandler.prototype.onSessionRetrieved = function(err, session) {
        if (err) {
            this.onErrorOccurred(err);
            return;
        }

        //set the session
        this.session = session;

        var hostname = this.hostname;
        var siteObj = RequestHandler.sites[hostname];
        var redirectHost = RequestHandler.redirectHosts[hostname];

        // If we need to redirect to a different host
        if (!siteObj && redirectHost && RequestHandler.sites[redirectHost]) {
            return this.doRedirect(pb.SiteService.getHostWithProtocol(redirectHost), pb.HttpStatus.MOVED_PERMANENTLY);
        }
        this.siteObj = siteObj;

        //derive the localization. We do it here so that if the site isn't
        //available we can still have one available when we error out
        this.localizationService = this.deriveLocalization({ session: session });

        //make sure we have a site
        if (!siteObj) {
            var error = new Error("The host (" + hostname + ") has not been registered with a site. In single site mode, you must use your site root (" + pb.config.siteRoot + ").");
            pb.log.error(error);
            return this.serveError(error);
        }

        this.site = this.siteObj.uid;
        this.siteName = this.siteObj.displayName;
        //find the controller to hand off to
        var route = this.getRoute(this.url.pathname);
        if (route === null) {
            return this.serve404();
        }
        this.route = route;

        //get active theme
        var self = this;
        var settings = pb.SettingServiceFactory.getService(pb.config.settings.use_memory, pb.config.settings.use_cache, this.siteObj.uid);
        settings.get('active_theme', function(err, activeTheme){
            if (!activeTheme) {
                pb.log.warn("RequestHandler: The active theme is not set.  Defaulting to '%s'", RequestHandler.DEFAULT_THEME);
                activeTheme = RequestHandler.DEFAULT_THEME;
            }

            self.activeTheme = activeTheme;
            self.onThemeRetrieved(activeTheme, route);
        });
    };

    /**
     * Compares the path against the registered routes's to lookup the route object.
     * @method getRoute
     * @param {String} path The URL path for the incoming request
     * @return {Object} The route object or NULL if the path does not match any route
     */
    RequestHandler.prototype.getRoute = function(path) {

        //check static routes first.  It must be an exact match including
        //casing and any ending slash.
        var isSilly = pb.log.isSilly();
        var route   = RequestHandler.staticRoutes[path];
        if (!util.isNullOrUndefined(route)) {
            if(route.themes[this.siteObj.uid] || route.themes[GLOBAL_SITE]) {
                if (isSilly) {
                    pb.log.silly('RequestHandler: Found static route [%s]', path);
                }
                return route;
            }
        }

        //now do the hard work.  Iterate over the available patterns until a
        //pattern is found.
        for (var i = 0; i < RequestHandler.storage.length; i++) {

            var curr   = RequestHandler.storage[i];
            var result = curr.expression.test(path);

            if (isSilly) {
                pb.log.silly('RequestHandler: Comparing Path [%s] to Pattern [%s] Result [%s]', path, curr.pattern, result);
            }
            if (result) {
                if(curr.themes[this.siteObj.uid] || curr.themes[GLOBAL_SITE]) {
                    return curr;
                }
                break;
            }
        }

        //ensures we return null when route is not found for backward
        //compatibility.
        return null;
    };

    /**
     * Determines if the route supports the given HTTP method
     * @static
     * @method routeSupportsMethod
     * @param {Object} themeRoutes The route object that contains the specifics for
     * the theme variation of the route.
     * @param {String} method HTTP method
     */
    RequestHandler.routeSupportsMethod = function(themeRoutes, method) {
        method = method.toUpperCase();
        return !util.isNullOrUndefined(themeRoutes[method]);
    };

    /**
     * Determines if a route supports a particular theme and HTTP method
     * @static
     * @method routeSupportsTheme
     * @param {Object} route
     * @param {String} theme The theme
     * @param {String} method HTTP method
     * @param {string} site current site
     * @return {Boolean}
     */
    RequestHandler.routeSupportsSiteTheme = function(route, theme, method, site) {
        return !util.isNullOrUndefined(route.themes[site]) &&
            !util.isNullOrUndefined(route.themes[site][theme]) &&
            RequestHandler.routeSupportsMethod(route.themes[site][theme], method);
    };

    /**
     * @static
     * @method routeSupportsGlobalTheme
     * @param {Object} route
     * @param {String} theme
     * @param {String} method
     */
    RequestHandler.routeSupportsGlobalTheme = function(route, theme, method) {
        return RequestHandler.routeSupportsSiteTheme(route, theme, method, GLOBAL_SITE);
    };

    /**
     * Determines the theme that will be executed for the route.
     * The themes will be prioritized as: active theme, pencilblue, followed by
     * iterating over all other inherited themes.
     * @method getRouteTheme
     * @param {String} activeTheme
     * @param {Object} route
     * @return {Object} An object with two properties: theme and method
     */
    RequestHandler.prototype.getRouteTheme = function(activeTheme, route) {
        var obj = {theme: null, method: null, site: null};

        var methods = [this.req.method, 'ALL'];
        for (var i = 0; i < methods.length; i++) {

            //check for themed route
            var themesToCheck = [activeTheme, RequestHandler.DEFAULT_THEME];
            if (this.siteObj.uid in route.themes) {
                util.arrayPushAll(Object.keys(route.themes[this.siteObj.uid]), themesToCheck);
            }
            if (!pb.SiteService.isGlobal(this.siteObj.uid) && (pb.SiteService.GLOBAL_SITE in route.themes)) {
                util.arrayPushAll(Object.keys(route.themes[pb.SiteService.GLOBAL_SITE]), themesToCheck);
            }
            themesToCheck = _.uniq(themesToCheck);
            for (var j = 0; j < themesToCheck.length; j++) {

                //see if theme supports method and provides support
                if (RequestHandler.routeSupportsSiteTheme(route, themesToCheck[j], methods[i], this.siteObj.uid)) {
                    obj.theme  = themesToCheck[j];
                    obj.method = methods[i];
                    obj.site   = this.siteObj.uid;
                    return obj;
                } else if (RequestHandler.routeSupportsGlobalTheme(route, themesToCheck[j], methods[i])) {
                    obj.theme  = themesToCheck[j];
                    obj.method = methods[i];
                    obj.site   = GLOBAL_SITE;
                    return obj;
                }
            }
        }
        return obj;
    };

    /**
     *
     * @method onThemeRetrieved
     * @param {String} activeTheme
     * @param {Object} route
     */
    RequestHandler.prototype.onThemeRetrieved = function(activeTheme, route) {
        var self = this;

        //check for unregistered route for theme
        var rt = this.routeTheme = this.getRouteTheme(activeTheme, route);

        if (pb.log.isSilly()) {
            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);
        }

        //make sure we let the plugins hook in.
        this.emitThemeRouteRetrieved(function(err) {
            if (util.isError(err)) {
                return self.serveError(err);
            }

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

            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();
                }
            }

            //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
     * @param {String} activeTheme The user set active theme
     * @param {String} routeTheme The plugin/theme who's controller will handle the request
     * @param {String} method
     * @param {String} site
     * @param {Object} route
     */
    RequestHandler.prototype.onSecurityChecksPassed = function(activeTheme, routeTheme, method, site, route) {

        //extract path variables
        var pathVars = this.getPathVariables(route);
        if (typeof pathVars.locale !== 'undefined') {
            if (!this.siteObj.supportedLocales[pathVars.locale]) {

                //TODO make this check more general
                return this.serve404();
            }

            //update the localization
            this.localizationService = this.deriveLocalization({ session: this.session, routeLocalization: pathVars.locale });
        }

        //instantiate controller
        var ControllerType  = route.themes[site][routeTheme][method].controller;
        var cInstance       = new ControllerType();

        //execute it
        var context = {
            pathVars: pathVars,
            cInstance: cInstance,
            themeRoute: route.themes[site][routeTheme][method],
            activeTheme: activeTheme
        };
        this.doRender(context);
    };

    /**
     *
     * @method getPathVariables
     * @param {Object} route
     * @param {Object} route.path_vars
     */
    RequestHandler.prototype.getPathVariables = function(route) {
        var pathVars = {};
        var pathParts = this.url.pathname.split('/');
        Object.keys(route.path_vars).forEach(function(field) {
            pathVars[field] = pathParts[route.path_vars[field]];
        });
        return pathVars;
    };

    /**
     * Begins the rendering process by initializing the controller.  This is done
     * by gathering all initialization parameters and calling the controller's
     * "init" function.
     * @method doRender
     * @param {object} context
     * @param {Object} context.pathVars The URL path's variables
     * @param {BaseController} context.cInstance An instance of the controller to be executed
     * @param {Object} context.themeRoute
     * @param {String} context.activeTheme The user set active theme
     */
    RequestHandler.prototype.doRender = function(context) {
        var self  = this;

        //attempt to parse body
        this.parseBody(context.themeRoute.request_body, function(err, body) {
            if (util.isError(err)) {
                err.code = 400;
                return self.serveError(err);
            }

            //build out properties & merge in any that are special to this call
            var props = {
                request_handler: self,
                request: self.req,
                response: self.resp,
                session: self.session,
                localization_service: self.localizationService,
                path_vars: context.pathVars,
                pathVars: context.pathVars,
                query: self.url.query,
                body: body,
                site: self.site,
                siteObj: self.siteObj,
                siteName: self.siteName,
                activeTheme: context.activeTheme || self.activeTheme,
                routeLocalized: !!context.themeRoute.localization
            };
            if (util.isObject(context.initParams)) {
                util.merge(context.initParams, props);
            }
            var d = domain.create();
            d.add(context.cInstance);
            d.run(function () {
                process.nextTick(function () {
                    //initialize the controller
                    context.cInstance.init(props, function () {
                        self.onControllerInitialized(context.cInstance, context.themeRoute);
                    });
                });
            });
            d.on('error', function (err) {
                pb.log.error("RequestHandler: An error occurred during controller execution. URL=[%s:%s] ROUTE=%s\n%s", self.req.method, self.req.url, JSON.stringify(self.route), err.stack);
                self.serveError(err);
            });
        });
    };

    /**
     * Parses the incoming request body when the body type specified matches one of
     * those explicitly allowed by the rotue.
     * @method parseBody
     * @param {Array} mimes An array of allowed MIME strings.
     * @param {Function} cb A callback that takes 2 parameters: An Error, if
     * occurred and the parsed body.  The parsed value is often an object but the
     * value is dependent on the parser selected by the content type.
     */
    RequestHandler.prototype.parseBody = function(mimes, cb) {

        //we don't force a mime.  Controllers have the ability to handle this
        //themselves.
        if (!util.isArray(mimes)) {
            return cb(null, null);
        }

        //verify that the content type is acceptable
        var contentType = this.req.headers['content-type'];
        if (contentType) {

            //we split on ';' to check for multipart encoding since it specifies a
            //boundary
            contentType = contentType.split(';')[0];
            if (mimes.indexOf(contentType) === -1) {
                //a type was specified but its not accepted by the controller
                //TODO return HTTP 415
                return cb(null, null);
            }
        }

        //create the parser
        var BodyParser = BODY_PARSER_MAP[contentType];
        if (!BodyParser) {
            pb.log.silly('RequestHandler: no handler was found to parse the body type [%s]', contentType);
            return cb(null, null);
        }

        //execute the parsing
        var self = this;
        var d = domain.create();
        d.on('error', cb);
        d.run(function() {
            process.nextTick(function() {

                //initialize the parser and parse content
                var parser = new BodyParser();
                parser.parse(self.req, cb);
            });
        });
    };

    /**
     *
     * @method onControllerInitialized
     * @param {BaseController} controller
     * @param {object} themeRoute
     */
    RequestHandler.prototype.onControllerInitialized = function (controller, themeRoute) {
        var self = this;

        controller[themeRoute.handler ? themeRoute.handler : 'render'](function (result) {
            self.onRenderComplete(result);
        });
    };

    /**
     *
     * @method onRenderComplete
     * @param {Error|object} data
     * @param {string} [data.redirect]
     * @param {Integer} [data.code
     */
    RequestHandler.prototype.onRenderComplete = function(data){
        if (util.isError(data)) {
            return this.serveError(data);
        }

        //set cookie
        var cookies = new Cookies(this.req, this.resp);
        if (this.setSessionCookie) {
            try{
                cookies.set(pb.SessionHandler.COOKIE_NAME, this.session.uid, pb.SessionHandler.getSessionCookie(this.session));
            }
            catch(e){
                pb.log.error('RequestHandler: %s', e.stack);
            }
        }

        //do any necessary redirects
        var doRedirect = typeof data.redirect !== 'undefined';
        if(doRedirect) {
            this.doRedirect(data.redirect, data.statusCode);
        }
        else {
            //output data here
            this.writeResponse(data);
        }

        //calculate response time
        if (pb.log.isDebug()) {
            pb.log.debug("Response Time: "+(new Date().getTime() - this.startTime)+
                    "ms URL=["+this.req.method+']'+
                    this.req.url+(doRedirect ? ' Redirect='+data.redirect : '') +
                    (typeof data.code === 'undefined' ? '' : ' CODE='+data.code));
        }

        //close session after data sent
        //public content doesn't require a session so in order to not error out we
        //check if the session exists first.
        if (this.session) {
            var self = this;
            pb.session.close(this.session, function(err/*, result*/) {
                if (util.isError(err)) {
                    pb.log.warn('RequestHandler: Failed to close session [%s]', self.session.uid);
                }
            });
        }
    };

    /**
     *
     * @method writeResponse
     * @param {Object} data
     */
    RequestHandler.prototype.writeResponse = function(data){
        var self = this;

        //infer a response code when not provided
        if(typeof data.code === 'undefined'){
            data.code = 200;
        }

        // If a response code other than 200 is provided, force that code into the head
        var contentType = 'text/html';
        if (typeof data.content_type !== 'undefined') {
            contentType = data.content_type;
        }
        else if (this.themeRoute && this.themeRoute.content_type !== undefined) {
            contentType = this.themeRoute.content_type;
        }

        //send response
        //the catch allows us to prevent any plugins that callback trwice from
        //screwing us over due to the attempt to write headers twice.
        try {
            //set any custom headers
            if (util.isObject(data.headers)) {
                Object.keys(data.headers).forEach(function(header) {
                    self.resp.setHeader(header, data.headers[header]);
                });
            }
            if (pb.config.server.x_powered_by) {
                this.resp.setHeader('x-powered-by', pb.config.server.x_powered_by);
            }
            this.resp.setHeader('content-type', contentType);
            this.resp.writeHead(data.code);

            //write content
            var content = data.content;
            if (!Buffer.isBuffer(content) && util.isObject(data.content)) {
                content = JSON.stringify(content);
            }
            this.resp.end(content);
        }
        catch(e) {
            pb.log.error('RequestHandler: '+e.stack);
        }
    };

    /**
     * Creates a cookie string
     * @method writeCookie
     * @param {Object} descriptor The pieces of the cookie that are to be included
     * in the string.  These pieces are represented as key value pairs.  Each value
     * will be serialized via its implicity "toString" function.
     * @param {String} [cookieStr=''] The current cookie string if it exists
     * @return {String} The cookie represented as a string
     */
    RequestHandler.prototype.writeCookie = function(descriptor, cookieStr){
        return Object.keys(descriptor).reduce(function(cs, key) {
            return cookieStr + key + '=' + descriptor[key]+'; ';
        }, cookieStr || '');
    };

    /**
     * Verifies that the incoming request meets all necessary security critiera
     * @method checkSecurity
     * @param {String} activeTheme
     * @param {String} method
     * @param {String} site
     * @param {Function} cb
     */
    RequestHandler.prototype.checkSecurity = function(activeTheme, method, site, cb){
        var self        = this;
        this.themeRoute = this.route.themes[site][activeTheme][method];

        //verify if setup is needed
        var checkSystemSetup = function(callback) {
            var ctx = {
                themeRoute: self.themeRoute
            };
            self.checkSystemSetup(ctx, function(err, result) {
                callback(result.success ? null : result, result);
            });
        };

        var checkRequiresAuth = function(callback) {
            var ctx = {
                themeRoute: self.themeRoute,
                session: self.session,
                req: self.req,
                hostname: self.hostname,
                url: self.url
            };
            var result = RequestHandler.checkRequiresAuth(ctx);
            callback(result.success ? null : result, result);
        };

        var checkAdminLevel = function(callback) {
            var ctx = {
                themeRoute: self.themeRoute,
                session: self.session
            };
            var result = RequestHandler.checkAdminLevel(ctx);
            callback(result.success ? null : result, result);
        };

        var checkPermissions = function(callback) {
            var ctx = {
                themeRoute: self.themeRoute,
                session: self.session
            };
            var result = RequestHandler.checkPermissions(ctx);
            callback(result.success ? null : result, result);
        };

        var tasks = {
            checkSystemSetup: checkSystemSetup,
            checkRequiresAuth: checkRequiresAuth,
            checkAdminLevel: checkAdminLevel,
            checkPermissions: checkPermissions
        };
        async.series(tasks, function(err, results){
            if (err) {
                cb(err, {success: false, results: results});
                return;
            }

            cb(null, {success: true, results: results});
        });
    };

    RequestHandler.prototype.checkSystemSetup = function(context, cb) {
        var result = {success: true};
        if (context.themeRoute.setup_required !== undefined && !context.themeRoute.setup_required) {
            return cb(null, result);
        }
        pb.settings.get('system_initialized', function(err, isSetup){

            //verify system init
            if (!isSetup) {
                result.success = false;
                result.redirect = '/setup';
            }
            cb(err, result);
        });
    };

    /**
     *
     * @method doRedirect
     * @param {String} location
     * @param {Integer} [statusCode=302]
     */
    RequestHandler.prototype.doRedirect = function(location, statusCode) {
        this.resp.statusCode = statusCode || pb.HttpStatus.MOVED_TEMPORARILY;
        this.resp.setHeader("Location", location);
        this.resp.end();
    };

    /**
     *
     * @method onErrorOccurred
     * @param {Error} err
     */
    RequestHandler.prototype.onErrorOccurred = function(err){
        throw err;
    };

    /**
     * Parses cookies passed for a request
     * @static
     * @method parseCookies
     * @param {Request} req
     * @return {Object}
     */
    RequestHandler.parseCookies = function(req){

        var parsedCookies = {};
        if (req.headers.cookie) {

            var cookieParameters = req.headers.cookie.split(';');
            for(var i = 0; i < cookieParameters.length; i++)  {

                var keyVal = cookieParameters[i].split('=');
                parsedCookies[keyVal[0]] = keyVal[1];
            }
        }
        return parsedCookies;
    };

    /**
     * Checks to see if the URL exists in the current context of the system
     * @static
     * @method urlExists
     * @param {String} url
     * @param {string} id
     * @param {string} [site]
     * @param {function} cb (Error, boolean)
     */
    RequestHandler.urlExists = function(url, id, site, cb) {
        var dao = new pb.DAO();
        if(typeof site === 'function') {
            cb = site;
            site = undefined;
        }
        var getTask = function(collection) {
            return function (callback) {
                var where = {url: url};
                if(site) {
                    where.site = site;
                }
                if (id) {
                    where[pb.DAO.getIdField()] = pb.DAO.getNotIdField(id);
                }
                dao.count(collection, where, function(err, count) {
                    if(util.isError(err) || count > 0) {
                        callback(true, count);
                    }
                    else {
                        callback(null, count);
                    }
                });
            };
        };
        async.series([getTask('article'), getTask('page')], function(err/*, results*/){
            cb(err, err !== null);
        });
    };

    /**
     * Determines if the provided URL pathname "/admin/content/articles" is a valid admin URL.
     * @static
     * @method isAdminURL
     * @param {String} urlPath
     * @return {boolean}
     */
    RequestHandler.isAdminURL = function(urlPath) {
        if (urlPath !== null) {

            var index = urlPath.indexOf('/');
            if (index === 0 && urlPath.length > 0) {
                urlPath = urlPath.substring(1);
            }

            var pieces = urlPath.split('/');
            return pieces.length > 0 && pieces[0].indexOf('admin') === 0;
        }
        return false;
    };

    /**
     *
     * @static
     * @method isSystemSafeURL
     * @param {String} url
     * @param {String} id
     * @param {string} [site]
     * @param {Function} cb
     */
    RequestHandler.isSystemSafeURL = function(url, id, site, cb) {
        if(typeof site === 'function') {
            cb = site;
            site = undefined;
        }
        if (url === null || RequestHandler.isAdminURL(url)) {
            return cb(null, false);
        }
        RequestHandler.urlExists(url, id, site, function(err, exists){
            cb(err, !exists);
        });
    };

    /**
     * Registers a body parser prototype for the specified mime
     * @static
     * @method registerBodyParser
     * @param {String} mime A non empty string representing the mime type that the prototype can parse
     * @param {Function} prototype A prototype that can have an instance created and parse the specified mime type
     * @return {Boolean} TRUE if the body parser was registered, FALSE if not
     */
    RequestHandler.registerBodyParser = function(mime, prototype) {
        if (!pb.validation.isNonEmptyStr(mime, true) || !util.isFunction(prototype)) {
            return false;
        }

        //set the prototype handler
        BODY_PARSER_MAP[mime] = prototype;
        return true;
    };

    /**
     * Retrieves the body parser mapping
     * @static
     * @method getBodyParsers
     * @return {Object} MIME string as the key and parser as the value
     */
    RequestHandler.getBodyParsers = function() {
        return util.merge(BODY_PARSER_MAP, {});
    };

    /**
     * @static
     * @method checkPermissions
     * @param {object} context
     * @param {object} context.themeRoute
     * @param {Array} context.themeRoute.permissions
     * @param {object} context.session
     * @param {object} context.session.authentication
     * @param {object} context.session.authentication.user
     * @param {object} context.session.authentication.user.permissions
     * @returns {{success: boolean}}
     */
    RequestHandler.checkPermissions = function(context) {

        var result   = {success: true};
        var reqPerms = context.themeRoute.permissions;
        var auth     = context.session.authentication;console.log('PermCheck: ', auth);
        if (auth && auth.user &&
            auth.admin_level !== pb.SecurityService.ACCESS_ADMINISTRATOR &&
            auth.user.permissions &&
            util.isArray(reqPerms)) {

            var permMap = auth.user.permissions;
            for(var i = 0; i < reqPerms.length; i++) {

                if (!permMap[reqPerms[i]]) {
                    result.success = false;
                    result.content = '403 Forbidden';
                    result.code    = HttpStatusCodes.FORBIDDEN;
                    break;
                }
            }
        }
        return result;
    };

    /**
     * @static
     * @method checkAdminLevel
     * @param {object} context
     * @param {object} context.themeRoute
     * @param {number} context.themeRoute.access_level
     * @param {object} context.session
     * @param {object} context.session.authentication
     * @param {number} context.session.authentication.admin_level
     * @returns {{success: boolean}}
     */
    RequestHandler.checkAdminLevel = function(context) {

        var result = {success: true};
        if (typeof context.themeRoute.access_level !== 'undefined') {

            if (context.session.authentication.admin_level < context.themeRoute.access_level) {
                result.success = false;
                result.content = '403 Forbidden';
                result.code    = 403;
            }
        }
        return result;
    };

    /**
     * @static
     * @method checkRequiresAuth
     * @param {object} context
     * @param {object} context.themeRoute
     * @param {boolean} context.themeRotue.auth_required
     * @param {object} context.session
     * @param {object} context.session.authentication
     * @param {number} context.session.authentication.user_id
     * @param {Request} context.req
     * @param {string} context.hostname
     * @param {object} context.url
     * @param {string} context.url.href
     * @returns {{success: boolean}}
     */
    RequestHandler.checkRequiresAuth = function(context) {

        var result = {success: true};
        if (context.themeRoute.auth_required === true) {

            if (context.session.authentication.user_id === null || context.session.authentication.user_id === undefined) {
                result.success  = false;
                result.redirect = RequestHandler.isAdminURL(context.url.pathname) ? '/admin/login' : '/user/login';
                context.session.on_login = context.req.method.toLowerCase() === 'get' ? context.url.href :
                    pb.UrlService.createSystemUrl('/admin', { hostname: context.hostname });
            }
        }
        return result;
    };

    /**
     * Builds out the context that is passed to a controller
     * @static
     * @method buildControllerContext
     * @param {Request} req
     * @param {Response} res
     * @param {object} extraData
     * @returns {Object}
     */
    RequestHandler.buildControllerContext = function(req, res, extraData) {
        return util.merge(extraData || {}, {
            request_handler: req.handler,
            request: req,
            response: res,
            session: req.session,
            localization_service: req.localizationService,
            path_vars: req.pathVars,
            pathVars: req.pathVars,
            query: req.handler.url.query,
            body: req.body,
            site: req.site,
            siteObj: req.siteObj,
            siteName: req.siteName,
            activeTheme: req.activeTheme,
            routeLocalized: !!req.routeTheme.localization
        });
    };

    return RequestHandler;
};