feat: #7743 plugins

v1.18.x
Barış Soner Uşaklı 6 years ago
parent f5f5f76b12
commit c126cd8572

@ -213,3 +213,5 @@ events.output = function (numEvents) {
process.exit(0); process.exit(0);
}); });
}; };
require('./promisify')(events);

@ -216,7 +216,7 @@ Data.getModules = async function getModules(pluginData) {
await Promise.all(Object.keys(pluginModules).map(key => processModule(key))); await Promise.all(Object.keys(pluginModules).map(key => processModule(key)));
const len = Object.keys(modules).length; const len = Object.keys(modules).length;
winston.info('[plugins] Found ' + len + ' AMD-style module(s) for plugin ' + pluginData.id); winston.verbose('[plugins] Found ' + len + ' AMD-style module(s) for plugin ' + pluginData.id);
return modules; return modules;
}; };

@ -10,30 +10,31 @@ module.exports = function (Plugins) {
}; };
Plugins.internals = { Plugins.internals = {
_register: function (data, callback) { _register: function (data) {
Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || []; Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || [];
Plugins.loadedHooks[data.hook].push(data); Plugins.loadedHooks[data.hook].push(data);
callback();
}, },
}; };
const hookTypeToMethod = {
filter: fireFilterHook,
action: fireActionHook,
static: fireStaticHook,
response: fireResponseHook,
};
/* /*
`data` is an object consisting of (* is required): `data` is an object consisting of (* is required):
`data.hook`*, the name of the NodeBB hook `data.hook`*, the name of the NodeBB hook
`data.method`*, the method called in that plugin (can be an array of functions) `data.method`*, the method called in that plugin (can be an array of functions)
`data.priority`, the relative priority of the method when it is eventually called (default: 10) `data.priority`, the relative priority of the method when it is eventually called (default: 10)
*/ */
Plugins.registerHook = function (id, data, callback) { Plugins.registerHook = function (id, data) {
callback = callback || function () {}; if (!data.hook || !data.method) {
winston.warn('[plugins/' + id + '] registerHook called with invalid data.hook/method', data);
if (!data.hook) { return;
winston.warn('[plugins/' + id + '] registerHook called with invalid data.hook', data);
return callback();
} }
var method;
if (Plugins.deprecatedHooks[data.hook]) { if (Plugins.deprecatedHooks[data.hook]) {
winston.warn('[plugins/' + id + '] Hook `' + data.hook + '` is deprecated, ' + winston.warn('[plugins/' + id + '] Hook `' + data.hook + '` is deprecated, ' +
(Plugins.deprecatedHooks[data.hook] ? (Plugins.deprecatedHooks[data.hook] ?
@ -42,7 +43,6 @@ module.exports = function (Plugins) {
)); ));
} }
if (data.hook && data.method) {
data.id = id; data.id = id;
if (!data.priority) { if (!data.priority) {
data.priority = 10; data.priority = 10;
@ -50,12 +50,12 @@ module.exports = function (Plugins) {
if (Array.isArray(data.method) && data.method.every(method => typeof method === 'function' || typeof method === 'string')) { if (Array.isArray(data.method) && data.method.every(method => typeof method === 'function' || typeof method === 'string')) {
// Go go gadget recursion! // Go go gadget recursion!
async.eachSeries(data.method, function (method, next) { data.method.forEach(function (method) {
const singularData = Object.assign({}, data, { method: method }); const singularData = Object.assign({}, data, { method: method });
Plugins.registerHook(id, singularData, next); Plugins.registerHook(id, singularData);
}, callback); });
} else if (typeof data.method === 'string' && data.method.length > 0) { } else if (typeof data.method === 'string' && data.method.length > 0) {
method = data.method.split('.').reduce(function (memo, prop) { const method = data.method.split('.').reduce(function (memo, prop) {
if (memo && memo[prop]) { if (memo && memo[prop]) {
return memo[prop]; return memo[prop];
} }
@ -66,13 +66,11 @@ module.exports = function (Plugins) {
// Write the actual method reference to the hookObj // Write the actual method reference to the hookObj
data.method = method; data.method = method;
Plugins.internals._register(data, callback); Plugins.internals._register(data);
} else if (typeof data.method === 'function') { } else if (typeof data.method === 'function') {
Plugins.internals._register(data, callback); Plugins.internals._register(data);
} else { } else {
winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method); winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method);
return callback();
}
} }
}; };
@ -83,52 +81,33 @@ module.exports = function (Plugins) {
}); });
}; };
Plugins.fireHook = function (hook, params, callback) { Plugins.fireHook = async function (hook, params) {
callback = typeof callback === 'function' ? callback : function () {}; const hookList = Plugins.loadedHooks[hook];
function done(err, result) { const hookType = hook.split(':')[0];
if (err) {
return callback(err);
}
if (hook !== 'action:plugins.firehook') { if (hook !== 'action:plugins.firehook') {
Plugins.fireHook('action:plugins.firehook', { hook: hook, params: params }); winston.verbose('[plugins/fireHook] ' + hook);
}
if (result !== undefined) {
callback(null, result);
} else {
callback();
} }
if (!hookTypeToMethod[hookType]) {
winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
return;
} }
var hookList = Plugins.loadedHooks[hook]; const result = await hookTypeToMethod[hookType](hook, hookList, params);
var hookType = hook.split(':')[0];
if (hook !== 'action:plugins.firehook') { if (hook !== 'action:plugins.firehook') {
winston.verbose('[plugins/fireHook] ' + hook); Plugins.fireHook('action:plugins.firehook', { hook: hook, params: params });
} }
switch (hookType) { if (result !== undefined) {
case 'filter': return result;
fireFilterHook(hook, hookList, params, done);
break;
case 'action':
fireActionHook(hook, hookList, params, done);
break;
case 'static':
fireStaticHook(hook, hookList, params, done);
break;
case 'response':
fireResponseHook(hook, hookList, params, done);
break;
default:
winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
callback();
break;
} }
}; };
function fireFilterHook(hook, hookList, params, callback) { async function fireFilterHook(hook, hookList, params) {
if (!Array.isArray(hookList) || !hookList.length) { if (!Array.isArray(hookList) || !hookList.length) {
return callback(null, params); return params;
} }
async.reduce(hookList, params, function (params, hookObj, next) { return await async.reduce(hookList, params, function (params, hookObj, next) {
if (typeof hookObj.method !== 'function') { if (typeof hookObj.method !== 'function') {
if (global.env === 'development') { if (global.env === 'development') {
winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.');
@ -142,14 +121,14 @@ module.exports = function (Plugins) {
err => setImmediate(next, err) err => setImmediate(next, err)
); );
} }
}, callback); });
} }
function fireActionHook(hook, hookList, params, callback) { async function fireActionHook(hook, hookList, params) {
if (!Array.isArray(hookList) || !hookList.length) { if (!Array.isArray(hookList) || !hookList.length) {
return callback(); return;
} }
async.each(hookList, function (hookObj, next) { await async.each(hookList, function (hookObj, next) {
if (typeof hookObj.method !== 'function') { if (typeof hookObj.method !== 'function') {
if (global.env === 'development') { if (global.env === 'development') {
winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.');
@ -159,14 +138,14 @@ module.exports = function (Plugins) {
hookObj.method(params); hookObj.method(params);
next(); next();
}, callback); });
} }
function fireStaticHook(hook, hookList, params, callback) { async function fireStaticHook(hook, hookList, params) {
if (!Array.isArray(hookList) || !hookList.length) { if (!Array.isArray(hookList) || !hookList.length) {
return callback(); return;
} }
async.each(hookList, function (hookObj, next) { await async.each(hookList, function (hookObj, next) {
if (typeof hookObj.method === 'function') { if (typeof hookObj.method === 'function') {
let timedOut = false; let timedOut = false;
const timeoutId = setTimeout(function () { const timeoutId = setTimeout(function () {
@ -201,14 +180,14 @@ module.exports = function (Plugins) {
} else { } else {
next(); next();
} }
}, callback); });
} }
function fireResponseHook(hook, hookList, params, callback) { async function fireResponseHook(hook, hookList, params) {
if (!Array.isArray(hookList) || !hookList.length) { if (!Array.isArray(hookList) || !hookList.length) {
return callback(); return;
} }
async.eachSeries(hookList, function (hookObj, next) { await async.eachSeries(hookList, function (hookObj, next) {
if (typeof hookObj.method !== 'function') { if (typeof hookObj.method !== 'function') {
if (global.env === 'development') { if (global.env === 'development') {
winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); winston.warn('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.');
@ -223,7 +202,7 @@ module.exports = function (Plugins) {
hookObj.method(params); hookObj.method(params);
next(); next();
}, callback); });
} }
Plugins.hasListeners = function (hook) { Plugins.hasListeners = function (hook) {

@ -1,16 +1,19 @@
'use strict'; 'use strict';
var fs = require('fs'); const fs = require('fs');
var path = require('path'); const path = require('path');
var async = require('async'); const async = require('async');
var winston = require('winston'); const winston = require('winston');
var semver = require('semver'); const semver = require('semver');
var nconf = require('nconf'); const nconf = require('nconf');
const util = require('util');
const readdirAsync = util.promisify(fs.readdir);
var app; var app;
var middleware; var middleware;
var Plugins = module.exports; const Plugins = module.exports;
require('./install')(Plugins); require('./install')(Plugins);
require('./load')(Plugins); require('./load')(Plugins);
@ -65,10 +68,9 @@ Plugins.requireLibrary = function (pluginID, libraryPath) {
Plugins.libraryPaths.push(libraryPath); Plugins.libraryPaths.push(libraryPath);
}; };
Plugins.init = function (nbbApp, nbbMiddleware, callback) { Plugins.init = async function (nbbApp, nbbMiddleware) {
callback = callback || function () {};
if (Plugins.initialized) { if (Plugins.initialized) {
return callback(); return;
} }
if (nbbApp) { if (nbbApp) {
@ -80,22 +82,15 @@ Plugins.init = function (nbbApp, nbbMiddleware, callback) {
winston.verbose('[plugins] Initializing plugins system'); winston.verbose('[plugins] Initializing plugins system');
} }
Plugins.reload(function (err) { await Plugins.reload();
if (err) {
winston.error('[plugins] NodeBB encountered a problem while loading plugins', err);
return callback(err);
}
if (global.env === 'development') { if (global.env === 'development') {
winston.info('[plugins] Plugins OK'); winston.info('[plugins] Plugins OK');
} }
Plugins.initialized = true; Plugins.initialized = true;
callback();
});
}; };
Plugins.reload = function (callback) { Plugins.reload = async function () {
// Resetting all local plugin data // Resetting all local plugin data
Plugins.libraries = {}; Plugins.libraries = {};
Plugins.loadedHooks = {}; Plugins.loadedHooks = {};
@ -109,12 +104,12 @@ Plugins.reload = function (callback) {
Plugins.libraryPaths.length = 0; Plugins.libraryPaths.length = 0;
Plugins.loadedPlugins.length = 0; Plugins.loadedPlugins.length = 0;
async.waterfall([ const paths = await Plugins.getPluginPaths();
Plugins.getPluginPaths, for (const path of paths) {
function (paths, next) { /* eslint-disable no-await-in-loop */
async.eachSeries(paths, Plugins.loadPlugin, next); await Plugins.loadPlugin(path);
}, }
function (next) {
// If some plugins are incompatible, throw the warning here // If some plugins are incompatible, throw the warning here
if (Plugins.versionWarning.length && nconf.get('isPrimary') === 'true') { if (Plugins.versionWarning.length && nconf.get('isPrimary') === 'true') {
console.log(''); console.log('');
@ -126,98 +121,75 @@ Plugins.reload = function (callback) {
} }
Object.keys(Plugins.loadedHooks).forEach(function (hook) { Object.keys(Plugins.loadedHooks).forEach(function (hook) {
var hooks = Plugins.loadedHooks[hook]; Plugins.loadedHooks[hook].sort((a, b) => a.priority - b.priority);
hooks.sort(function (a, b) {
return a.priority - b.priority;
}); });
});
next();
},
], callback);
}; };
Plugins.reloadRoutes = function (router, callback) { Plugins.reloadRoutes = async function (router) {
var controllers = require('../controllers'); var controllers = require('../controllers');
Plugins.fireHook('static:app.load', { app: app, router: router, middleware: middleware, controllers: controllers }, function (err) { await Plugins.fireHook('static:app.load', { app: app, router: router, middleware: middleware, controllers: controllers });
if (err) {
winston.error('[plugins] Encountered error while executing post-router plugins hooks', err);
return callback(err);
}
winston.verbose('[plugins] All plugins reloaded and rerouted'); winston.verbose('[plugins] All plugins reloaded and rerouted');
callback();
});
}; };
Plugins.get = function (id, callback) { function request(url, callback) {
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + id;
require('request')(url, { require('request')(url, {
json: true, json: true,
}, function (err, res, body) { }, function (err, res, body) {
if (res.statusCode === 404 || !body.payload) { if (res.statusCode === 404 || !body) {
return callback(err, {}); return callback(err, {});
} }
callback(err, body);
Plugins.normalise([body.payload], function (err, normalised) {
normalised = normalised.filter(function (plugin) {
return plugin.id === id;
});
return callback(err, !err ? normalised[0] : undefined);
});
}); });
}
const requestAsync = util.promisify(request);
Plugins.get = async function (id) {
const url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + id;
const body = await requestAsync(url);
let normalised = await Plugins.normalise([body ? body.payload : {}]);
normalised = normalised.filter(plugin => plugin.id === id);
return normalised.length ? normalised[0] : undefined;
}; };
Plugins.list = function (matching, callback) { Plugins.list = async function (matching) {
if (arguments.length === 1 && typeof matching === 'function') { if (matching === undefined) {
callback = matching;
matching = true; matching = true;
} }
var version = require(path.join(nconf.get('base_dir'), 'package.json')).version; const version = require(path.join(nconf.get('base_dir'), 'package.json')).version;
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins' + (matching !== false ? '?version=' + version : ''); const url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins' + (matching !== false ? '?version=' + version : '');
try {
require('request')(url, { const body = await requestAsync(url);
json: true, return await Plugins.normalise(body);
}, function (err, res, body) { } catch (err) {
if (err || (res && res.statusCode !== 200)) { winston.error('Error loading ' + url, err);
winston.error('Error loading ' + url, err || body); return await Plugins.normalise([]);
return Plugins.normalise([], callback);
} }
Plugins.normalise(body, callback);
});
}; };
Plugins.normalise = function (apiReturn, callback) { Plugins.normalise = async function (apiReturn) {
var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/; const themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/;
var pluginMap = {}; const pluginMap = {};
var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies; const dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies;
apiReturn = Array.isArray(apiReturn) ? apiReturn : []; apiReturn = Array.isArray(apiReturn) ? apiReturn : [];
for (var i = 0; i < apiReturn.length; i += 1) { apiReturn.forEach(function (packageData) {
apiReturn[i].id = apiReturn[i].name; packageData.id = packageData.name;
apiReturn[i].installed = false; packageData.installed = false;
apiReturn[i].active = false; packageData.active = false;
apiReturn[i].url = apiReturn[i].url || (apiReturn[i].repository ? apiReturn[i].repository.url : ''); packageData.url = packageData.url || (packageData.repository ? packageData.repository.url : '');
pluginMap[apiReturn[i].name] = apiReturn[i]; pluginMap[packageData.name] = packageData;
}
Plugins.showInstalled(function (err, installedPlugins) {
if (err) {
return callback(err);
}
installedPlugins = installedPlugins.filter(function (plugin) {
return plugin && !plugin.system;
}); });
async.each(installedPlugins, function (plugin, next) { let installedPlugins = await Plugins.showInstalled();
installedPlugins = installedPlugins.filter(plugin => plugin && !plugin.system);
installedPlugins.forEach(function (plugin) {
// If it errored out because a package.json or plugin.json couldn't be read, no need to do this stuff // If it errored out because a package.json or plugin.json couldn't be read, no need to do this stuff
if (plugin.error) { if (plugin.error) {
pluginMap[plugin.id] = pluginMap[plugin.id] || {}; pluginMap[plugin.id] = pluginMap[plugin.id] || {};
pluginMap[plugin.id].installed = true; pluginMap[plugin.id].installed = true;
pluginMap[plugin.id].error = true; pluginMap[plugin.id].error = true;
return next(); return;
} }
pluginMap[plugin.id] = pluginMap[plugin.id] || {}; pluginMap[plugin.id] = pluginMap[plugin.id] || {};
@ -240,13 +212,9 @@ Plugins.normalise = function (apiReturn, callback) {
pluginMap[plugin.id].latest = pluginMap[plugin.id].latest || plugin.version; pluginMap[plugin.id].latest = pluginMap[plugin.id].latest || plugin.version;
} }
pluginMap[plugin.id].outdated = semver.gt(pluginMap[plugin.id].latest, pluginMap[plugin.id].version); pluginMap[plugin.id].outdated = semver.gt(pluginMap[plugin.id].latest, pluginMap[plugin.id].version);
next(); });
}, function (err) {
if (err) {
return callback(err);
}
var pluginArray = []; const pluginArray = [];
for (var key in pluginMap) { for (var key in pluginMap) {
if (pluginMap.hasOwnProperty(key)) { if (pluginMap.hasOwnProperty(key)) {
@ -263,24 +231,39 @@ Plugins.normalise = function (apiReturn, callback) {
return 0; return 0;
}); });
callback(null, pluginArray); return pluginArray;
});
});
}; };
Plugins.nodeModulesPath = path.join(__dirname, '../../node_modules'); Plugins.nodeModulesPath = path.join(__dirname, '../../node_modules');
Plugins.showInstalled = function (callback) { Plugins.showInstalled = async function () {
var pluginNamePattern = /^(@.*?\/)?nodebb-(theme|plugin|widget|rewards)-.*$/; const dirs = await readdirAsync(Plugins.nodeModulesPath);
async.waterfall([ let pluginPaths = await findNodeBBModules(dirs);
function (next) { pluginPaths = pluginPaths.map(dir => path.join(Plugins.nodeModulesPath, dir));
fs.readdir(Plugins.nodeModulesPath, next);
}, async function load(file) {
function (dirs, next) { try {
var pluginPaths = []; const pluginData = await Plugins.loadPluginInfo(file);
const isActive = await Plugins.isActive(pluginData.name);
delete pluginData.hooks;
delete pluginData.library;
pluginData.active = isActive;
pluginData.installed = true;
pluginData.error = false;
return pluginData;
} catch (err) {
winston.error(err);
}
}
const plugins = await Promise.all(pluginPaths.map(file => load(file)));
return plugins.filter(Boolean);
};
async.each(dirs, function (dirname, next) { async function findNodeBBModules(dirs) {
const pluginNamePattern = /^(@.*?\/)?nodebb-(theme|plugin|widget|rewards)-.*$/;
const pluginPaths = [];
await async.each(dirs, function (dirname, next) {
var dirPath = path.join(Plugins.nodeModulesPath, dirname); var dirPath = path.join(Plugins.nodeModulesPath, dirname);
async.waterfall([ async.waterfall([
@ -326,49 +309,8 @@ Plugins.showInstalled = function (callback) {
}, cb); }, cb);
}, },
], next); ], next);
}, function (err) {
next(err, pluginPaths);
});
},
function (dirs, next) {
dirs = dirs.map(function (dir) {
return path.join(Plugins.nodeModulesPath, dir);
}); });
var plugins = []; return pluginPaths;
}
async.each(dirs, function (file, next) {
async.waterfall([
function (next) {
Plugins.loadPluginInfo(file, next);
},
function (pluginData, next) {
Plugins.isActive(pluginData.name, function (err, active) {
if (err) {
return next(new Error('no-active-state'));
}
delete pluginData.hooks;
delete pluginData.library;
pluginData.active = active;
pluginData.installed = true;
pluginData.error = false;
next(null, pluginData);
});
},
], function (err, pluginData) {
if (err) {
return next(); // Silently fail
}
plugins.push(pluginData);
next();
});
}, function (err) {
next(err, plugins);
});
},
], callback);
};
Plugins.async = require('../promisify')(Plugins); Plugins.async = require('../promisify')(Plugins);

@ -1,21 +1,23 @@
'use strict'; 'use strict';
var winston = require('winston'); const winston = require('winston');
var async = require('async'); const path = require('path');
var path = require('path'); const fs = require('fs');
var fs = require('fs'); const nconf = require('nconf');
var nconf = require('nconf'); const os = require('os');
var os = require('os'); const cproc = require('child_process');
var cproc = require('child_process'); const util = require('util');
var db = require('../database'); const db = require('../database');
var meta = require('../meta'); const meta = require('../meta');
var pubsub = require('../pubsub'); const pubsub = require('../pubsub');
var events = require('../events'); const events = require('../events');
var packageManager = nconf.get('package_manager') === 'yarn' ? 'yarn' : 'npm'; const statAsync = util.promisify(fs.stat);
var packageManagerExecutable = packageManager;
var packageManagerCommands = { const packageManager = nconf.get('package_manager') === 'yarn' ? 'yarn' : 'npm';
let packageManagerExecutable = packageManager;
const packageManagerCommands = {
yarn: { yarn: {
install: 'add', install: 'add',
uninstall: 'remove', uninstall: 'remove',
@ -45,83 +47,43 @@ module.exports = function (Plugins) {
}); });
} }
Plugins.toggleActive = function (id, callback) { Plugins.toggleActive = async function (id) {
callback = callback || function () {}; const isActive = await Plugins.isActive(id);
var isActive;
async.waterfall([
function (next) {
Plugins.isActive(id, next);
},
function (_isActive, next) {
isActive = _isActive;
if (isActive) { if (isActive) {
db.sortedSetRemove('plugins:active', id, next); await db.sortedSetRemove('plugins:active', id);
} else { } else {
db.sortedSetCard('plugins:active', function (err, count) { const count = await db.sortedSetCard('plugins:active');
if (err) { await db.sortedSetAdd('plugins:active', count, id);
return next(err);
}
db.sortedSetAdd('plugins:active', count, id, next);
});
} }
},
function (next) {
meta.reloadRequired = true; meta.reloadRequired = true;
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', { id: id }); Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', { id: id });
setImmediate(next); await events.log({
},
function (next) {
events.log({
type: 'plugin-' + (isActive ? 'deactivate' : 'activate'), type: 'plugin-' + (isActive ? 'deactivate' : 'activate'),
text: id, text: id,
}, next);
},
], function (err) {
if (err) {
winston.warn('[plugins] Could not toggle active state on plugin \'' + id + '\'');
return callback(err);
}
callback(null, { id: id, active: !isActive });
}); });
return { id: id, active: !isActive };
}; };
Plugins.toggleInstall = function (id, version, callback) { Plugins.toggleInstall = async function (id, version) {
pubsub.publish('plugins:toggleInstall', { hostname: os.hostname(), id: id, version: version }); pubsub.publish('plugins:toggleInstall', { hostname: os.hostname(), id: id, version: version });
toggleInstall(id, version, callback); return await toggleInstall(id, version);
}; };
function toggleInstall(id, version, callback) { const runPackageManagerCommandAsync = util.promisify(runPackageManagerCommand);
var installed;
var type; async function toggleInstall(id, version) {
async.waterfall([ const [installed, active] = await Promise.all([
function (next) { Plugins.isInstalled(id),
Plugins.isInstalled(id, next); Plugins.isActive(id),
}, ]);
function (_installed, next) { const type = installed ? 'uninstall' : 'install';
installed = _installed;
type = installed ? 'uninstall' : 'install';
Plugins.isActive(id, next);
},
function (active, next) {
if (active) { if (active) {
Plugins.toggleActive(id, function (err) { await Plugins.toggleActive(id);
next(err);
});
return;
} }
setImmediate(next); await runPackageManagerCommandAsync(type, id, version || 'latest');
}, const pluginData = await Plugins.get(id);
function (next) {
runPackageManagerCommand(type, id, version || 'latest', next);
},
function (next) {
Plugins.get(id, next);
},
function (pluginData, next) {
Plugins.fireHook('action:plugin.' + type, { id: id, version: version }); Plugins.fireHook('action:plugin.' + type, { id: id, version: version });
setImmediate(next, null, pluginData); return pluginData;
},
], callback);
} }
function runPackageManagerCommand(command, pkgName, version, callback) { function runPackageManagerCommand(command, pkgName, version, callback) {
@ -139,37 +101,34 @@ module.exports = function (Plugins) {
}); });
} }
Plugins.upgrade = function (id, version, callback) {
Plugins.upgrade = async function (id, version) {
pubsub.publish('plugins:upgrade', { hostname: os.hostname(), id: id, version: version }); pubsub.publish('plugins:upgrade', { hostname: os.hostname(), id: id, version: version });
upgrade(id, version, callback); return await upgrade(id, version);
}; };
function upgrade(id, version, callback) { async function upgrade(id, version) {
async.waterfall([ await runPackageManagerCommandAsync('install', id, version || 'latest');
async.apply(runPackageManagerCommand, 'install', id, version || 'latest'), const isActive = await Plugins.isActive(id);
function (next) {
Plugins.isActive(id, next);
},
function (isActive, next) {
meta.reloadRequired = isActive; meta.reloadRequired = isActive;
next(null, isActive); return isActive;
},
], callback);
} }
Plugins.isInstalled = function (id, callback) { Plugins.isInstalled = async function (id) {
var pluginDir = path.join(__dirname, '../../node_modules', id); const pluginDir = path.join(__dirname, '../../node_modules', id);
try {
fs.stat(pluginDir, function (err, stats) { const stats = await statAsync(pluginDir);
callback(null, err ? false : stats.isDirectory()); return stats.isDirectory();
}); } catch (err) {
return false;
}
}; };
Plugins.isActive = function (id, callback) { Plugins.isActive = async function (id) {
db.isSortedSetMember('plugins:active', id, callback); return await db.isSortedSetMember('plugins:active', id);
}; };
Plugins.getActive = function (callback) { Plugins.getActive = async function () {
db.getSortedSetRange('plugins:active', 0, -1, callback); return await db.getSortedSetRange('plugins:active', 0, -1);
}; };
}; };

@ -1,21 +1,21 @@
'use strict'; 'use strict';
var path = require('path'); const path = require('path');
var semver = require('semver'); const semver = require('semver');
var async = require('async'); const async = require('async');
var winston = require('winston'); const winston = require('winston');
var nconf = require('nconf'); const nconf = require('nconf');
var _ = require('lodash'); const _ = require('lodash');
var meta = require('../meta'); const meta = require('../meta');
module.exports = function (Plugins) { module.exports = function (Plugins) {
function registerPluginAssets(pluginData, fields, callback) { async function registerPluginAssets(pluginData, fields) {
function add(dest, arr) { function add(dest, arr) {
dest.push.apply(dest, arr || []); dest.push.apply(dest, arr || []);
} }
var handlers = { const handlers = {
staticDirs: function (next) { staticDirs: function (next) {
Plugins.data.getStaticDirectories(pluginData, next); Plugins.data.getStaticDirectories(pluginData, next);
}, },
@ -45,20 +45,16 @@ module.exports = function (Plugins) {
}, },
}; };
var methods; var methods = {};
if (Array.isArray(fields)) { if (Array.isArray(fields)) {
methods = fields.reduce(function (prev, field) { fields.forEach(function (field) {
prev[field] = handlers[field]; methods[field] = handlers[field];
return prev; });
}, {});
} else { } else {
methods = handlers; methods = handlers;
} }
async.parallel(methods, function (err, results) { const results = await async.parallel(methods);
if (err) {
return callback(err);
}
Object.assign(Plugins.staticDirs, results.staticDirs || {}); Object.assign(Plugins.staticDirs, results.staticDirs || {});
add(Plugins.cssFiles, results.cssFiles); add(Plugins.cssFiles, results.cssFiles);
@ -75,13 +71,10 @@ module.exports = function (Plugins) {
Plugins.languageData.namespaces = _.union(Plugins.languageData.namespaces, results.languageData.namespaces); Plugins.languageData.namespaces = _.union(Plugins.languageData.namespaces, results.languageData.namespaces);
} }
Plugins.pluginsData[pluginData.id] = pluginData; Plugins.pluginsData[pluginData.id] = pluginData;
callback();
});
} }
Plugins.prepareForBuild = function (targets, callback) { Plugins.prepareForBuild = async function (targets) {
var map = { const map = {
'plugin static dirs': ['staticDirs'], 'plugin static dirs': ['staticDirs'],
'requirejs modules': ['modules'], 'requirejs modules': ['modules'],
'client js bundle': ['clientScripts'], 'client js bundle': ['clientScripts'],
@ -92,7 +85,7 @@ module.exports = function (Plugins) {
languages: ['languageData'], languages: ['languageData'],
}; };
var fields = _.uniq(_.flatMap(targets, target => map[target] || [])); const fields = _.uniq(_.flatMap(targets, target => map[target] || []));
// clear old data before build // clear old data before build
fields.forEach((field) => { fields.forEach((field) => {
@ -116,43 +109,34 @@ module.exports = function (Plugins) {
}); });
winston.verbose('[plugins] loading the following fields from plugin data: ' + fields.join(', ')); winston.verbose('[plugins] loading the following fields from plugin data: ' + fields.join(', '));
const plugins = await Plugins.data.getActive();
async.waterfall([ await Promise.all(plugins.map(p => registerPluginAssets(p, fields)));
Plugins.data.getActive,
function (plugins, next) {
async.each(plugins, function (pluginData, next) {
registerPluginAssets(pluginData, fields, next);
}, next);
},
], callback);
}; };
var themeNamePattern = /(@.*?\/)?nodebb-theme-.*$/; const themeNamePattern = /(@.*?\/)?nodebb-theme-.*$/;
Plugins.loadPlugin = function (pluginPath, callback) { Plugins.loadPlugin = async function (pluginPath) {
Plugins.data.loadPluginInfo(pluginPath, function (err, pluginData) { let pluginData;
if (err) { try {
pluginData = await Plugins.data.loadPluginInfo(pluginPath);
} catch (err) {
if (err.message === '[[error:parse-error]]') { if (err.message === '[[error:parse-error]]') {
return callback(); return;
} }
if (!themeNamePattern.test(pluginPath)) {
return callback(themeNamePattern.test(pluginPath) ? null : err); throw err;
}
return;
} }
checkVersion(pluginData); checkVersion(pluginData);
async.parallel([ try {
function (next) { registerHooks(pluginData);
registerHooks(pluginData, next); await registerPluginAssets(pluginData, ['soundpack']);
}, } catch (err) {
function (next) {
registerPluginAssets(pluginData, ['soundpack'], next);
},
], function (err) {
if (err) {
winston.error(err.stack); winston.error(err.stack);
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id); winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
return callback(); return;
} }
if (!pluginData.private) { if (!pluginData.private) {
@ -163,9 +147,6 @@ module.exports = function (Plugins) {
} }
winston.verbose('[plugins] Loaded plugin: ' + pluginData.id); winston.verbose('[plugins] Loaded plugin: ' + pluginData.id);
callback();
});
});
}; };
function checkVersion(pluginData) { function checkVersion(pluginData) {
@ -184,28 +165,24 @@ module.exports = function (Plugins) {
} }
} }
function registerHooks(pluginData, callback) { function registerHooks(pluginData) {
if (!pluginData.library) { if (!pluginData.library) {
return callback(); return;
} }
var libraryPath = path.join(pluginData.path, pluginData.library); const libraryPath = path.join(pluginData.path, pluginData.library);
try { try {
if (!Plugins.libraries[pluginData.id]) { if (!Plugins.libraries[pluginData.id]) {
Plugins.requireLibrary(pluginData.id, libraryPath); Plugins.requireLibrary(pluginData.id, libraryPath);
} }
if (Array.isArray(pluginData.hooks) && pluginData.hooks.length > 0) { if (Array.isArray(pluginData.hooks)) {
async.each(pluginData.hooks, function (hook, next) { pluginData.hooks.forEach(hook => Plugins.registerHook(pluginData.id, hook));
Plugins.registerHook(pluginData.id, hook, next);
}, callback);
} else {
callback();
} }
} catch (err) { } catch (err) {
winston.warn('[plugins] Unable to parse library for: ' + pluginData.id); winston.warn('[plugins] Unable to parse library for: ' + pluginData.id);
callback(err); throw err;
} }
} }
}; };

Loading…
Cancel
Save