API Docs for: 0.8.0
Show:

File: include/error/formatters/error_formatters.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 path        = require('path');
var HttpStatusCodes = require('http-status-codes');
var XmlErrorFormatter = require('./xml_error_formatter');

module.exports = function(pb) {

    //pb dependencies
    var util = pb.util;

    /**
     * Provides functions and mechanisms to serialize errors
     * @class ErrorFormatters
     * @constructor
     */
    function ErrorFormatters() {}

    /**
     * The fallback MIME type
     * @private
     * @static
     * @readonly
     * @property DEFAULT_MIME
     * @type {String}
     */
    var DEFAULT_MIME = 'text/html';

    /**
     * Converts an error to a plain object that can be serialized
     * @private
     * @static
     * @method convertToObject
     * @param {Object} params
     * @return {Object}
     */
    var convertToObject = function(params) {
        var content = {
            code: params.error.code,
            message: params.error.message
        };
        if (pb.config.logging.showErrors) {
            content.stack = params.error.stack;
        }
        if (params.error.code === HttpStatusCodes.BAD_REQUEST) {
            delete content.stack;
            content.validationErrors = params.error.validationErrors;
        }
        return content;
    };

    /**
     * @private
     * @static
     * @property failedControllerPaths
     * @type {Object}
     */
    var failedControllerPaths = {};

    /**
     * Serializes an error as JSON
     * @static
     * @method html
     * @param {Object} params
     * @param {String} params.mime The MIME type of the format to render
     * @param {Error} params.error The error to be rendered
     * @param {Request} [params.request]
     * @param {Localization} [params.localization]
     * @param {Function} cb
     */
    ErrorFormatters.json = function(params, cb){
        cb(
            null,
            JSON.stringify(convertToObject(params))
        );
    };

    /**
     * Serializes an error as HTML
     * @static
     * @method html
     * @param {Object} params
     * @param {String} params.mime The MIME type of the format to render
     * @param {Error} params.error The error to be rendered
     * @param {Request} [params.request]
     * @param {Localization} [params.localization]
     * @param {Function} cb
     */
    ErrorFormatters.html = function(params, cb) {
        if (params.errorCount > 1) {

            //we know we've hit a recursive error situation.  Bail out
            return cb(
                null,
                '<html><body><h2>Whoops! Something unexpected happened.</h2><br/><pre>' +
                params.error.stack +
                '</pre></body></html>'
            );
        }

        //let the default error controller handle it.
        var code = params.error.code || HttpStatusCodes.INTERNAL_SERVER_ERROR;
        var ErrorController  = null;
        var paths = [
            path.join(pb.config.docRoot, 'plugins', params.activeTheme, 'controllers/error', code + '.js'),
            path.join(pb.config.docRoot, 'plugins', params.activeTheme, 'controllers/error/index.js')
        ];
        if (params.activeTheme !== pb.config.plugins.default) {
            paths.push(path.join(pb.config.docRoot, 'plugins', params.activeTheme, 'controllers/error', code + '.js'));
        }
        paths.push(path.join(pb.config.docRoot, 'controllers/error_controller.js'));

        //iterate over paths until you find a good one
        for (var i = 0; i < paths.length; i++) {
            if (failedControllerPaths[paths[i]]) {
                //we've seen it and it didn't exist or had a syntax error.  Moving on!
                continue;
            }

            //attempt to load the controller
            try {
                ErrorController = require(paths[i])(pb);
                break;
            }
            catch(e){

                //we failed so make sure don't do that again...
                failedControllerPaths[paths[i]] = true;
            }
        }
        params.request.controllerInstance = new ErrorController();
        params.request.controllerInstance.error = params.error;
        params.request.themeRoute = params.request.themeRoute || {};
        params.request.routeTheme = params.request.routeTheme || {};
        params.request.siteObj = params.request.siteObj || pb.SiteService.getGlobalSiteContext();
        params.request.themeRoute.handler = 'render';
        params.request.router.continueAfter('parseRequestBody');
    };

    /**
     * Serializes an error as XML
     * @static
     * @method html
     * @param {Object} params
     * @param {String} params.mime The MIME type of the format to render
     * @param {Error} params.error The error to be rendered
     * @param {Request} [params.request]
     * @param {Localization} [params.localization]
     * @param {Function} cb
     */
    ErrorFormatters.xml = function(params, cb) {

        var objToSerialize = convertToObject(params);
        cb(null, XmlErrorFormatter.serialize(objToSerialize));
    };

    /**
     * Registers a function to be mapped to a given MIME type.  The function
     * will be expected to serialize any given Error to the format specified by
     * the MIME type
     * @static
     * @method register
     * @param {String} mime The mime type to register the provider for
     * @param {Function} A function that takes two parameters.  The first is an
     * object that provides the error and the second parameter is the callback.
     * @return {Boolean} TRUE when the provider was registered, FALSE if not
     */
    ErrorFormatters.register = function(mime, formatterFunction) {
        if (!util.isString(mime) || !util.isFunction(formatterFunction)) {
            return false;
        }
        MIME_MAP[mime] = formatterFunction;
        return true;
    };

    /**
     * Unregisters the provider for the given MIME type.  If a default MIME
     * type is specified the current formatter will be unregistered and set to
     * the default implementation
     * @static
     * @method unregister
     * @param {String} mime The MIME type to unregister
     * @return {Boolean} TRUE when the provider was found and unregistered,
     * FALSE if not
     */
    ErrorFormatters.unregister = function(mime) {
        if (util.isFunction(MIME_MAP[mime])) {
            delete MIME_MAP[mime];

            //set the defaults back if we have them
            if (util.isFunction(DEFAULTS[mime])) {
                MIME_MAP[mime] = DEFAULTS[mime];
            }
            return true;
        }
        return false;
    };

    /**
     * Formats an error for the provided MIME type
     * @static
     * @method formatForMime
     * @param {Object} params
     * @param {String} params.mime The MIME type of the format to render
     * @param {Error} params.error The error to be rendered
     * @param {Request} [params.request]
     * @param {Localization} [params.localization]
     * @param {Function} cb
     */
    ErrorFormatters.formatForMime = function(params, cb) {
        if (!util.isObject(params)) {
            return cb(new Error('The params parameter must be an object'));
        }
        else if (!util.isString(params.mime)) {
            return cb(new Error('The params.mime parameter must be a string'));
        }
        else if (!util.isError(params.error)) {
            return cb(new Error('The params.error parameter must be an Error'));
        }

        //find the formatter, fall back to HTML if not provided
        var mime      = params.mime;
        var formatter = MIME_MAP[mime];
        if (util.isNullOrUndefined(formatter)) {
            mime      = DEFAULT_MIME;
            formatter = MIME_MAP[mime];
        }

        //execute the formatter
        formatter(params, function(err, content) {
            cb(
                err,
                {
                    mime: mime,
                    content: content
                }
            );
        });
    };

    /**
     * Retrieves the formatter for the specified MIME type
     * @static
     * @method get
     * @param {String} mime
     * @return {Function} formatter for the specified MIME. 'undefined' if does
     * not exist.
     */
    ErrorFormatters.get = function(mime) {
        return MIME_MAP[mime];
    };

    /**
     * Contains the default mapping of MIME type to function that will serialize
     * the error to that format
     * @private
     * @static
     * @property DEFAULTS
     * @type {Object}
     */
    var DEFAULTS = Object.freeze({
        'application/json': ErrorFormatters.json,
        'text/json': ErrorFormatters.json,
        'text/html': ErrorFormatters.html,
        'application/xml': ErrorFormatters.xml,
        'text/xml': ErrorFormatters.xml
    });

    /**
     * Contains the mapping of MIME type to function that will serialize the
     * error to that format
     * @private
     * @static
     * @property MIME_MAP
     * @type {Object}
     */
    var MIME_MAP = util.merge(DEFAULTS, {});

    return ErrorFormatters;
};