diff --git a/src/plugins.js b/src/plugins.js index 6c27e96418..fb0d9256ab 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -6,7 +6,6 @@ var fs = require('fs'), winston = require('winston'), semver = require('semver'), express = require('express'), - npm = require('npm'), db = require('./database'), emitter = require('./emitter'), @@ -14,13 +13,16 @@ var fs = require('fs'), translator = require('../public/src/translator'), utils = require('../public/src/utils'), hotswap = require('./hotswap'), - pkg = require('../package.json'), controllers = require('./controllers'), app, middleware; (function(Plugins) { + require('./plugins/install')(Plugins); + require('./plugins/load')(Plugins); + require('./plugins/hooks')(Plugins); + Plugins.libraries = {}; Plugins.loadedHooks = {}; Plugins.staticDirs = {}; @@ -32,6 +34,11 @@ var fs = require('fs'), Plugins.initialized = false; + Plugins.requireLibrary = function(pluginData, libraryPath) { + Plugins.libraries[pluginData.id] = require(libraryPath); + Plugins.libraryPaths.push(libraryPath); + }; + Plugins.init = function(nbbApp, nbbMiddleware) { if (Plugins.initialized) { return; @@ -133,420 +140,6 @@ var fs = require('fs'), }); }; - Plugins.loadPlugin = function(pluginPath, callback) { - fs.readFile(path.join(pluginPath, 'plugin.json'), function(err, data) { - if (err) { - return callback(pluginPath.match('nodebb-theme') ? null : err); - } - - var pluginData, libraryPath, staticDir; - - try { - pluginData = JSON.parse(data); - } catch (err) { - var pluginDir = pluginPath.split(path.sep); - pluginDir = pluginDir[pluginDir.length -1]; - - winston.error('[plugins/' + pluginDir + '] Plugin not loaded - please check its plugin.json for errors'); - return callback(); - } - - /* - Starting v0.5.0, `minver` is deprecated in favour of `compatibility`. - `minver` will be transparently parsed to `compatibility` until v0.6.0, - at which point `minver` will not be parsed altogether. - - Please see NodeBB/NodeBB#1437 for more details - */ - if (pluginData.minver && !pluginData.compatibility) { - pluginData.compatibility = '~' + pluginData.minver; - } - // End backwards compatibility block (#1437) - - if (pluginData.compatibility && semver.validRange(pluginData.compatibility)) { - if (!semver.gtr(pkg.version, pluginData.compatibility)) { - // NodeBB may not be new enough to run this plugin - process.stdout.write('\n'); - winston.warn('[plugins/' + pluginData.id + '] This plugin may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing.'); - winston.warn('[plugins/' + pluginData.id + '] In the event of an unresponsive NodeBB caused by this plugin, run ./nodebb reset plugin="' + pluginData.id + '".'); - process.stdout.write('\n'); - } - } - - async.parallel([ - function(next) { - if (pluginData.library) { - libraryPath = path.join(pluginPath, pluginData.library); - - fs.exists(libraryPath, function(exists) { - if (exists) { - if (!Plugins.libraries[pluginData.id]) { - Plugins.libraries[pluginData.id] = require(libraryPath); - Plugins.libraryPaths.push(libraryPath); - } - - // Register hooks for this plugin - if (pluginData.hooks && Array.isArray(pluginData.hooks) && pluginData.hooks.length > 0) { - async.each(pluginData.hooks, function(hook, next) { - Plugins.registerHook(pluginData.id, hook, next); - }, next); - } else { - next(null); - } - } else { - winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id); - next(); - } - }); - } else { - winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id); - next(); - } - }, - function(next) { - // Static Directories for Plugins - var realPath, - validMappedPath = /^[\w\-_]+$/; - - pluginData.staticDirs = pluginData.staticDirs || {}; - - // Deprecated, to be removed v0.5 - if (pluginData.staticDir) { - winston.warn('[plugins/' + pluginData.id + '] staticDir is deprecated, use staticDirs instead'); - Plugins.staticDirs[pluginData.id] = path.join(pluginPath, pluginData.staticDir); - } - - function mapStaticDirs(mappedPath) { - if (Plugins.staticDirs[mappedPath]) { - winston.warn('[plugins/' + pluginData.id + '] Mapped path (' + mappedPath + ') already specified!'); - } else if (!validMappedPath.test(mappedPath)) { - winston.warn('[plugins/' + pluginData.id + '] Invalid mapped path specified: ' + mappedPath + '. Path must adhere to: ' + validMappedPath.toString()); - } else { - realPath = pluginData.staticDirs[mappedPath]; - staticDir = path.join(pluginPath, realPath); - - (function(staticDir) { - fs.exists(staticDir, function(exists) { - if (exists) { - Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir; - } else { - winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + mappedPath + ' => ' + staticDir + '\' not found.'); - } - }); - }(staticDir)); - } - } - - for(var key in pluginData.staticDirs) { - if (pluginData.staticDirs.hasOwnProperty(key)) { - mapStaticDirs(key); - } - } - - next(); - }, - function(next) { - // CSS Files for plugins - if (pluginData.css && pluginData.css instanceof Array) { - if (global.env === 'development') { - winston.verbose('[plugins] Found ' + pluginData.css.length + ' CSS file(s) for plugin ' + pluginData.id); - } - - Plugins.cssFiles = Plugins.cssFiles.concat(pluginData.css.map(function(file) { - return path.join(pluginData.id, file); - })); - } - - next(); - }, - function(next) { - // LESS files for plugins - if (pluginData.less && pluginData.less instanceof Array) { - if (global.env === 'development') { - winston.verbose('[plugins] Found ' + pluginData.less.length + ' LESS file(s) for plugin ' + pluginData.id); - } - - Plugins.lessFiles = Plugins.lessFiles.concat(pluginData.less.map(function(file) { - return path.join(pluginData.id, file); - })); - } - - next(); - }, - function(next) { - // Client-side scripts - if (pluginData.scripts && pluginData.scripts instanceof Array) { - if (global.env === 'development') { - winston.verbose('[plugins] Found ' + pluginData.scripts.length + ' js file(s) for plugin ' + pluginData.id); - } - - Plugins.clientScripts = Plugins.clientScripts.concat(pluginData.scripts.map(function(file) { - return path.join(__dirname, '../node_modules/', pluginData.id, file); - })); - } - - next(); - }, - function(next) { - if (pluginData.languages && typeof pluginData.languages === 'string') { - var pathToFolder = path.join(__dirname, '../node_modules/', pluginData.id, pluginData.languages); - - utils.walk(pathToFolder, function(err, languages) { - var arr = []; - - async.each(languages, function(pathToLang, next) { - fs.readFile(pathToLang, function(err, file) { - var json; - - try { - json = JSON.parse(file.toString()); - } catch (err) { - winston.error('[plugins] Unable to parse custom language file: ' + pathToLang + '\r\n' + err.stack); - return next(err); - } - - arr.push({ - file: json, - route: pathToLang.replace(pathToFolder, '') - }); - - next(err); - }); - }, function(err) { - Plugins.customLanguages = Plugins.customLanguages.concat(arr); - next(err); - }); - }); - } else { - next(); - } - } - ], function(err) { - if (!err) { - if (global.env === 'development') { - winston.verbose('[plugins] Loaded plugin: ' + pluginData.id); - } - callback(); - } else { - callback(new Error('Could not load plugin system')); - } - }); - }); - }; - - Plugins.registerHook = function(id, data, callback) { - /* - `data` is an object consisting of (* is required): - `data.hook`*, the name of the NodeBB hook - `data.method`*, the method called in that plugin - `data.priority`, the relative priority of the method when it is eventually called (default: 10) - */ - - var method; - - if (data.hook && data.method) { - data.id = id; - if (!data.priority) { - data.priority = 10; - } - - if (typeof data.method === 'string' && data.method.length > 0) { - method = data.method.split('.').reduce(function(memo, prop) { - if (memo !== null && memo[prop]) { - return memo[prop]; - } else { - // Couldn't find method by path, aborting - return null; - } - }, Plugins.libraries[data.id]); - - // Write the actual method reference to the hookObj - data.method = method; - - register(); - } else if (typeof data.method === 'function') { - register(); - } else { - winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method); - } - } - - function register() { - Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || []; - Plugins.loadedHooks[data.hook].push(data); - - if (typeof callback === 'function') { - callback(); - } - } - }; - - Plugins.hasListeners = function(hook) { - return !!(Plugins.loadedHooks[hook] && Plugins.loadedHooks[hook].length > 0); - }; - - Plugins.fireHook = function(hook, params, callback) { - callback = typeof callback === 'function' ? callback : function() {}; - - var hookList = Plugins.loadedHooks[hook]; - - if (!Array.isArray(hookList) || !hookList.length) { - return callback(null, params); - } - - var hookType = hook.split(':')[0]; - switch (hookType) { - case 'filter': - fireFilterHook(hook, hookList, params, callback); - break; - case 'action': - fireActionHook(hook, hookList, params, callback); - break; - case 'static': - fireStaticHook(hook, hookList, params, callback); - break; - default: - winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook); - break; - } - }; - - function fireFilterHook(hook, hookList, params, callback) { - async.reduce(hookList, params, function(params, hookObj, next) { - if (typeof hookObj.method !== 'function') { - if (global.env === 'development') { - winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); - } - return next(null, params); - } - - hookObj.method(params, next); - - }, function(err, values) { - if (err) { - winston.error('[plugins] ' + hook + ', ' + err.message); - } - - callback(err, values); - }); - } - - function fireActionHook(hook, hookList, params, callback) { - async.each(hookList, function(hookObj, next) { - - if (typeof hookObj.method !== 'function') { - if (global.env === 'development') { - winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); - } - return next(); - } - - hookObj.method(params); - next(); - }, callback); - } - - function fireStaticHook(hook, hookList, params, callback) { - async.each(hookList, function(hookObj, next) { - if (typeof hookObj.method === 'function') { - var timedOut = false; - - var timeoutId = setTimeout(function() { - winston.warn('[plugins] Callback timed out, hook \'' + hook + '\' in plugin \'' + hookObj.id + '\''); - timedOut = true; - next(); - }, 5000); - - hookObj.method(params, function() { - clearTimeout(timeoutId); - if (!timedOut) { - next.apply(null, arguments); - } - }); - } else { - next(); - } - }, callback); - } - - Plugins.isActive = function(id, callback) { - db.isSetMember('plugins:active', id, callback); - }; - - Plugins.toggleActive = function(id, callback) { - Plugins.isActive(id, function(err, active) { - if (err) { - winston.warn('[plugins] Could not toggle active state on plugin \'' + id + '\''); - return callback(err); - } - - db[(active ? 'setRemove' : 'setAdd')]('plugins:active', id, function(err, success) { - if (err) { - winston.warn('[plugins] Could not toggle active state on plugin \'' + id + '\''); - return callback(err); - } - - meta.reloadRequired = true; - - if (active) { - Plugins.fireHook('action:plugin.deactivate', id); - } - - if (typeof callback === 'function') { - callback(null, { - id: id, - active: !active - }); - } - }); - }); - }; - - Plugins.toggleInstall = function(id, version, callback) { - Plugins.isInstalled(id, function(err, installed) { - if (err) { - return callback(err); - } - - async.waterfall([ - function(next) { - Plugins.isActive(id, next); - }, - function(active, next) { - if (active) { - Plugins.toggleActive(id, function(err, status) { - next(err); - }); - return; - } - next(); - }, - function(next) { - npm.load({}, next); - }, - function(res, next) { - npm.commands[installed ? 'uninstall' : 'install'](installed ? id : [id + '@' + (version || 'latest')], next); - } - ], function(err) { - callback(err, { - id: id, - installed: !installed - }); - }); - }); - }; - - Plugins.upgrade = function(id, version, callback) { - async.waterfall([ - function(next) { - npm.load({}, next); - }, - function(res, next) { - npm.commands.install([id + '@' + (version || 'latest')], next); - } - ], callback); - }; - Plugins.getTemplates = function(callback) { var templates = {}; @@ -660,14 +253,6 @@ var fs = require('fs'), return ''; } - Plugins.isInstalled = function(id, callback) { - var pluginDir = path.join(__dirname, '../node_modules', id); - - fs.stat(pluginDir, function(err, stats) { - callback(null, err ? false : stats.isDirectory()); - }); - }; - Plugins.showInstalled = function(callback) { var npmPluginPath = path.join(__dirname, '../node_modules'); @@ -755,9 +340,7 @@ var fs = require('fs'), next(null, plugins); }); } - ], function(err, plugins) { - callback(err, plugins); - }); + ], callback); }; Plugins.clearRequireCache = function(next) { diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js new file mode 100644 index 0000000000..eea9f773c1 --- /dev/null +++ b/src/plugins/hooks.js @@ -0,0 +1,141 @@ +'use strict'; + +var winston = require('winston'), + async = require('async'); + +module.exports = function(Plugins) { + + Plugins.registerHook = function(id, data, callback) { + function register() { + Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || []; + Plugins.loadedHooks[data.hook].push(data); + + if (typeof callback === 'function') { + callback(); + } + } + /* + `data` is an object consisting of (* is required): + `data.hook`*, the name of the NodeBB hook + `data.method`*, the method called in that plugin + `data.priority`, the relative priority of the method when it is eventually called (default: 10) + */ + + var method; + + if (data.hook && data.method) { + data.id = id; + if (!data.priority) { + data.priority = 10; + } + + if (typeof data.method === 'string' && data.method.length > 0) { + method = data.method.split('.').reduce(function(memo, prop) { + if (memo !== null && memo[prop]) { + return memo[prop]; + } else { + // Couldn't find method by path, aborting + return null; + } + }, Plugins.libraries[data.id]); + + // Write the actual method reference to the hookObj + data.method = method; + + register(); + } else if (typeof data.method === 'function') { + register(); + } else { + winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method); + } + } + }; + + Plugins.fireHook = function(hook, params, callback) { + callback = typeof callback === 'function' ? callback : function() {}; + + var hookList = Plugins.loadedHooks[hook]; + + if (!Array.isArray(hookList) || !hookList.length) { + return callback(null, params); + } + + var hookType = hook.split(':')[0]; + switch (hookType) { + case 'filter': + fireFilterHook(hook, hookList, params, callback); + break; + case 'action': + fireActionHook(hook, hookList, params, callback); + break; + case 'static': + fireStaticHook(hook, hookList, params, callback); + break; + default: + winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook); + break; + } + }; + + function fireFilterHook(hook, hookList, params, callback) { + async.reduce(hookList, params, function(params, hookObj, next) { + if (typeof hookObj.method !== 'function') { + if (global.env === 'development') { + winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); + } + return next(null, params); + } + + hookObj.method(params, next); + + }, function(err, values) { + if (err) { + winston.error('[plugins] ' + hook + ', ' + err.message); + } + + callback(err, values); + }); + } + + function fireActionHook(hook, hookList, params, callback) { + async.each(hookList, function(hookObj, next) { + + if (typeof hookObj.method !== 'function') { + if (global.env === 'development') { + winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); + } + return next(); + } + + hookObj.method(params); + next(); + }, callback); + } + + function fireStaticHook(hook, hookList, params, callback) { + async.each(hookList, function(hookObj, next) { + if (typeof hookObj.method === 'function') { + var timedOut = false; + + var timeoutId = setTimeout(function() { + winston.warn('[plugins] Callback timed out, hook \'' + hook + '\' in plugin \'' + hookObj.id + '\''); + timedOut = true; + next(); + }, 5000); + + hookObj.method(params, function() { + clearTimeout(timeoutId); + if (!timedOut) { + next.apply(null, arguments); + } + }); + } else { + next(); + } + }, callback); + } + + Plugins.hasListeners = function(hook) { + return !!(Plugins.loadedHooks[hook] && Plugins.loadedHooks[hook].length > 0); + }; +}; \ No newline at end of file diff --git a/src/plugins/install.js b/src/plugins/install.js new file mode 100644 index 0000000000..33ad260d16 --- /dev/null +++ b/src/plugins/install.js @@ -0,0 +1,101 @@ +'use strict'; + +var winston = require('winston'), + async = require('async'), + npm = require('npm'), + path = require('path'), + fs = require('fs'), + + db = require('../database'), + meta = require('../meta'), + pubsub = require('../pubsub'); + + +module.exports = function(Plugins) { + + Plugins.toggleActive = function(id, callback) { + Plugins.isActive(id, function(err, active) { + if (err) { + winston.warn('[plugins] Could not toggle active state on plugin \'' + id + '\''); + return callback(err); + } + + db[(active ? 'setRemove' : 'setAdd')]('plugins:active', id, function(err, success) { + if (err) { + winston.warn('[plugins] Could not toggle active state on plugin \'' + id + '\''); + return callback(err); + } + + meta.reloadRequired = true; + + if (active) { + Plugins.fireHook('action:plugin.deactivate', id); + } + + if (typeof callback === 'function') { + callback(null, { + id: id, + active: !active + }); + } + }); + }); + }; + + Plugins.toggleInstall = function(id, version, callback) { + Plugins.isInstalled(id, function(err, installed) { + if (err) { + return callback(err); + } + + async.waterfall([ + function(next) { + Plugins.isActive(id, next); + }, + function(active, next) { + if (active) { + Plugins.toggleActive(id, function(err, status) { + next(err); + }); + return; + } + next(); + }, + function(next) { + npm.load({}, next); + }, + function(res, next) { + npm.commands[installed ? 'uninstall' : 'install'](installed ? id : [id + '@' + (version || 'latest')], next); + } + ], function(err) { + callback(err, { + id: id, + installed: !installed + }); + }); + }); + }; + + Plugins.upgrade = function(id, version, callback) { + async.waterfall([ + function(next) { + npm.load({}, next); + }, + function(res, next) { + npm.commands.install([id + '@' + (version || 'latest')], next); + } + ], callback); + }; + + Plugins.isInstalled = function(id, callback) { + var pluginDir = path.join(__dirname, '../node_modules', id); + + fs.stat(pluginDir, function(err, stats) { + callback(null, err ? false : stats.isDirectory()); + }); + }; + + Plugins.isActive = function(id, callback) { + db.isSetMember('plugins:active', id, callback); + }; +}; \ No newline at end of file diff --git a/src/plugins/load.js b/src/plugins/load.js new file mode 100644 index 0000000000..4b9b3128b3 --- /dev/null +++ b/src/plugins/load.js @@ -0,0 +1,219 @@ +'use strict'; + +var fs = require('fs'), + path = require('path'), + semver = require('semver'), + async = require('async'), + winston = require('winston'), + pkg = require('../../package.json'), + utils = require('../../public/src/utils'); + + +module.exports = function(Plugins) { + + Plugins.loadPlugin = function(pluginPath, callback) { + fs.readFile(path.join(pluginPath, 'plugin.json'), function(err, data) { + if (err) { + return callback(pluginPath.match('nodebb-theme') ? null : err); + } + + var pluginData, staticDir; + + try { + pluginData = JSON.parse(data); + } catch (err) { + var pluginDir = pluginPath.split(path.sep); + pluginDir = pluginDir[pluginDir.length -1]; + + winston.error('[plugins/' + pluginDir + '] Plugin not loaded - please check its plugin.json for errors'); + return callback(); + } + + if (pluginData.compatibility && semver.validRange(pluginData.compatibility)) { + if (!semver.gtr(pkg.version, pluginData.compatibility)) { + // NodeBB may not be new enough to run this plugin + process.stdout.write('\n'); + winston.warn('[plugins/' + pluginData.id + '] This plugin may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing.'); + winston.warn('[plugins/' + pluginData.id + '] In the event of an unresponsive NodeBB caused by this plugin, run ./nodebb reset plugin="' + pluginData.id + '".'); + process.stdout.write('\n'); + } + } + + async.parallel([ + function(next) { + registerHooks(pluginData, pluginPath, next); + }, + function(next) { + mapStaticDirectories(pluginData, pluginPath, next); + }, + function(next) { + mapCssFiles(pluginData, next); + }, + function(next) { + mapLessFiles(pluginData, next); + }, + function(next) { + mapClientSideScripts(pluginData, next); + }, + function(next) { + loadLanguages(pluginData, next); + } + ], function(err) { + if (err) { + winston.verbose('[plugins] Could not load plugin : ' + pluginData.id); + return callback(err); + } + + winston.verbose('[plugins] Loaded plugin: ' + pluginData.id); + callback(); + }); + }); + }; + + function registerHooks(pluginData, pluginPath, callback) { + function libraryNotFound() { + winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id); + callback(); + } + + if (!pluginData.library) { + return libraryNotFound(); + } + + var libraryPath = path.join(pluginPath, pluginData.library); + + fs.exists(libraryPath, function(exists) { + if (!exists) { + return libraryNotFound(); + } + + if (!Plugins.libraries[pluginData.id]) { + Plugins.requireLibrary(pluginData, libraryPath); + } + + if (Array.isArray(pluginData.hooks) && pluginData.hooks.length > 0) { + async.each(pluginData.hooks, function(hook, next) { + Plugins.registerHook(pluginData.id, hook, next); + }, callback); + } else { + callback(); + } + }); + } + + function mapStaticDirectories(pluginData, pluginPath, callback) { + function mapStaticDirs(mappedPath, callback) { + if (Plugins.staticDirs[mappedPath]) { + winston.warn('[plugins/' + pluginData.id + '] Mapped path (' + mappedPath + ') already specified!'); + callback(); + } else if (!validMappedPath.test(mappedPath)) { + winston.warn('[plugins/' + pluginData.id + '] Invalid mapped path specified: ' + mappedPath + '. Path must adhere to: ' + validMappedPath.toString()); + callback(); + } else { + var realPath = pluginData.staticDirs[mappedPath]; + var staticDir = path.join(pluginPath, realPath); + + fs.exists(staticDir, function(exists) { + if (exists) { + Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir; + } else { + winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + mappedPath + ' => ' + staticDir + '\' not found.'); + } + callback(); + }); + } + } + + var validMappedPath = /^[\w\-_]+$/; + + pluginData.staticDirs = pluginData.staticDirs || {}; + + var dirs = Object.keys(pluginData.staticDirs); + async.each(dirs, mapStaticDirs, callback); + } + + function mapCssFiles(pluginData, callback) { + if (Array.isArray(pluginData.css)) { + if (global.env === 'development') { + winston.verbose('[plugins] Found ' + pluginData.css.length + ' CSS file(s) for plugin ' + pluginData.id); + } + + Plugins.cssFiles = Plugins.cssFiles.concat(pluginData.css.map(function(file) { + return path.join(pluginData.id, file); + })); + } + + callback(); + } + + function mapLessFiles(pluginData, callback) { + if (Array.isArray(pluginData.less)) { + if (global.env === 'development') { + winston.verbose('[plugins] Found ' + pluginData.less.length + ' LESS file(s) for plugin ' + pluginData.id); + } + + Plugins.lessFiles = Plugins.lessFiles.concat(pluginData.less.map(function(file) { + return path.join(pluginData.id, file); + })); + } + + callback(); + } + + function mapClientSideScripts(pluginData, callback) { + if (Array.isArray(pluginData.scripts)) { + if (global.env === 'development') { + winston.verbose('[plugins] Found ' + pluginData.scripts.length + ' js file(s) for plugin ' + pluginData.id); + } + + Plugins.clientScripts = Plugins.clientScripts.concat(pluginData.scripts.map(function(file) { + return path.join(__dirname, '../../node_modules/', pluginData.id, file); + })); + } + + callback(); + } + + function loadLanguages(pluginData, callback) { + if (typeof pluginData.languages !== 'string') { + return callback(); + } + + var pathToFolder = path.join(__dirname, '../../node_modules/', pluginData.id, pluginData.languages); + + utils.walk(pathToFolder, function(err, languages) { + var arr = []; + + async.each(languages, function(pathToLang, next) { + fs.readFile(pathToLang, function(err, file) { + if (err) { + return next(err); + } + var json; + + try { + json = JSON.parse(file.toString()); + } catch (err) { + winston.error('[plugins] Unable to parse custom language file: ' + pathToLang + '\r\n' + err.stack); + return next(err); + } + + arr.push({ + file: json, + route: pathToLang.replace(pathToFolder, '') + }); + + next(); + }); + }, function(err) { + if (err) { + return callback(err); + } + Plugins.customLanguages = Plugins.customLanguages.concat(arr); + callback(); + }); + }); + + } + +}; \ No newline at end of file