feat: #7743 plugins

This commit is contained in:
Barış Soner Uşaklı
2019-07-22 00:30:47 -04:00
parent f5f5f76b12
commit c126cd8572
6 changed files with 400 additions and 541 deletions

View File

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

View File

@@ -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;
}; };

View File

@@ -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,37 +43,34 @@ 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; }
}
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];
} }
// Couldn't find method by path, aborting // Couldn't find method by path, aborting
return null; return null;
}, Plugins.libraries[data.id]); }, Plugins.libraries[data.id]);
// 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') {
Plugins.fireHook('action:plugins.firehook', { hook: hook, params: params });
}
if (result !== undefined) {
callback(null, result);
} else {
callback();
}
}
var hookList = Plugins.loadedHooks[hook];
var hookType = hook.split(':')[0];
if (hook !== 'action:plugins.firehook') { if (hook !== 'action:plugins.firehook') {
winston.verbose('[plugins/fireHook] ' + hook); winston.verbose('[plugins/fireHook] ' + hook);
} }
switch (hookType) {
case 'filter': if (!hookTypeToMethod[hookType]) {
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); winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
callback(); return;
break; }
const result = await hookTypeToMethod[hookType](hook, hookList, params);
if (hook !== 'action:plugins.firehook') {
Plugins.fireHook('action:plugins.firehook', { hook: hook, params: params });
}
if (result !== undefined) {
return result;
} }
}; };
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) {

View File

@@ -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) { if (global.env === 'development') {
winston.error('[plugins] NodeBB encountered a problem while loading plugins', err); winston.info('[plugins] Plugins OK');
return callback(err); }
}
if (global.env === 'development') { Plugins.initialized = true;
winston.info('[plugins] Plugins OK');
}
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,266 +104,213 @@ 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 (Plugins.versionWarning.length && nconf.get('isPrimary') === 'true') {
console.log('');
winston.warn('[plugins/load] The following plugins may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing. In the event of an unresponsive NodeBB caused by this plugin, run `./nodebb reset -p PLUGINNAME` to disable it.');
for (var x = 0, numPlugins = Plugins.versionWarning.length; x < numPlugins; x += 1) {
console.log(' * '.yellow + Plugins.versionWarning[x]);
}
console.log('');
}
Object.keys(Plugins.loadedHooks).forEach(function (hook) { // If some plugins are incompatible, throw the warning here
var hooks = Plugins.loadedHooks[hook]; if (Plugins.versionWarning.length && nconf.get('isPrimary') === 'true') {
hooks.sort(function (a, b) { console.log('');
return a.priority - b.priority; winston.warn('[plugins/load] The following plugins may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing. In the event of an unresponsive NodeBB caused by this plugin, run `./nodebb reset -p PLUGINNAME` to disable it.');
}); for (var x = 0, numPlugins = Plugins.versionWarning.length; x < numPlugins; x += 1) {
}); console.log(' * '.yellow + Plugins.versionWarning[x]);
next();
},
], callback);
};
Plugins.reloadRoutes = function (router, callback) {
var controllers = require('../controllers');
Plugins.fireHook('static:app.load', { app: app, router: router, middleware: middleware, controllers: controllers }, function (err) {
if (err) {
winston.error('[plugins] Encountered error while executing post-router plugins hooks', err);
return callback(err);
} }
console.log('');
}
winston.verbose('[plugins] All plugins reloaded and rerouted'); Object.keys(Plugins.loadedHooks).forEach(function (hook) {
callback(); Plugins.loadedHooks[hook].sort((a, b) => a.priority - b.priority);
}); });
}; };
Plugins.get = function (id, callback) { Plugins.reloadRoutes = async function (router) {
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + id; var controllers = require('../controllers');
await Plugins.fireHook('static:app.load', { app: app, router: router, middleware: middleware, controllers: controllers });
winston.verbose('[plugins] All plugins reloaded and rerouted');
};
function request(url, callback) {
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) { let installedPlugins = await Plugins.showInstalled();
if (err) { installedPlugins = installedPlugins.filter(plugin => plugin && !plugin.system);
return callback(err);
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 (plugin.error) {
pluginMap[plugin.id] = pluginMap[plugin.id] || {};
pluginMap[plugin.id].installed = true;
pluginMap[plugin.id].error = true;
return;
} }
installedPlugins = installedPlugins.filter(function (plugin) { pluginMap[plugin.id] = pluginMap[plugin.id] || {};
return plugin && !plugin.system; pluginMap[plugin.id].id = pluginMap[plugin.id].id || plugin.id;
}); pluginMap[plugin.id].name = plugin.name || pluginMap[plugin.id].name;
pluginMap[plugin.id].description = plugin.description;
pluginMap[plugin.id].url = pluginMap[plugin.id].url || plugin.url;
pluginMap[plugin.id].installed = true;
pluginMap[plugin.id].isTheme = themeNamePattern.test(plugin.id);
pluginMap[plugin.id].error = plugin.error || false;
pluginMap[plugin.id].active = plugin.active;
pluginMap[plugin.id].version = plugin.version;
pluginMap[plugin.id].settingsRoute = plugin.settingsRoute;
pluginMap[plugin.id].license = plugin.license;
async.each(installedPlugins, function (plugin, next) { // If package.json defines a version to use, stick to that
// If it errored out because a package.json or plugin.json couldn't be read, no need to do this stuff if (dependencies.hasOwnProperty(plugin.id) && semver.valid(dependencies[plugin.id])) {
if (plugin.error) { pluginMap[plugin.id].latest = dependencies[plugin.id];
pluginMap[plugin.id] = pluginMap[plugin.id] || {}; } else {
pluginMap[plugin.id].installed = true; pluginMap[plugin.id].latest = pluginMap[plugin.id].latest || plugin.version;
pluginMap[plugin.id].error = true; }
return next(); pluginMap[plugin.id].outdated = semver.gt(pluginMap[plugin.id].latest, pluginMap[plugin.id].version);
}
pluginMap[plugin.id] = pluginMap[plugin.id] || {};
pluginMap[plugin.id].id = pluginMap[plugin.id].id || plugin.id;
pluginMap[plugin.id].name = plugin.name || pluginMap[plugin.id].name;
pluginMap[plugin.id].description = plugin.description;
pluginMap[plugin.id].url = pluginMap[plugin.id].url || plugin.url;
pluginMap[plugin.id].installed = true;
pluginMap[plugin.id].isTheme = themeNamePattern.test(plugin.id);
pluginMap[plugin.id].error = plugin.error || false;
pluginMap[plugin.id].active = plugin.active;
pluginMap[plugin.id].version = plugin.version;
pluginMap[plugin.id].settingsRoute = plugin.settingsRoute;
pluginMap[plugin.id].license = plugin.license;
// If package.json defines a version to use, stick to that
if (dependencies.hasOwnProperty(plugin.id) && semver.valid(dependencies[plugin.id])) {
pluginMap[plugin.id].latest = dependencies[plugin.id];
} else {
pluginMap[plugin.id].latest = pluginMap[plugin.id].latest || plugin.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 = [];
for (var key in pluginMap) {
if (pluginMap.hasOwnProperty(key)) {
pluginArray.push(pluginMap[key]);
}
}
pluginArray.sort(function (a, b) {
if (a.name > b.name) {
return 1;
} else if (a.name < b.name) {
return -1;
}
return 0;
});
callback(null, pluginArray);
});
}); });
const pluginArray = [];
for (var key in pluginMap) {
if (pluginMap.hasOwnProperty(key)) {
pluginArray.push(pluginMap[key]);
}
}
pluginArray.sort(function (a, b) {
if (a.name > b.name) {
return 1;
} else if (a.name < b.name) {
return -1;
}
return 0;
});
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);
},
function (dirs, next) {
var pluginPaths = [];
async.each(dirs, function (dirname, next) { async function load(file) {
var dirPath = path.join(Plugins.nodeModulesPath, dirname); try {
const pluginData = await Plugins.loadPluginInfo(file);
async.waterfall([ const isActive = await Plugins.isActive(pluginData.name);
function (cb) { delete pluginData.hooks;
fs.stat(dirPath, function (err, stats) { delete pluginData.library;
if (err && err.code !== 'ENOENT') { pluginData.active = isActive;
return cb(err); pluginData.installed = true;
} pluginData.error = false;
if (err || !stats.isDirectory()) { return pluginData;
return next(); } catch (err) {
} winston.error(err);
}
if (pluginNamePattern.test(dirname)) { }
pluginPaths.push(dirname); const plugins = await Promise.all(pluginPaths.map(file => load(file)));
return next(); return plugins.filter(Boolean);
}
if (dirname[0] !== '@') {
return next();
}
fs.readdir(dirPath, cb);
});
},
function (subdirs, cb) {
async.each(subdirs, function (subdir, next) {
if (!pluginNamePattern.test(subdir)) {
return next();
}
var subdirPath = path.join(dirPath, subdir);
fs.stat(subdirPath, function (err, stats) {
if (err && err.code !== 'ENOENT') {
return next(err);
}
if (err || !stats.isDirectory()) {
return next();
}
pluginPaths.push(dirname + '/' + subdir);
next();
});
}, cb);
},
], next);
}, function (err) {
next(err, pluginPaths);
});
},
function (dirs, next) {
dirs = dirs.map(function (dir) {
return path.join(Plugins.nodeModulesPath, dir);
});
var plugins = [];
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);
}; };
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);
async.waterfall([
function (cb) {
fs.stat(dirPath, function (err, stats) {
if (err && err.code !== 'ENOENT') {
return cb(err);
}
if (err || !stats.isDirectory()) {
return next();
}
if (pluginNamePattern.test(dirname)) {
pluginPaths.push(dirname);
return next();
}
if (dirname[0] !== '@') {
return next();
}
fs.readdir(dirPath, cb);
});
},
function (subdirs, cb) {
async.each(subdirs, function (subdir, next) {
if (!pluginNamePattern.test(subdir)) {
return next();
}
var subdirPath = path.join(dirPath, subdir);
fs.stat(subdirPath, function (err, stats) {
if (err && err.code !== 'ENOENT') {
return next(err);
}
if (err || !stats.isDirectory()) {
return next();
}
pluginPaths.push(dirname + '/' + subdir);
next();
});
}, cb);
},
], next);
});
return pluginPaths;
}
Plugins.async = require('../promisify')(Plugins); Plugins.async = require('../promisify')(Plugins);

View File

@@ -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; if (isActive) {
async.waterfall([ await db.sortedSetRemove('plugins:active', id);
function (next) { } else {
Plugins.isActive(id, next); const count = await db.sortedSetCard('plugins:active');
}, await db.sortedSetAdd('plugins:active', count, id);
function (_isActive, next) { }
isActive = _isActive; meta.reloadRequired = true;
if (isActive) { Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', { id: id });
db.sortedSetRemove('plugins:active', id, next); await events.log({
} else { type: 'plugin-' + (isActive ? 'deactivate' : 'activate'),
db.sortedSetCard('plugins:active', function (err, count) { text: id,
if (err) {
return next(err);
}
db.sortedSetAdd('plugins:active', count, id, next);
});
}
},
function (next) {
meta.reloadRequired = true;
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', { id: id });
setImmediate(next);
},
function (next) {
events.log({
type: 'plugin-' + (isActive ? 'deactivate' : 'activate'),
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; if (active) {
type = installed ? 'uninstall' : 'install'; await Plugins.toggleActive(id);
Plugins.isActive(id, next); }
}, await runPackageManagerCommandAsync(type, id, version || 'latest');
function (active, next) { const pluginData = await Plugins.get(id);
if (active) { Plugins.fireHook('action:plugin.' + type, { id: id, version: version });
Plugins.toggleActive(id, function (err) { return pluginData;
next(err);
});
return;
}
setImmediate(next);
},
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 });
setImmediate(next, null, 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) { meta.reloadRequired = isActive;
Plugins.isActive(id, next); return isActive;
},
function (isActive, next) {
meta.reloadRequired = isActive;
next(null, 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);
}; };
}; };

View File

@@ -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,43 +45,36 @@ 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);
add(Plugins.lessFiles, results.lessFiles); add(Plugins.lessFiles, results.lessFiles);
add(Plugins.acpLessFiles, results.acpLessFiles); add(Plugins.acpLessFiles, results.acpLessFiles);
add(Plugins.clientScripts, results.clientScripts); add(Plugins.clientScripts, results.clientScripts);
add(Plugins.acpScripts, results.acpScripts); add(Plugins.acpScripts, results.acpScripts);
Object.assign(meta.js.scripts.modules, results.modules || {}); Object.assign(meta.js.scripts.modules, results.modules || {});
if (results.soundpack) { if (results.soundpack) {
Plugins.soundpacks.push(results.soundpack); Plugins.soundpacks.push(results.soundpack);
} }
if (results.languageData) { if (results.languageData) {
Plugins.languageData.languages = _.union(Plugins.languageData.languages, results.languageData.languages); Plugins.languageData.languages = _.union(Plugins.languageData.languages, results.languageData.languages);
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,56 +109,44 @@ 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 {
if (err.message === '[[error:parse-error]]') { pluginData = await Plugins.data.loadPluginInfo(pluginPath);
return callback(); } catch (err) {
} if (err.message === '[[error:parse-error]]') {
return;
return callback(themeNamePattern.test(pluginPath) ? null : err);
} }
if (!themeNamePattern.test(pluginPath)) {
throw err;
}
return;
}
checkVersion(pluginData);
checkVersion(pluginData); try {
registerHooks(pluginData);
await registerPluginAssets(pluginData, ['soundpack']);
} catch (err) {
winston.error(err.stack);
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
return;
}
async.parallel([ if (!pluginData.private) {
function (next) { Plugins.loadedPlugins.push({
registerHooks(pluginData, next); id: pluginData.id,
}, version: pluginData.version,
function (next) {
registerPluginAssets(pluginData, ['soundpack'], next);
},
], function (err) {
if (err) {
winston.error(err.stack);
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
return callback();
}
if (!pluginData.private) {
Plugins.loadedPlugins.push({
id: pluginData.id,
version: pluginData.version,
});
}
winston.verbose('[plugins] Loaded plugin: ' + pluginData.id);
callback();
}); });
}); }
winston.verbose('[plugins] Loaded plugin: ' + pluginData.id);
}; };
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;
} }
} }
}; };