API Docs for: 0.8.0
Show:

File: include/service/entities/plugin_service.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 fs      = require('fs');
var npm     = require('npm');
var path    = require('path');
var async   = require('async');
var domain  = require('domain');
var semver  = require('semver');
var util    = require('../../util.js');

module.exports = function PluginServiceModule(pb) {

    /**
     * @private
     * @static
     * @readonly
     * @property GLOBAL_SITE
     * @type {String}
     */
    var GLOBAL_SITE = pb.SiteService.GLOBAL_SITE;

    /**
     * PluginService - Provides functions for interacting with plugins.
     * Install/uninstall, setting retrieval, plugin retrieval, etc.
     *
     * @class PluginService
     * @constructor
     * @param {Object} options
     * @param {String} [options.site]
     * @module Services
     * @submodule Entities
     */
    function PluginService(options){
        if (!util.isObject(options)) {
            options = {};
        }

        /**
         * @property site
         * @type {String}
         */
        this.site = options.site || GLOBAL_SITE;

        /**
         * @property _pluginRepository
         * @type {PluginRepository}
         */
        this._pluginRepository = pb.PluginRepository;

        //construct settings services
        var caching = pb.config.plugins.caching;

        /**
         * A setting service that sets and retrieves the settings for plugins
         * @property pluginSettingsService
         * @type {SimpleLayeredService}
         */
        this.pluginSettingsService = PluginService.genSettingsService('plugin_settings', caching.use_memory, caching.use_cache, 'PluginSettingService', this.site);

        /**
         * A setting service that sets and retrieves the settings for plugins
         * @property pluginSettingsService
         * @type {SimpleLayeredService}
         */
        this.themeSettingsService  = PluginService.genSettingsService('theme_settings', caching.use_memory, caching.use_cache, 'ThemeSettingService', this.site);
    }

    // Constants
    /**
     * The absolute path to the plugins directory for this PecilBlue installation
     * @property PLUGINS_DIR
     * @type {String}
     */
    var PLUGINS_DIR = path.join(pb.config.docRoot, 'plugins');

    /**
     * The name of the file that defines the plugin's properties
     * @property DETAILS_FILE_NAME
     * @type {String}
     */
    var DETAILS_FILE_NAME = 'details.json';

    /**
     * The name of the directory for each plugin that contains the public resources
     * @property PUBLIC_DIR_NAME
     * @type {String}
     */
    var PUBLIC_DIR_NAME   = 'public';

    // Statics
    /**
     * A hash of the plugins that are installed and active in this instance of PB.
     * @property ACTIVE_PLUGINS
     * @type {Object}
     */
    var ACTIVE_PLUGINS = {};

    /**
     * A hash of shared plugin information used during plugin initialization
     * @property PLUGIN_INIT_CACHE;
     * @type {Object}
     */
    var PLUGIN_INIT_CACHE = {};

    /**
     * Retrieves the path to the active fav icon.
     * @method getActiveIcon
     * @param {Function} cb A callback that provides two parameters: cb(Error, URL_PATH_TO_ICON)
     */
    PluginService.prototype.getActiveIcon = function(cb) {
        var self = this;
        var settings = pb.SettingServiceFactory.getService(pb.config.settings.use_memory, pb.config.settings.use_cache, this.site);
        settings.get('active_theme', function(err, theme) {
            var active_theme = PluginService.getPluginForSite(theme, self.site);
            cb(err, active_theme && active_theme.icon ? active_theme.icon : '/favicon.ico');
        });
    };

    /**
     * Remove the active plugin entry from the current PB process.
     * NOTE: it is not recommended to call this directly.
     * @static
     * @method deactivatePlugin
     * @param {String} pluginUid
     * @param {string} site
     * @return {Boolean}
     */
    PluginService.deactivatePlugin = function(pluginUid, site) {
        if (!pb.ValidationService.isNonEmptyStr(pluginUid)) {
            throw new Error('A non-existent or empty plugin UID was passed');
        }

        if(!site) {
            site = GLOBAL_SITE;
        }

        if (ACTIVE_PLUGINS[site] && ACTIVE_PLUGINS[site][pluginUid]) {
            delete ACTIVE_PLUGINS[site][pluginUid];
            return true;
        }
        return false;
    };

    /**
     * Activates a plugin based on the UID and the provided spec
     * @static
     * @method activatePlugin
     * @param {string} pluginUid
     * @param {object} pluginSpec
     * @param {function} pluginSpec.main_module
     * @param {string} pluginSpec.public_dir
     * @param {object} pluginSpec.permissions
     * @param {Array} pluginSpec.templates
     * @param {string} pluginSpec.icon
     * @param {object} pluginSpec.services
     * @param {string} site
     * @returns {boolean} TRUE if the site is set as active and FALSE when the site is already active
     */
    PluginService.activatePlugin = function(pluginUid, pluginSpec, site) {
        if (ACTIVE_PLUGINS[site] && ACTIVE_PLUGINS[site][pluginUid]) {
            return false;
        }
        if (!ACTIVE_PLUGINS[site]) {
            ACTIVE_PLUGINS[site] = {};
        }
        ACTIVE_PLUGINS[site][pluginUid] = pluginSpec;
        return true;
    };

    /**
     * Retrieves the main module prototype for the specified active plugin
     * @static
     * @method getActiveMainModule
     * @param {String} pluginUid
     * @param {string} site
     * @return {Function} The prototype that is the plugin's main module.
     */
    PluginService.getActiveMainModule = function(pluginUid, site) {
        if(!site) {
            site = GLOBAL_SITE;
        }
        return (ACTIVE_PLUGINS[site] && ACTIVE_PLUGINS[site][pluginUid]) ? ACTIVE_PLUGINS[site][pluginUid].main_module : null;
    };

    /**
     * Retrieves the names of the active plugins for this instance
     * @method getActivePluginNames
     * @return {array} An array that contain the names of the plugins that
     * initialized successfully within this instance.
     */
    PluginService.prototype.getActivePluginNames = function() {
        var globalPlugins = [];
        if(ACTIVE_PLUGINS[GLOBAL_SITE]) {
            globalPlugins = Object.keys(ACTIVE_PLUGINS[GLOBAL_SITE]);
        }
        var sitePlugins = [];
        if(ACTIVE_PLUGINS[this.site]) {
            sitePlugins = Object.keys(ACTIVE_PLUGINS[this.site]);
        }
        return util.dedupeArray(sitePlugins.concat(globalPlugins));
    };

    /**
     * Get a array of active plugin names with site name as a prefix: site_name_plugin_name
     * @method getAllActivePluginNames
     * @return {Array} array of active plugin names with site name prefix.
     */
    PluginService.prototype.getAllActivePluginNames = function() {
        var pluginNames = [];
        var siteNames = Object.keys(ACTIVE_PLUGINS);
        for( var i = 0; i < siteNames.length; i++ ) {
            var sitePluginNames = Object.keys(ACTIVE_PLUGINS[siteNames[i]]);
            for ( var j = 0; j < sitePluginNames.length; j++ ) {
                pluginNames.push(siteNames[i] + '_' + sitePluginNames[j]);
            }
        }
        return pluginNames;
    };

    /**
     * Retrieves a single setting for the specified plugin.
     * @method getSetting
     * @param {string} settingName The name of the setting to retrieve
     * @param {string} pluginName The name of the plugin who owns the setting
     * @param {function} cb A callback that provides two parameters: cb(error, settingValue).
     * Null is returned if the setting does not exist or the specified plugin is not
     * installed.
     */
    PluginService.prototype.getSetting = function(settingName, pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.getSetting(settingName, pluginName, cb);
    };

    /**
     * Retrieves all of the settings for the specfied plugin.
     * @method getSettings
     * @param pluginName The name of the plugin who's settings are being requested
     * @param cb A callback that provides two parameters: cb(error, settings).
     * Null is provided in the event that the plugin is not installed.
     */
    PluginService.prototype.getSettings = function(pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.getSettings(pluginName, cb);
    };

    /**
     * Retrieves the settings for a plugin as hash of key/value pairs.  This
     * differs from the getSettings function because the getSettings function
     * provides the settings in their raw form as an array of objects containing
     * multiple properties.  In most circumstances just the k/v pair is needed and
     * not any additional information about the property.  The function takes the
     * raw settings array and transforms it into an object where the setting name
     * is the property and the setting value is the value.
     * @method getSettingsKV
     * @param {String} pluginName The unique ID of the plugin who settings are to be retrieved
     * @param {Function} cb A callback that takes two parameters.  A error, if
     * exists, and a hash of of the plugin's settings' names/values.
     */
    PluginService.prototype.getSettingsKV = function(pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.getSettingsKV(pluginName, cb);
    };

    /**
     * Replaces a single setting for the specified plugin
     * @method setSetting
     * @param name The name of the setting to change
     * @param value The new value for the setting
     * @param pluginName The plugin who's setting is being changed.
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the setting was persisted successfully, FALSE if not.
     */
    PluginService.prototype.setSetting = function(name, value, pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.setSetting(name, value, pluginName, cb);
    };

    /**
     * Replaces the settings for the specified plugin.
     * @method setSettings
     * @param settings The settings object to be validated and persisted
     * @param pluginName The name of the plugin who's settings are being represented
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the settings were persisted successfully, FALSE if not.
     */
    PluginService.prototype.setSettings = function(settings, pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.setSettings(settings, pluginName, cb);
    };

    /**
     * Replaces a single theme setting for the specified plugin
     * @method setThemeSetting
     * @param name The name of the setting to change
     * @param value The new value for the setting
     * @param pluginName The plugin who's setting is being changed.
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the setting was persisted successfully, FALSE if not.
     */
    PluginService.prototype.setThemeSetting = function(name, value, pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.setThemeSetting(name, value, pluginName, cb);
    };

    /**
     * Replaces the theme settings for the specified plugin.
     * @method setThemeSettings
     * @param settings The settings object to be validated and persisted
     * @param pluginName The uid of the plugin who's settings are being represented
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the settings were persisted successfully, FALSE if not.
     */
    PluginService.prototype.setThemeSettings = function(settings, pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.setThemeSettings(settings, pluginName, cb);
    };

    /**
     * Retrieves a single theme setting value.
     * @method getThemeSetting
     * @param settingName The uid of the setting
     * @param pluginName The plugin to retrieve the setting from
     * @param cb A callback that provides two parameters: cb(error, settingValue)
     */
    PluginService.prototype.getThemeSetting = function(settingName, pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.getThemeSetting(settingName, pluginName, cb);
    };

    /**
     * Retrieves the theme settings for the specified plugin
     * @method getThemeSettings
     * @param pluginName The uid of the plugin
     * @param cb A callback that provides two parameters: cb(err, settingsObject)
     */
    PluginService.prototype.getThemeSettings = function(pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.getThemeSettings(pluginName, cb);
    };

    /**
     * Retrieves the theme settings for the specified plugin only for the site set in the current plugin service
     * @method getThemeSettingsBySite
     * @param pluginName
     * @param cb
     */
    PluginService.prototype.getThemeSettingsBySite = function (pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.getThemeSettingsBySite(pluginName, cb);
    };

    /**
     * Retrieves the theme settings for a plugin as hash of key/value pairs.  This
     * differs from the getThemeSettings function because the getThemeSettings function
     * provides the settings in their raw form as an array of objects containing
     * multiple properties.  In most circumstances just the k/v pair is needed and
     * not any additional information about the property.  The function takes the
     * raw settings array and transforms it into an object where the setting name
     * is the property and the setting value is the value.
     * @method getThemeSettingsKV
     * @param {String} pluginName The unique ID of the plugin who settings are to be retrieved
     * @param {Function} cb A callback that takes two parameters.  A error, if
     * exists, and a hash of of the plugin's settings' names/values.
     */
    PluginService.prototype.getThemeSettingsKV = function(pluginName, cb) {
        var settingService = getPluginSettingService(this);
        settingService.getThemeSettingsKV(pluginName, cb);
    };


    /**
     * Loads the settings from a details object and persists them in the DB.  Any
     * existing settings for the plugin are deleted before the new settings are
     * persisted.
     *
     * @method resetSettings
     * @param details The details object to extract the settings from
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the settings were successfully cleared and reloaded. FALSE if not.
     */
    PluginService.prototype.resetSettings = function(details, cb) {
        var settingService = getPluginSettingService(this);
        settingService.resetSettings(details, cb);
    };

    /**
     * Loads the Theme settings from a details object and persists them in the DB.  Any
     * existing theme settings for the plugin are deleted before the new settings
     * are persisted. If the plugin does not have a theme then false is provided in
     * the callback.
     *
     * @method resetThemeSettings
     * @param {Object} details The details object to extract the settings from
     * @param {Function} cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the settings were successfully cleared and reloaded. FALSE if not.
     */
    PluginService.prototype.resetThemeSettings = function(details, cb) {
        var settingService = getPluginSettingService(this);
        settingService.resetThemeSettings(details, cb);
    };

    /**
     * Deletes the plugin settings for when plugin uninstalls.
     * @method purgePluginSettings
     * @param {String} pluginUid - the plugin unique id
     * @param {Function} cb - callback function
     */
    PluginService.prototype.purgePluginSettings = function(pluginUid, cb) {
        var settingService = getPluginSettingService(this);
        settingService.purgePluginSettings(pluginUid, cb);
    };

    /**
     * Deletes the theme settings for when plugin uninstalls.
     * @method purgeThemeSettings
     * @param {String} pluginUid - the plugin unique id
     * @param {Function} cb - callback function
     */
    PluginService.prototype.purgeThemeSettings = function(pluginUid, cb) {
        var settingService = getPluginSettingService(this);
        settingService.purgeThemeSettings(pluginUid, cb);
    };

    /**
     * Indicates if a plugin by the specified identifier is installed.
     *
     * @method isInstalled
     * @param {string} pluginIdentifier The identifier can either be an ObjectID or the
     * plugin name
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the plugin is installed, FALSE if not.
     */
    PluginService.prototype.isInstalled = function(pluginIdentifier, cb) {
        this.getPluginBySite(pluginIdentifier, function(err, plugin) {
            cb(err, plugin ? true : false);
        });
    };

    /**
     * Retrieves a plugin descriptor (plugin document)
     *
     * @method getPlugin
     * @param {string} pluginIdentifier The identifier can either be an ObjectID or the
     * plugin name
     * @param {Function} cb A callback that provides two parameters: cb(error, plugin).  If the
     * plugin does exist null is provided.
     */
    PluginService.prototype.getPlugin = function(pluginIdentifier, cb) {
        this._pluginRepository.loadPluginAvailableToThisSite(pluginIdentifier, this.site, cb);
    };

    /**
     * @method getPluginBySite
     * @param {string} pluginIdentifier
     * @param {Function} cb
     */
    PluginService.prototype.getPluginBySite = function(pluginIdentifier, cb) {
        this._pluginRepository.loadPluginOwnedByThisSite(pluginIdentifier, this.site, cb);
    };

    /**
     * Retrieves the plugins that have themes associated with them
     * @method getPluginsWithThemes
     * @param {Function} cb Provides two parameters: Error, Array
     */
    PluginService.prototype.getPluginsWithThemes = function(cb) {
        this._pluginRepository.loadPluginsWithThemesAvailableToThisSite(this.site, cb);
    };

    /**
     * Get plugins that contain a theme on a site level.
     * @method getPluginsWithThemesBySite
     * @param {Function} cb - callback function
     */
    PluginService.prototype.getPluginsWithThemesBySite = function(cb) {
        this._pluginRepository.loadPluginsWithThemesOwnedByThisSite(this.site, cb);
    };

    /**
     * Convenience function to generate a service to handle settings for a plugin.
     *
     * @static
     * @method genSettingsService
     * @param objType The type of object that will be dealt with.  (plugin_settings,
     * theme_settings)
     * @param useMemory {Boolean} Indicates if the generated layered service should
     * use an in memory service.
     * @param useCache {Boolean} Indicates if the generated layered service should
     * use a cache service.
     * @param serviceName The name of the service
     * @param {string} site
     * @return {SimpleLayeredService}
     */
    PluginService.genSettingsService = function(objType, useMemory, useCache, serviceName, site) {

        //add in-memory service
        var services = [];

        var options = {
            objType: objType,
            site: site,
            onlyThisSite: false,
            timeout: pb.config.plugins.caching.memory_timeout
        };

        if (useMemory){
            services.push(new pb.MemoryEntityService(options));
        }

        //add cache service
        if (useCache) {
            options.timeout = 3600;
            services.push(new pb.CacheEntityService(options));
        }

        //always add DB
        options.keyField = 'plugin_uid';
        options.valueField = 'settings';
        services.push(new pb.DBEntityService(options));
        return new pb.SimpleLayeredService(services, serviceName);
    };

    /**
     * Compares the details loaded from the plugin config (details.json)
     * with the database values pulled from getSettingsKV(). If a discrepancy
     * is detected, the plugin settings are updated in the database so future
     * calls to getSettingsKV() will have the latest values.
     *
     * @method syncSettings
     * @param plugin {Object} The plugin object
     * @param details {Object} The details object to extract the settings from
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE/FALSE if an error occured.
     */
    PluginService.prototype.syncSettings = function(plugin, details, cb) {
        var self = this;
        this.getSettingsKV(plugin.uid, function(err, settings) {
            var isError = util.isError(err);
            if (isError || !settings) {
                if (isError) {
                    pb.log.error("PluginService: Failed to load settings from plugin [%s]", plugin.uid);
                }
                return cb(err, !isError);
            }

            var discrepancy = false;
            var formattedSettings = [];

            // Detect new settings
            details.settings.forEach(function(setting) {
                var settingName = setting.name;
                var val = settings[settingName];
                if (typeof val === 'undefined') {
                    discrepancy = true;
                    val = setting.value;
                    formattedSettings.push({name: settingName, value: val});
                }
                else {
                    formattedSettings.push({name: settingName, value: val});
                }
            });

            // If there's a size difference, there's a discrepancy
            discrepancy = discrepancy || (details.settings.length !== Object.keys(settings).length);

            // Return if no discrepancy was found
            if (!discrepancy) {
                return cb(null, true);
            }

            self.resetSettings({uid: plugin.uid, settings: formattedSettings}, function(err/*, result*/) {
                if (util.isError(err)) {
                    pb.log.error("PluginService: Failed to save off updated settings for plugin [%s]", plugin.uid);
                }
                cb(err, !util.isError(err));
            });
        });
    };

    /**
     * Loads the settings from a details object and persists them in the DB.  Any
     * existing settings for the plugin are deleted before the new settings are
     * persisted.
     *
     * @method resetSettings
     * @param details The details object to extract the settings from
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the settings were successfully cleared and reloaded. FALSE if not.
     */
    PluginService.prototype.resetSettings = function(details, cb) {
        var self = this;

        //retrieve plugin to prove it exists (plus we need the id)
        var pluginName = details.uid;
        this.getPlugin(pluginName, function(err, plugin) {
            if (util.isError(err) || !plugin) {
                return cb(err ? err : new Error("The plugin "+pluginName+" is not installed"), false);
            }

            //remove any existing settings
            self.pluginSettingsService.purge(pluginName, function (err, result) {
                if (util.isError(err) || !result) {
                    return cb(err, false);
                }

                //build the object to persist
                var baseDoc  = {
                    plugin_name: plugin.name,
                    plugin_uid: plugin.uid,
                    plugin_id: plugin[pb.DAO.getIdField()].toString(),
                    settings: details.settings
                };
                var settings = pb.DocumentCreator.create('plugin_settings', baseDoc);

                //save it
                var dao      = new pb.SiteQueryService({site: self.site});
                dao.save(settings, function(err/*, result*/) {
                    cb(err, !util.isError(err));
                });
            });
        });
    };

    /**
     * Loads the Theme settings from a details object and persists them in the DB.  Any
     * existing theme settings for the plugin are deleted before the new settings
     * are persisted. If the plugin does not have a theme then false is provided in
     * the callback.
     *
     * @method resetThemeSettings
     * @param details The details object to extract the settings from
     * @param cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the settings were successfully cleared and reloaded. FALSE if not.
     */
    PluginService.prototype.resetThemeSettings = function(details, cb) {
        var self = this;

        //error checking
        var pluginName = details.uid;
        if (!details.theme || !details.theme.settings) {
            cb(new Error("PluginService: Settings are required when attempting to reset a plugin's theme settings"), false);
            return;
        }

        //retrieve plugin to prove it exists (plus we need the id)
        this.getPlugin(pluginName, function(err, plugin) {
            if (util.isError(err) || !plugin) {
                cb(err, false);
                return;
            }

            //remove any existing settings
            self.themeSettingsService.purge(pluginName, function (err, result) {
                if (util.isError(err) || !result) {
                    cb(err, false);
                    return;
                }

                //build the object to persist
                var baseDoc  = {
                    plugin_name: plugin.name,
                    plugin_uid: plugin.uid,
                    plugin_id: plugin[pb.DAO.getIdField()].toString(),
                    settings: details.theme.settings
                };
                var settings = pb.DocumentCreator.create('theme_settings', baseDoc);

                //save it
                var dao      = new pb.SiteQueryService({site: self.site});
                dao.save(settings, function(err/*, result*/) {
                    cb(err, !util.isError(err));
                });
            });
        });
    };

    /**
     * Retrieves the permission set for a given role.  All active plugins are
     * inspected.
     * @static
     * @method getPermissionsForRole
     * @param {Integer|String} role The role to get permissions for
     * @return {Object} A hash of the permissions
     */
    PluginService.getPermissionsForRole = function(role) {
        if (!isNaN(role)) {
            role = pb.security.getRoleName(role);
        }

        var perms = {};
        Object.keys(ACTIVE_PLUGINS).forEach(function(site) {
            Object.keys(ACTIVE_PLUGINS[site]).forEach(function(pluginUid) {
                var permissions = ACTIVE_PLUGINS[site][pluginUid].permissions;
                if (permissions) {

                    var permsAtLevel = permissions[role];
                    if (permsAtLevel) {
                        util.merge(permsAtLevel, perms);
                    }
                }
            });
        });

        return perms;
    };

    /**
     * Retrieves the file path to the public directory for the specified plugin.
     * @static
     * @method getActivePluginDir
     * @param {String} pluginUid A plugin's UID value
     * @return {String} File path to the plugin's public directory
     */
    PluginService.getActivePluginPublicDir = function(pluginUid) {
        var publicPath = null;
        var keys = Object.keys(ACTIVE_PLUGINS);
        for (var i = 0; i < keys.length; i++) {
            if (ACTIVE_PLUGINS[keys[i]][pluginUid]) {
                publicPath = ACTIVE_PLUGINS[keys[i]][pluginUid].public_dir;
                break;
            }
        }
        return publicPath;
    };

    /**
     * Indicates if the specified plugin is active in this instance of PB.
     * @static
     * @method isActivePlugin
     * @param {String} uid The unique identifier for a plugin
     * @param {string} site
     * @return {Boolean} TRUE if the plugin is active, FALSE if not
     */
    PluginService.isActivePlugin = function(uid, site) {
        if(!site) {
            site = GLOBAL_SITE;
        }
        return !!PluginService.getPluginForSite(uid, site);
    };

    /**
     *
     * @param theme
     * @param site
     * @returns {*}
     */
    PluginService.getPluginForSite = function(theme, site) {
        if (ACTIVE_PLUGINS[site] && ACTIVE_PLUGINS[site][theme]) {
            return ACTIVE_PLUGINS[site][theme];
        } else if (ACTIVE_PLUGINS[GLOBAL_SITE] && ACTIVE_PLUGINS[GLOBAL_SITE][theme]) {
            return ACTIVE_PLUGINS[GLOBAL_SITE][theme];
        }
        return null;
    };

    /**
     * Indicates if the specified plugin is active for a given site in this instance of PB.
     * @static
     * @method isActivePlugin
     * @param {String} uid The unique identifier for a plugin
     * @param {string} site
     * @return {Boolean} TRUE if the plugin is active, FALSE if not
     */
    PluginService.isPluginActiveBySite = function(uid, site) {
        if(!site) {
            site = GLOBAL_SITE;
        }
        return ACTIVE_PLUGINS[site] && ACTIVE_PLUGINS[site][uid];
    };

    /**
     * Generates a URL path to a public resource for a plugin.
     * @static
     * @method genPublicPath
     * @param {String} plugin The UID of the plugin
     * @param {String} relativePathToMedia The relative path to the resource from
     * the plugin's public directory.
     * @return {String} URL path to the resource
     */
    PluginService.genPublicPath = function(plugin, relativePathToMedia) {
        if (!util.isString(plugin) || !util.isString(relativePathToMedia)) {
            return '';
        }
        return pb.UrlService.urlJoin('/public', plugin, relativePathToMedia);
    };

    /**
     * Retrieves the details for the active plugins.
     * @method getActivePlugins
     * @param {Function} cb A callback that provides two parameters: cb(Error, Array)
     */
    PluginService.prototype.getActivePlugins = function(cb) {
        this._pluginRepository.loadIncludedPluginsOwnedByThisSite(this.getActivePluginNames(), this.site, cb);
    };

    /**
     * Retrieves the content templates for all of the active plugins
     * @static
     * @method getActiveContentTemplates
     * @param targetSite
     * @return {Array} An array of objects
     */
    PluginService.getActiveContentTemplates = function(targetSite) {

        var templates = [];
        Object.keys(ACTIVE_PLUGINS).forEach(function(site) {
            if (!pb.SiteService.isNotSetOrEqual(targetSite, site)) {
                return;
            }
            var pluginsForSite = ACTIVE_PLUGINS[site];
            Object.keys(pluginsForSite).forEach(function(uid) {
                var plugin = pluginsForSite[uid];
                if (plugin.templates) {
                    var clone = util.clone(plugin.templates);
                    for(var i = 0; i < clone.length; i++) {
                        clone[i].theme_uid = uid;
                        templates.push(clone[i]);
                    }
                }
            });
        });
        return templates;
    };

    /**
     * Retrieves the inactive plugins for this instance of PencilBlue.  An inactive
     * plugin is considered one who failed to install or one that failed to start
     * properly.
     * @method getInactivePlugins
     * @param {Function} cb A callback that provides two parameters: cb(Error, Array)
     */
    PluginService.prototype.getInactivePlugins = function(cb) {
        this._pluginRepository.loadPluginsNotIncludedOwnedByThisSite(this.getActivePluginNames(), this.site, cb);
    };

    /**
     * Retrieves the available plugins.  An available plugin is one who is
     * uninstalled but available to be installed.
     * @method getAvailablePlugins
     * @param {Array} active An array of plugin detail objects.  Each object is
     * required to have a uid property that is a string.
     * @param {Array} inactive An array of plugin details objects. Each object is
     * required to have a uid property that is a string.
     * @param {Function} cb A callback that provides two parameters: cb(Error, Array)
     */
    PluginService.prototype.getAvailablePlugins = function(active, inactive, cb) {
        if (util.isArray(active)) {
            active = util.arrayToHash(active, function(active, i) {
                return active[i] ? active[i].uid : '';
            });
        }
        if (util.isArray(inactive)) {
            inactive = util.arrayToHash(inactive, function(inactive, i) {
                return inactive[i] ? inactive[i].uid : '';
            });
        }

        util.getDirectories(PluginService.getPluginsDir(), function(err, directories) {
            if (util.isError(err)) {
                cb(err, null);
                return;
            }

            var plugins   = [];
            var tasks     = util.getTasks(directories, function(directories, i) {
                return function(callback) {

                    //skip pencilblue
                    var parts   = directories[i].split(path.sep);
                    var dirName = parts[parts.length - 1];
                    if (dirName === pb.config.plugins.default) {
                        callback(null, true);
                        return;
                    }

                    var detailsFilePath = path.join(directories[i], DETAILS_FILE_NAME);
                    PluginService.loadDetailsFile(detailsFilePath, function(err, details) {
                        if (util.isError(err)) {
                            plugins.push({
                                uid: dirName,
                                dirName: dirName,
                                description: "Failed to load & parse the details.json file.",
                                validationErrors: ['An invalid details file was provided for plugin. '+err.stack]
                            });
                            callback(null, false);
                            return;
                        }

                        PluginService.validateDetails(details, dirName, function(err/*, result*/) {
                            if (util.isError(err)) {
                                plugins.push({
                                    uid: dirName,
                                    dirName: dirName,
                                    version: details.version,
                                    description: "The plugin details file failed validation ",
                                    validationErrors: err.validationErrors
                                });
                                callback(null, false);
                                return;
                            }
                            else if ( (active && active[details.uid]) || (inactive && inactive[details.uid])) {
                                callback(null, true);
                                return;
                            }
                            details.dirName = dirName;
                            plugins.push(details);
                            callback(null, true);
                        });
                    });
                };
            });
            async.series(tasks, function(err/*, results*/) {
                cb(err, plugins);
            });
        });
    };

    /**
     * Retrieves a map of the system's plugin.  The map provides three properties:
     * active, inactive, available.
     * @method getPluginMap
     * @param {Function} cb A callback that provides two parameters: cb(Error, Object)
     */
    PluginService.prototype.getPluginMap = function(cb) {
        var self  = this;
        var tasks = {

            active: function(callback) {
                self.getActivePlugins(callback);
            },

            inactive: function(callback) {
                self.getInactivePlugins(callback);
            }
        };
        async.series(tasks, function(err, results) {
            if (util.isError(err)) {
                cb(err, results);
                return;
            }

            self.getAvailablePlugins(results.active, results.inactive, function(err, available) {
                results.available = available;
                cb(err, results);
            });
        });
    };

    /**
     * Uninstalls the plugin with the specified UID.
     * @method uninstallPlugin
     * @param {String} pluginUid The unique plugin identifier
     * @param {Object} [options]
     * @param {String} [options.jobId] Required when uninstalling from the executing
     * process instead of calling upon the cluster.
     * @param {Boolean} [options.forCluster=true] When true or not provided the function
     * instructs the cluster to uninstall the plugin.  When explicitly FALSE the
     * function installs the plugin from the executing process.
     * @param {Function} cb A callback that provides two parameters: cb(Error, Boolean)
     */
    PluginService.prototype.uninstallPlugin = function(pluginUid, options, cb) {
        if (util.isFunction(options)) {
            cb = options;
            options = {};
        }
        if (!util.isObject(options)) {
            options = {};
        }
        if (!util.isFunction(cb)) {
            cb = util.cb;
        }

        //log start of operation
        if (pb.log.isDebug()) {
            pb.log.debug("PluginService:[%s] Attempting uninstall with options: %s", pluginUid, util.inspect(options));
        }


        var name  = util.format('UNINSTALL_PLUGIN_%s', pluginUid);
        var jobId = options.jobId;
        var site = this.site;
        var job = new pb.PluginUninstallJob();
        job.init(name, jobId);
        job.setPluginUid(pluginUid);
        job.setSite(site);
        job.setRunAsInitiator(options.forCluster !== false);
        job.run(cb);
        return job.getId();
    };

    /**
     * Installs a plugin by stepping through a series of steps that must be
     * completed in order.  There is currently no fallback plan for a failed install.
     * In order for a plugin to be fully installed it must perform the following
     * actions without error:
     * <ol>
     * <li>Load and parse the plugin's details.json file</li>
     * <li>Pass validation</li>
     * <li>Must not already be installed</li>
     * <li>Successfully register itself with the system</li>
     * <li>Successfully load any plugin settings</li>
     * <li>Successfully load any theme settings</li>
     * <li>Successfully execute the plugin's onInstall function</li>
     * <li>Successfully initialize the plugin for runtime</li>
     * </ol>
     * @method installPlugin
     * @param {string} pluginDirName The name of the directory that contains the
     * plugin and its details.json file.
     * @param {function} [cb] A callback that provides two parameters: cb(err, TRUE/FALSE)
     * @return {String} The job ID
     */
    PluginService.prototype.installPlugin = function(pluginDirName, cb) {

        cb = cb || util.cb;
        var name = util.format('INSTALL_PLUGIN_%s', pluginDirName);
        var job  = new pb.PluginInstallJob();
        job.init(name);
        job.setRunAsInitiator(true);
        job.setSite(this.site);
        job.setPluginUid(pluginDirName);
        job.run(cb);
        return job.getId();
    };

    /**
     * Attempts to initialize all installed plugins.
     * @method initPlugins
     * @param {Function} cb A callback that provides two parameters: cb(Error, Boolean)
     */
    PluginService.prototype.initPlugins = function(cb) {
        pb.log.debug('PluginService: Beginning plugin initialization...');

        var self = this;
        async.waterfall([
            util.wrapTask(this._pluginRepository, this._pluginRepository.loadPluginsAcrossAllSites),
            function(plugins, callback) {
                if (plugins.length === 0) {
                    pb.log.debug('PluginService: No plugins are installed');
                    return callback(null, []);
                }
                var tasks  = util.getTasks(plugins, function(plugins, i) {
                    return function(callback) {

                        try {
                            self.initPlugin(plugins[i], function(err, didInitialize) {
                                callback(null, {plugin: plugins[i], error: err, initialized: didInitialize});
                            });
                        }
                        catch(err) {
                            callback(null, {plugin: plugins[i], error: err, initialized: false});
                        }
                    };
                });
                async.series(tasks, callback);
            }
        ], cb);
    };

    /**
     * Initializes a plugin during startup or just after a plugin has been installed.
     * @method initPlugin
     * @param {object} plugin The plugin details
     * @param {function} cb A callback that provides two parameters: cb(Error, Boolean)
     */
    PluginService.prototype.initPlugin = function(plugin, cb) {
        if (typeof plugin !== 'object') {
            return cb(new Error('PluginService:[INIT] The plugin object must be passed in order to initialize the plugin'), null);
        }

        var service = new pb.PluginInitializationService({
            pluginService: this,
            pluginCache: PLUGIN_INIT_CACHE
        });
        service.initialize(plugin, {}, function(err, result) {
            PLUGIN_INIT_CACHE = {};
            cb(err, result);
        });
    };

    /**
     * Verifies that a plugin has all of the required dependencies installed from NPM
     * @method hasDependencies
     * @param {Object} plugin
     * @param {Function} cb
     */
    PluginService.prototype.hasDependencies = function(plugin, cb) {
        var npmPluginDependencyService = new pb.NpmPluginDependencyService();
        npmPluginDependencyService.hasDependencies(plugin, {}, cb);
    };

    /**
     * Installs the dependencies for a plugin via NPM.
     * @method installPluginDependencies
     * @param {String} pluginDirName
     * @param {Object} dependencies
     * @param {Object} plugin
     * @param {Function} cb
     */
    PluginService.prototype.installPluginDependencies = function(pluginDirName, dependencies, plugin, cb) {

        //verify parameters
        if (!pb.ValidationService.isNonEmptyStr(pluginDirName, true) || !util.isObject(dependencies)) {
            return cb(new Error('The plugin directory name and the dependencies are required'));
        }

        var npmDependencyService = new pb.NpmPluginDependencyService();
        npmDependencyService.installAll(plugin, {}, cb);
    };

    /**
     * Loads a module dependencies for the specified plugin.
     * @deprecated
     * @static
     * @method require
     * @param {String} pluginDirName
     * @param {String} moduleName
     * @return {*} The entity returned by the "require" call.
     */
    PluginService.require = function(pluginDirName, moduleName) {
        pb.log.warn('PluginService: require is deprecated. Use NpmPluginDependencyService.require');
        return pb.NpmPluginDependencyService.require(pluginDirName, moduleName);
    };

    /**
     * Loads the localization files from the specified plugin directory and places
     * them into a hash where the key is the name of the localization file.
     * @deprecated since 0.6.0
     * @method getLocalizations
     * @param {String} pluginDirName The name of the plugin directory
     * @param {Function} cb A callback that provides two parameters: cb(Error, Object).
     * When the directory does not exist NULL is returned as the result parameter.
     */
    PluginService.prototype.getLocalizations = function(pluginDirName, cb) {
        var service = new pb.PluginLocalizationLoader({pluginUid: pluginDirName, site: this.site});
        service.getAll({}, cb);
    };

    /**
     * Retrieves a plugin service prototype.  It is expected to be a prototype but
     * it may also be an instance as along as that instance fulfills all
     * responsibilities of the service interface.  When the desired service does not
     * exist NULL is returned.
     * @deprecated
     * @method getService
     * @param {String} serviceName
     * @param {String} pluginUid The unique plugin identifier
     * @param {string} site
     * @return {Object} Service prototype
     */
    PluginService.prototype.getService = function(serviceName, pluginUid, site) {
        pb.log.warn('PluginService: Instance function getService is deprecated. Use pb.PluginService.getService instead: plugin:' + pluginUid + ' service:' + serviceName);
        try{
            return PluginService.getService(serviceName, pluginUid, site);
        }
        catch(e) {
            //for backward compatibility until the function is removed
            return null;
        }
    };

    /**
     * Retrieves a plugin service prototype.  It is expected to be a prototype but
     * it may also be an instance as along as that instance fulfills all
     * responsibilities of the service interface.  When the desired service does not
     * exist NULL is returned.
     * @static
     * @method getService
     * @param {String} serviceName
     * @param {String} pluginUid The unique plugin identifier
     * @param {string} [site=global] - The site UID
     * @return {Object} Service prototype
     */
    PluginService.getService = function(serviceName, pluginUid, site) {
        if(!site) {
            site = GLOBAL_SITE;
        }
        if (ACTIVE_PLUGINS[site] && ACTIVE_PLUGINS[site][pluginUid]) {
            if (ACTIVE_PLUGINS[site][pluginUid].services && ACTIVE_PLUGINS[site][pluginUid].services[serviceName]) {
                return ACTIVE_PLUGINS[site][pluginUid].services[serviceName];
            }
        } else if (ACTIVE_PLUGINS[GLOBAL_SITE] && ACTIVE_PLUGINS[GLOBAL_SITE][pluginUid]) {
            if (ACTIVE_PLUGINS[GLOBAL_SITE][pluginUid].services && ACTIVE_PLUGINS[GLOBAL_SITE][pluginUid].services[serviceName]) {
                return ACTIVE_PLUGINS[GLOBAL_SITE][pluginUid].services[serviceName];
            }
        }
        throw new Error('Either plugin ['+pluginUid+'] or the service ['+serviceName+'] does not exist for site ['+site+']');
    };

    /**
     * Attempts to require the main module file for a plugin.
     * @static
     * @method loadMainModule
     * @param {String} pluginDirName The name of the directory that the plugin is
     * contained within.
     * @param {String} pathToModule The name of the main module file.  It is also
     * to pass this parameter as the absolute file path to the module.  The
     * function first checks if the parameter is just the file name then checks to
     * see if it is an absolute path.
     * @return {Function} The main-module prototype
     */
    PluginService.loadMainModule = function(pluginDirName, pathToModule) {
        var pluginMM = path.join(PLUGINS_DIR, pluginDirName, pathToModule);
        var paths    = [pluginMM, pathToModule];

        var mainModule = null;
        for (var i = 0; i < paths.length; i++) {
            try {
                mainModule = require(paths[i])(pb);
                break;
            }
            catch(e) {
                if (pb.log.isDebug()) {
                    pb.log.warn('PluginService: Failed to load main module at %s: %s', paths[i], e.stack);
                }
            }
        }
        return mainModule;
    };

    /**
     * Retrieves the absolute file path to a plugin's public directory
     *
     * @static
     * @method getPublicPath
     * @param pluginDirName The name of the directory that contains the intended
     * plugin
     * @return {string} the absolute file path to a plugin's public directory
     */
    PluginService.getPublicPath = function(pluginDirName) {
        return path.join(PLUGINS_DIR, pluginDirName, PUBLIC_DIR_NAME);
    };

    /**
     * @static
     * @method getPluginsDir
     * @return {string} The absolute file path to the plugins directory
     */
    PluginService.getPluginsDir = function() {
        return PLUGINS_DIR;
    };

    /**
     * Constructs the path to a specific plugin's details.json file
     * @static
     * @method getDetailsPath
     * @param {String} pluginDirName The name of the directory that the plugin is
     * contained within.
     * @return {string} The absolute file path to the details.json file for a plugin
     */
    PluginService.getDetailsPath = function(pluginDirName) {
        return path.join(PLUGINS_DIR, pluginDirName, DETAILS_FILE_NAME);
    };

    /**
     * Attempts to load and parse the details.json file for a plugin.
     * @static
     * @method loadDetailsFile
     * @param {String} filePath The absolute path to the details.json file
     * @param {Function} cb A callback that provides two parameters: cb(error, detailsObject)
     */
    PluginService.loadDetailsFile = function(filePath, cb) {
        fs.readFile(filePath, function(err, data){
            if (util.isError(err)) {
                cb(err, null);
                return;
            }

            //attempt to parse the json
            try {
                cb(null, JSON.parse(data));
            }
            catch(e) {
                e.message = "Failed to parse json file ["+filePath+']. '+e.message;
                e.code    = 500;
                e.source  = e;
                cb(e, null);
            }
        });
    };

    /**
     * Validates a plugin's details.json file.
     * @deprecated
     * @static
     * @method validateDetails
     * @param {object} details The details object to validate
     * @param {object} [details.theme]
     * @param {object} details.permissions
     * @param {string} details.uid
     * @param {string} details.description
     * @param {string} details.version
     * @param {string} details.pb_version
     * @param {string} details.icon
     * @param {Array} [details.contributors]
     * @param {object} [details.author]
     * @param {Array} [details.settings]
     * @param {String} pluginDirName The name of the directory containing the original
     * details.json file that the details object was derived from.
     * @param {Function} cb A callback that provides two parameters: cb(error, TRUE/FALSE).
     * TRUE if the details object passes validation, FALSE if not.
     */
    PluginService.validateDetails = function(details, pluginDirName, cb) {
        if (!util.isObject(details)) {
            cb(new Error("Details cannot be null and must be an object"), false);
            return;
        }

        var validationService = new pb.PluginValidationService({});
        validationService.validate(details, {}, function(err, validationErrors) {
            if (util.isError(err)) {
                return cb(err);
            }

            //check for validation error
            validationErrors = validationErrors || [];
            if (validationErrors.length > 0) {
                err = new Error("Failed to validate plugin details");
                err.validationErrors = validationErrors;
            }
            cb(err, validationErrors.length === 0);
        });
    };

    /**
     * Validates the path to the plugin's icon file.  The path is considered valid
     * if the path to a valid file.  The path may be absolute or relative to the
     * plugin's public directory.
     * @deprecated
     * @static
     * @method validateIconPath
     * @param iconPath The path to the icon (image) file
     * @param pluginDirName The name of the directory housing the plugin
     * @return {Boolean} TRUE if the path is valid, FALSE if not
     */
    PluginService.validateIconPath = function(iconPath, pluginDirName) {
        return pb.PluginValidationService.validateIconPath(iconPath, pluginDirName);
    };

    /**
     * Validates the path of a main module file.  The path is considered valid if
     * the path points to JS file.  The path may be absolute or relative to the
     * specific plugin directory.
     * @deprecated
     * @static
     * @method validateMainModulePath
     * @param mmPath The relative or absolute path to the main module file
     * @param pluginDirName The name of the directory housing the plugin
     * @return {Boolean} TRUE if the path is valid, FALSE if not
     */
    PluginService.validateMainModulePath = function(mmPath, pluginDirName) {
        return pb.PluginValidationService.validateMainModulePath(mmPath, pluginDirName);
    };

    /**
     * Validates a setting from a details.json file.
     * @deprecated
     * @method validateSetting
     * @param setting The setting to validate
     * @param position The position in the settings array where the setting resides
     * as a 0 based index.
     * @return {Array} The array of errors that were generated.  If no errors were
     * produced an empty array is returned.
     */
    PluginService.validateSetting = function(setting, position) {

        //setup
        var errors = [];
        var v      = pb.validation;

        //validate object
        if (util.isObject(setting)) {

            //validate name
            if (!v.isNonEmptyStr(setting.name, true)) {
                errors.push(new Error("The setting name at position "+position+" must be provided"));
            }

            //validate value
            if (!pb.PluginDependencyService.validateSettingValue(setting.value)) {
                errors.push(new Error("The setting value at position "+position+" must be provided"));
            }
        }
        else {
            errors.push(new Error("The setting value at position "+position+" must be an object"));
        }

        return errors;
    };

    /**
     * Validates a details.json file's setting value.  The value is required to be a
     * string or a number.  Null, undefined, Arrays, Objects, and prototypes are NOT
     * allowed.
     * @deprecated
     * @static
     * @method validateSettingValue
     * @param {Boolean|Integer|Number|String} value The value to validate
     * @return {Boolean} TRUE if the value is valid, FALSE if not
     */
    PluginService.validateSettingValue = function(value) {
        return pb.PluginDependencyService.validateSettingValue(value);
    };

    /**
     * Retrieves all services (initialized).  The services are provided in the
     * callback.
     * @deprecated
     * @static
     * @method getServices
     * @param {String} pathToPlugin The absolute file path to the specific plugin directory.
     * @param {Function} cb A callback that provides two parameters: cb(error, servicesHash);
     */
    PluginService.getServices = function(pathToPlugin, cb) {
        pb.log.warn('PluginService: getServices is deprecated. Use PluginServiceLoader.getAll');

        var pathParts = pathToPlugin.split(path.sep);

        var service = new pb.PluginServiceLoader({pluginUid: pathParts[pathParts.length - 1]});
        service.getAll({}, cb);
    };

    /**
     * Loads a plugin service and initializes it.  The service is required to
     * implement an "init" function. The service is then provided as a parameter in
     * the callback.
     * @deprecated
     * @static
     * @method loadService
     * @param {String} pathToService The absolute file path to the service javascript file.
     * @param {Function} cb A callback that provides two parameters: cb(error, initializedService)
     */
    PluginService.loadService = function(pathToService, cb) {
        pb.log.warn('PluginService: loadService is deprecated. Use PluginServiceLoader.get');

        var service = new pb.PluginServiceLoader({});
        service.get(pathToService, function(err, serviceWrapper) {
            if (serviceWrapper) {
                serviceWrapper = serviceWrapper.service;
            }
            cb(err, serviceWrapper);
        });
    };

    /**
     * Loads the controllers for a plugin by iterating through the files in the
     * plugin's controllers directory.
     * @deprecated
     * @static
     * @method loadControllers
     * @param {String} pathToPlugin The absolute file path to the plugin =
     * @param {String} pluginUid The unique identifier for the plugin
     * @param {string} site
     * @param {Function} cb A callback that provides two parameters: cb(Error, Array)
     */
    PluginService.loadControllers = function(pathToPlugin, pluginUid, site, cb) {
        var knownControllerPaths = PLUGIN_INIT_CACHE[pluginUid].controllerPaths;
        if(knownControllerPaths && knownControllerPaths.length > 0) {
            var tasks =  util.getTasks(knownControllerPaths, function(knownControllerPaths, index) {
                return function(callback) {
                    PluginService.loadController(knownControllerPaths[index], pluginUid, site, callback);
                };
            });

            return async.parallel(tasks, cb);
        }
        var controllersDir = path.join(pathToPlugin, 'controllers');

        var options = {
            recursive: true,
            filter: function(fullPath/*, stat*/) {
                return fullPath.lastIndexOf('.js') === (fullPath.length - '.js'.length);
            }
        };
        util.getFiles(controllersDir, options, function(err, files) {
            if (util.isError(err)) {
                pb.log.debug('PluginService[INIT]: The controllers directory [%s] does not exist or could not be read.', controllersDir);
                pb.log.silly('PluginService[INIT]: %s', err.stack);
                cb(null, []);
                return;
            }

            PLUGIN_INIT_CACHE[pluginUid].controllerPaths = PLUGIN_INIT_CACHE[pluginUid].controllerPaths || [];
            var tasks = util.getTasks(files, function(files, index) {
                return function(callback) {

                    var pathToController = files[index];
                    PLUGIN_INIT_CACHE[pluginUid].controllerPaths.push(pathToController);
                    PluginService.loadController(pathToController, pluginUid, site, function(err/*, service*/) {
                        if (util.isError(err)) {
                            pb.log.warn('PluginService: Failed to load controller at [%s]: %s', pathToController, err.stack);
                        }
                        callback(null, util.isError(err));
                    });
                };
            });
            async.parallel(tasks, cb);
        });
    };

    /**
     * Loads a controller for a plugin and attempts to register the route with the
     * RequestHandler.
     * @static
     * @method loadController
     * @param {String} pathToController The absolute file path to the controller
     * @param {String} pluginUid The unique identifier for the plugin
     * @param {string} site
     * @param {Function} cb A callback that provides two parameters: cb(Error, Boolean)
     */
    PluginService.loadController = function(pathToController, pluginUid, site, cb) {
        try {

            //load the controller type
            var ControllerPrototype = require(pathToController)(pb);

            //ensure we can get the routes
            if (!util.isFunction(ControllerPrototype.getRoutes)){
                return cb(new Error('Controller at ['+pathToController+'] does not implement function "getRoutes"'));
            }

            //get the routes
            ControllerPrototype.getRoutes(function(err, routes) {
                if (util.isError(err)) {
                    return cb(err, null);
                }
                else if (!util.isArray(routes)) {
                    return cb(new Error('Controller at ['+pathToController+'] did not return an array of routes'), null);
                }

                //attempt to register route
                for(var i = 0; i < routes.length; i++) {
                    var route        = routes[i];
                    route.controller = pathToController;
                    var result       = pb.RequestHandler.registerRoute(route, pluginUid, site);

                    //verify registration
                    if (!result) {
                        pb.log.warn('PluginService: Failed to register route [%s] for controller at [%s]', util.inspect(route), pathToController);
                    }
                }

                //do callback
                cb(null, true);
            });
        }
        catch(err) {
            cb(err, null);
        }
    };

    /**
     * Derives the name of a plugin service instance.  The function attempts to get
     * the name of the service by looking to see if the service has implemented the
     * getName function.  If it has not then the service name is set to be the file
     * name minus any extension.
     * @deprecated since 0.6.0
     * @static
     * @method getServiceName
     * @param pathToService The file path to the service
     * @param service The service prototype
     * @return {String} The derived service name
     */
    PluginService.getServiceName = function(pathToService, service) {
        pb.log.warn('PluginService: getServiceName is deprecated. Use PluginServiceLoader.getServiceName');
        pb.PluginServiceLoader.getServiceName(pathToService, service);
    };

    /**
     * <b>NOTE: DO NOT CALL THIS DIRECTLY</b><br/>
     * The function is called when a command is recevied to uninstall a plugin.
     * The function builds out the appropriate options then calls the
     * uninstallPlugin function.  The result is then sent back to the calling
     * process via the CommandService.
     * @static
     * @method onUninstallPluginCommandReceived
     * @param {Object} command
     * @param {String} command.jobId The ID of the in-progress job that this
     * process is intended to join.
     */
    PluginService.onUninstallPluginCommandReceived = function(command) {
        if (!util.isObject(command)) {
            pb.log.error('PluginService: an invalid uninstall plugin command object was passed. %s', util.inspect(command));
            return;
        }

        var options = {
            forCluster: false,
            jobId: command.jobId
        };

        var pluginService = new PluginService({site: command.site});
        pluginService.uninstallPlugin(command.pluginUid, options, function(err, result) {

            var response = {
                error: err ? err.stack : undefined,
                result: result
            };
            pb.CommandService.getInstance().sendInResponseTo(command, response);
        });
    };

    /**
     * <b>NOTE: DO NOT CALL THIS DIRECTLY</b><br/>
     * The function is called when a command is recevied to validate that a plugin is available to this process for install.
     * The function builds out the appropriate options then calls the
     * uninstallPlugin function.  The result is then sent back to the calling
     * process via the CommandService.
     * @static
     * @method onIsPluginAvailableCommandReceived
     * @param {Object} command
     * @param {String} command.jobId The ID of the in-progress job that this
     * process is intended to join.
     */
    PluginService.onIsPluginAvailableCommandReceived = function(command) {
        if (!util.isObject(command)) {
            pb.log.error('PluginService: an invalid is_plugin_available command object was passed. %s', util.inspect(command));
            return;
        }

        var name = util.format("IS_AVAILABLE_%s", command.pluginUid);
        var job  = new pb.PluginAvailableJob();
        job.setRunAsInitiator(false)
            .init(name, command.jobId)
            .setPluginUid(command.pluginUid)
            .run(function(err, result) {

                var response = {
                    error: err ? err.stack : undefined,
                    result: result ? true : false
                };
                pb.CommandService.getInstance().sendInResponseTo(command, response);
            });
    };

    /**
     * <b>NOTE: DO NOT CALL THIS DIRECTLY</b><br/>
     * The function is called when a command is recevied to install plugin
     * dependencies.  The result is then sent back to the calling process via the
     * CommandService.
     * @static
     * @method onIsPluginAvailableCommandReceived
     * @param {Object} command
     * @param {String} command.jobId The ID of the in-progress job that this
     * process is intended to join.
     */
    PluginService.onInstallPluginDependenciesCommandReceived = function(command) {
        if (!util.isObject(command)) {
            pb.log.error('PluginService: an invalid install_plugin_dependencies command object was passed. %s', util.inspect(command));
            return;
        }

        var name = util.format("INSTALL_DEPENDENCIES_%s", command.pluginUid);
        var job  = new pb.PluginDependenciesJob();
        job.setRunAsInitiator(false)
            .init(name, command.jobId)
            .setPluginUid(command.pluginUid)
            .run(function(err, result) {

                var response = {
                    error: err ? err.stack : undefined,
                    result: result ? true : false
                };
                pb.CommandService.getInstance().sendInResponseTo(command, response);
            });
    };

    /**
     * <b>NOTE: DO NOT CALL THIS DIRECTLY</b><br/>
     * The function is called when a command is recevied to initialize a plugin.
     * The result is then sent back to the calling process via the
     * CommandService.
     * @static
     * @method onIsPluginAvailableCommandReceived
     * @param {Object} command
     * @param {String} command.jobId The ID of the in-progress job that this
     * process is intended to join.
     */
    PluginService.onInitializePluginCommandReceived = function(command) {
        if (!util.isObject(command)) {
            pb.log.error('PluginService: an invalid initialize_plugin command object was passed. %s', util.inspect(command));
            return;
        }

        var name = util.format("INITIALIZE_PLUGIN_%s", command.pluginUid);
        var job  = new pb.PluginInitializeJob();
        job.setRunAsInitiator(false)
            .init(name, command.jobId)
            .setPluginUid(command.pluginUid)
            .setSite(command.site)
            .run(function(err, result) {

                var response = {
                    error: err ? err.stack : undefined,
                    result: result ? true : false
                };
                pb.CommandService.getInstance().sendInResponseTo(command, response);
            });
    };

    /**
     *
     * @static
     * @method init
     */
    PluginService.init = function() {

        //register for commands
        var commandService = pb.CommandService.getInstance();
        commandService.registerForType(pb.PluginUninstallJob.UNINSTALL_PLUGIN_COMMAND, PluginService.onUninstallPluginCommandReceived);
        commandService.registerForType('is_plugin_available', PluginService.onIsPluginAvailableCommandReceived);
        commandService.registerForType('install_plugin_dependencies', PluginService.onInstallPluginDependenciesCommandReceived);
        commandService.registerForType('initialize_plugin', PluginService.onInitializePluginCommandReceived);
    };

    function getPluginSettingService(self) {
        if(!self.pluginSettingService) {
            self.pluginSettingService = new pb.PluginSettingService(self.site);
        }
        return self.pluginSettingService;
    }

    //exports
    return PluginService;
};