OpenVeo Core server

API Docs for: 7.0.0
Show:

File: app/server/loaders/pluginLoader.js

'use strict';

/**
 * @module core-loaders
 */

/**
 * Provides functions to load openveo plugins.
 *
 * @class pluginLoader
 * @static
 */

var fs = require('fs');
var path = require('path');
var async = require('async');
var openVeoApi = require('@openveo/api');
var migrationLoader = process.require('app/server/loaders/migrationLoader.js');
var storage = process.require('app/server/storage.js');
var ResourceFilter = openVeoApi.storages.ResourceFilter;

/**
 * Filters the list of plugins paths in case the same plugin appears
 * multiple time at different paths. Filters to keep only the most
 * top level one.
 *
 * @example
 *     var pluginsPaths = [
 *       '/openveo/node_modules/@openveo/plugin1',
 *       '/openveo/node_modules/@openveo/plugin2/node_modules/@openveo/plugin1'
 *     ];
 *     console.log(filterPluginsPaths(pluginsPaths));
 *     // [ '/openveo/node_modules/@openveo/plugin1' ]
 *
 * @method filterPluginsPaths
 * @private
 * @static
 * @param {Array} pluginsPaths The list of plugins paths to analyze
 * @return {Array} The filtered list of plugins paths
 */
function filterPluginsPaths(pluginsPaths) {
  var filteredPaths = [];

  // Got at least one path
  if (pluginsPaths.length) {
    var analyzedPaths = {};

    pluginsPaths.forEach(function(pluginPath) {

      // Extract plugin name
      // e.g "/openveo/node_modules/@openveo/plugin1"
      // becomes "plugin1"
      var pluginName = path.basename(pluginPath);

      // Plugin already analyzed
      // Replace it if the path length is shorter
      if (analyzedPaths[pluginName]) {
        if (analyzedPaths[pluginName].length > pluginPath.length)
          analyzedPaths[pluginName] = pluginPath;
      } else {

        // Plugin name not analyzed yet
        // Add it to the list of analyzed one
        analyzedPaths[pluginName] = pluginPath;

      }

    });

    for (var i in analyzedPaths)
      filteredPaths.push(analyzedPaths[i]);

  }

  return filteredPaths;
}

/**
 * Recursively and asynchronously analyze the given directory to get
 * npm plugins.
 *
 * There are two kinds of plugins : plugins maintained by the core team (under @openveo scope)
 * and contributers' plugins which must be prefixed by **openveo-**.
 *
 * @example
 *     getPluginPaths('/openveo', function(error, pluginsPaths){
 *       console.log(pluginsPaths);
 *       // [
 *       //   '/openveo/node_modules/@openveo/plugin',
 *       //   '/openveo/node_modules/openveo-contrib-plugin'
 *       // ]
 *     };
 *
 * @method getPluginPaths
 * @static
 * @param {String} startingPath Root path of an NPM module from where looking for plugins
 * @param {Function} callback A callback with two arguments :
 *    - **Error** An Error object or null
 *    - **Array** The list of plugins paths
 * @throws {TypeError} An error if starting path is not a valid string
 */
module.exports.getPluginPaths = function(startingPath, callback) {
  var self = this;
  var pluginsPaths = [];
  var asyncActions = [];
  startingPath = path.join(startingPath, 'node_modules');

  // Read node_modules directory
  fs.readdir(startingPath, function(error, resources) {
    if (error && error.code === 'ENOENT') {

      // Directory does not exist
      // No plugin for this path
      return callback(null, pluginsPaths);

    } else if (error)
      return callback(error);

    var getPluginPath = function(potentialPluginPath) {
      return function(callback) {
        fs.stat(potentialPluginPath, function(statError, stats) {
          if (statError)
            return callback(statError);

          if (stats.isDirectory()) {

            // Found a plugin path
            pluginsPaths.push(potentialPluginPath);

            self.getPluginPaths(potentialPluginPath, function(pathsError, subPluginsPaths) {
              if (pathsError)
                return callback(pathsError);

              // Found sub plugin paths
              pluginsPaths = pluginsPaths.concat(subPluginsPaths);

              // Done with this resource
              callback();
            });

          } else {

            // This is not a plugin path
            // Done with this resource
            callback();

          }
        });
      };
    };

    var getOfficialPluginsPaths = function(openveoDirPath) {
      return function(callback) {
        fs.stat(openveoDirPath, function(statError, stats) {
          if (statError)
            return callback(statError);

          if (stats.isDirectory()) {

            // Read directory
            fs.readdir(openveoDirPath, function(error, potentialPluginsResources) {
              var asyncActions = [];

              potentialPluginsResources.forEach(function(potentialPluginResource) {
                asyncActions.push(getPluginPath(path.join(openveoDirPath, potentialPluginResource)));
              });

              async.parallel(asyncActions, function(error, results) {
                callback(error);
              });
            });
          } else {

            // This is not a plugin path
            // Done with this resource
            callback();

          }
        });
      };
    };

    // Iterate through the list of resources in the directory
    // to find all resources starting by "openveo-" and if a resource
    // "@openveo" is present
    resources.forEach(function(resource) {
      var resourcePath = path.join(startingPath, resource);
      if (/^openveo-/.test(resource))
        asyncActions.push(getPluginPath(resourcePath));
      else if (resource === '@openveo')
        asyncActions.push(getOfficialPluginsPaths(resourcePath));
    });

    async.parallel(asyncActions, function(error, results) {
      if (error)
        callback(error);

      else
        callback(null, pluginsPaths);
    });
  });

};

/**
 * Recursively and asynchronously load all offical and contributed OpenVeo plugins under the given path.
 *
 * If the same plugin (same name) is encountered several times, the top level one
 * will be kept.
 *
 * @example
 *     var pluginLoader = process.require('app/server/loaders/pluginLoader.js');
 *
 *     // Load all potential openveo plugins from directory /home/openveo/openveo
 *     pluginLoader.loadPlugins('/home/openveo/openveo', function(error, plugins){
 *       console.log(plugins);
 *     };
 *
 * @method loadPlugins
 * @static
 * @async
 * @param {String} startingPath Root path of an NPM module from where looking for plugins
 * @param {Function} callback A callback with two arguments :
 *    - **Error** An Error object or null
 *    - **Array** A list of Plugin objects
 * @throws {TypeError} An error if starting path is not a valid string
 */
module.exports.loadPlugins = function(startingPath, callback) {
  var self = this;

  // Get the list of potential plugins absolute paths
  this.getPluginPaths(startingPath, function(error, pluginsPaths) {

    // An error occurred while scaning the directory looking for
    // openveo plugins
    if (error) {
      callback(error);
      return;
    }

    // Filter duplicate plugins to keep only the top level ones
    pluginsPaths = filterPluginsPaths(pluginsPaths);
    var plugins = [];
    var asyncActions = [];

    pluginsPaths.forEach(function(pluginPath) {
      asyncActions.push(function(callback) {

        // Load the plugin
        self.loadPlugin(pluginPath, function(loadError, loadedPlugin) {

          // An error occurred while loading the plugin
          // Skip the plugin and continue loading the other one
          if (loadError) {
            process.logger.warn(loadError.message, {
              action: 'loadPlugins',
              plugin: pluginPath
            });
            process.logger.info('Plugin ' + pluginPath + ' skipped');
          } else {

            // Plugin successfully loaded
            plugins.push(loadedPlugin);

          }

          callback();
        });

      });
    });

    async.parallel(asyncActions, function(error, results) {
      callback(null, plugins);
    });

  });

};

/**
 * Loads a single plugin by its path.
 *
 * @example
 *     var pluginLoader = process.require('app/server/loaders/pluginLoader.js');
 *
 *     // Load a plugin
 *     pluginLoader.loadPlugin('/node_modules/@openveo/publish', function(error, loadedPlugin){
 *       console.log(loadedPlugin);
 *     }
 *
 * @method loadPlugin
 * @static
 * @async
 * @param {String} pluginPath Absolute path to the plugin directory
 * @param {Function} callback A callback with two arguments :
 *    - **Error** An Error object or null
 *    - **Plugin** The loaded plugin or null
 * @throws {TypeError} An error if plugin path or starting path is not a valid string
 */
module.exports.loadPlugin = function(pluginPath, callback) {
  var plugin = null;
  var pluginComposition = [];
  var regResults;
  var reg = /(?:node_modules[/|\\](?:(?:@openveo[/|\\]([^/\\]*))|(?:openveo-([^/\\]*))))/g;

  // Extract the plugin(s) name(s) from the plugin path
  // e.g :
  // From plugin path : /www/openveo/node_modules/@openveo/plugin1/node_modules/openveo-plugin2
  // Retrieve : ['plugin1', 'plugin2']
  // The plugin to load is plugin2 which is also a sub plugin of plugin1
  while ((regResults = reg.exec(pluginPath)) !== null) {

    // Remove global match
    regResults.shift(0);

    // Get plugin name
    for (var i = 0; i < regResults.length; i++)
      if (regResults[i])
        pluginComposition.push(regResults[i]);
  }

  try {

    // Try to load the main file of the plugin
    var Plugin = require(pluginPath);

    // Validate that we are sharing the same openveo API and that
    // the main file returned by the plugin is a valid instance
    // of the Plugin Object from @openveo/api
    if (Plugin.prototype instanceof openVeoApi.plugin.Plugin && pluginComposition.length) {

      // Instanciate the Plugin Object
      plugin = new Plugin();
      plugin.path = pluginPath;

      // Define plugin router mount path
      // Plugin router is mounted on a subpath to avoid collisions
      // with the main openveo application
      // e.g "/plugin1"
      plugin.mountPath = '/' + pluginComposition.join('/');

    }

  } catch (e) {
    if (e.code === 'MODULE_NOT_FOUND')
      process.logger.info('Plugin ' + pluginPath + ' doesn\'t have a main file', {
        action: 'loadPlugin',
        message: e.message
      });
    else
      process.logger.error('Error while loading plugin ' + pluginPath, {
        action: 'loadPlugin',
        error: e.message,
        stack: e.stack
      });

    return callback(new Error(e.message));
  }

  if (!plugin)
    return callback(new Error('Plugin ' + pluginPath + ' is not an instance of Plugin'));

  // Complete plugin information adding the name of the plugin
  plugin.name = pluginComposition[pluginComposition.length - 1];

  this.loadPluginMetadata(plugin, callback);
};

/**
 * Loads plugin's configuration.
 *
 * @method loadPluginMetadata
 * @static
 * @async
 * @param {Plugin} plugin The plugin
 * @param {Function} callback A callback with :
 *    - **Error** An Error if something went wrong
 */
module.exports.loadPluginMetadata = function(plugin, callback) {
  async.parallel(
    [
      function(callback) {

        // Test if an assets directory exists at plugin root level
        fs.stat(path.join(plugin.path, 'assets'), function(error, stats) {

          if (stats && stats.isDirectory())
            plugin.assets = path.join(plugin.path, 'assets');

          callback();

        });
      },
      function(callback) {

        // Test if an i18n directory exists at plugin's root level
        fs.stat(path.join(plugin.path, 'i18n'), function(error, stats) {

          if (stats && stats.isDirectory())
            plugin.i18nDirectory = path.join(plugin.path, 'i18n');

          callback();

        });
      },
      function(callback) {

        // Test if a file "conf.js" exists at plugin root level
        fs.stat(path.join(plugin.path, 'conf.js'), function(error, stats) {

          if (stats && stats.isFile()) {
            try {

              // Try to load plugin configuration file
              var pluginConf = require(path.join(plugin.path, 'conf.js'));

              plugin.custom = pluginConf['custom'] || null;
              plugin.webServiceScopes = pluginConf['webServiceScopes'] || null;
              plugin.permissions = pluginConf['permissions'] || null;

              // Got views folders for this plugin
              if (pluginConf['viewsFolders'] && pluginConf['viewsFolders'].length) {
                plugin.viewsFolders = [];
                pluginConf['viewsFolders'].forEach(function(viewsFolder) {
                  plugin.viewsFolders.push(path.join(plugin.path, viewsFolder));
                });
              }

              // Got images thumbnailable folders for this plugin
              var imageProcessing = pluginConf.imageProcessing;

              if (imageProcessing) {
                if (imageProcessing.folders && imageProcessing.folders.length) {
                  plugin.imageProcessingFolders = [];
                  imageProcessing.folders.forEach(function(folder) {
                    folder.imagesDirectory = path.join(plugin.path, folder.imagesDirectory);

                    if (folder.cacheDirectory)
                      folder.cacheDirectory = path.join(plugin.path, folder.cacheDirectory);

                    plugin.imageProcessingFolders.push(folder);
                  });

                  if (imageProcessing.styles && imageProcessing.styles.length)
                    plugin.imageProcessingStyles = imageProcessing.styles;
                }
              }

              // Retrieve routes and back end conf from plugin conf
              var pluginHttp = pluginConf['http'];
              var pluginSocket = pluginConf['socket'];
              var backEndConf = pluginConf['backOffice'];

              // Got routes for this plugin
              // Retrieve public, private and Web Service routes
              if (pluginHttp && pluginHttp['routes']) {
                plugin.routes = pluginHttp['routes']['public'];
                plugin.privateRoutes = pluginHttp['routes']['private'];
                plugin.webServiceRoutes = pluginHttp['routes']['ws'];
              }

              // Got socket namespaces for this plugin
              if (pluginSocket && pluginSocket['namespaces'])
                plugin.namespaces = pluginSocket['namespaces'];

              // Got entities
              if (pluginConf['entities'])
                plugin.entities = pluginConf['entities'];

              // Got back end conf
              if (backEndConf) {

                // Retrieve back end menu pages from plugin conf
                plugin.menu = backEndConf['menu'] || null;

                // Retrieve back end scripts and css from plugin conf
                plugin.scriptLibFiles = backEndConf['scriptLibFiles'] || null;
                plugin.scriptFiles = backEndConf['scriptFiles'] || null;
                plugin.cssFiles = backEndConf['cssFiles'] || null;

              }
            } catch (e) {
              process.logger.warn(e.message, {
                action: 'loadPluginMetadata',
                plugin: plugin.name
              });
              callback(new Error(e.message));
              return;
            }
          }

          callback();

        });

      },
      function(callback) {

        // Test if a package.json file exists at plugin's root level
        fs.stat(path.join(plugin.path, 'package.json'), function(error, stats) {

          if (stats && stats.isFile()) {
            var pluginPackage = require(path.join(plugin.path, 'package.json'));
            plugin.version = {
              name: pluginPackage['name'],
              version: pluginPackage['version']
            } || null;
          }

          callback();

        });
      },
      function(callback) {
        var db = storage.getDatabase();
        db.getOne(
          'core_system',
          new ResourceFilter().equal('name', plugin.name),
          null,
          function(error, pluginInformation) {
            if (error) return callback(error);

            var lastVersion = '0.0.0';
            if (pluginInformation) lastVersion = pluginInformation.version;

            var migrationPath = path.join(plugin.path, 'migrations');
            migrationLoader.getDiffMigrationScript(migrationPath, lastVersion, function(error, migrations) {
              if (error) return callback(error);
              if (migrations && Object.keys(migrations).length > 0)
                plugin.migrations = migrations;
              callback();
            });
          }
        );
      }
    ],
    function(error) {

      // Got all plugin's metadata
      if (callback)
        callback(error, plugin);

    }
  );
};