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);
});
};
require('./promisify')(events);

@ -216,7 +216,7 @@ Data.getModules = async function getModules(pluginData) {
await Promise.all(Object.keys(pluginModules).map(key => processModule(key)));
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;
};

@ -10,30 +10,31 @@ module.exports = function (Plugins) {
};
Plugins.internals = {
_register: function (data, callback) {
_register: function (data) {
Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || [];
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.hook`*, the name of the NodeBB hook
`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)
*/
Plugins.registerHook = function (id, data, callback) {
callback = callback || function () {};
if (!data.hook) {
winston.warn('[plugins/' + id + '] registerHook called with invalid data.hook', data);
return callback();
Plugins.registerHook = function (id, data) {
if (!data.hook || !data.method) {
winston.warn('[plugins/' + id + '] registerHook called with invalid data.hook/method', data);
return;
}
var method;
if (Plugins.deprecatedHooks[data.hook]) {
winston.warn('[plugins/' + id + '] Hook `' + data.hook + '` is deprecated, ' +
(Plugins.deprecatedHooks[data.hook] ?
@ -42,37 +43,34 @@ module.exports = function (Plugins) {
));
}
if (data.hook && data.method) {
data.id = id;
if (!data.priority) {
data.priority = 10;
}
if (Array.isArray(data.method) && data.method.every(method => typeof method === 'function' || typeof method === 'string')) {
// Go go gadget recursion!
async.eachSeries(data.method, function (method, next) {
const singularData = Object.assign({}, data, { method: method });
Plugins.registerHook(id, singularData, next);
}, callback);
} else if (typeof data.method === 'string' && data.method.length > 0) {
method = data.method.split('.').reduce(function (memo, prop) {
if (memo && memo[prop]) {
return memo[prop];
}
// Couldn't find method by path, aborting
return null;
}, Plugins.libraries[data.id]);
// Write the actual method reference to the hookObj
data.method = method;
data.id = id;
if (!data.priority) {
data.priority = 10;
}
Plugins.internals._register(data, callback);
} else if (typeof data.method === 'function') {
Plugins.internals._register(data, callback);
} else {
winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method);
return callback();
}
if (Array.isArray(data.method) && data.method.every(method => typeof method === 'function' || typeof method === 'string')) {
// Go go gadget recursion!
data.method.forEach(function (method) {
const singularData = Object.assign({}, data, { method: method });
Plugins.registerHook(id, singularData);
});
} else if (typeof data.method === 'string' && data.method.length > 0) {
const method = data.method.split('.').reduce(function (memo, prop) {
if (memo && memo[prop]) {
return memo[prop];
}
// Couldn't find method by path, aborting
return null;
}, Plugins.libraries[data.id]);
// Write the actual method reference to the hookObj
data.method = method;
Plugins.internals._register(data);
} else if (typeof data.method === 'function') {
Plugins.internals._register(data);
} else {
winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method);
}
};
@ -83,52 +81,33 @@ module.exports = function (Plugins) {
});
};
Plugins.fireHook = function (hook, params, callback) {
callback = typeof callback === 'function' ? callback : function () {};
function done(err, result) {
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];
Plugins.fireHook = async function (hook, params) {
const hookList = Plugins.loadedHooks[hook];
const hookType = hook.split(':')[0];
if (hook !== 'action:plugins.firehook') {
winston.verbose('[plugins/fireHook] ' + hook);
}
switch (hookType) {
case 'filter':
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:
if (!hookTypeToMethod[hookType]) {
winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
callback();
break;
return;
}
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) {
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 (global.env === 'development') {
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)
);
}
}, callback);
});
}
function fireActionHook(hook, hookList, params, callback) {
async function fireActionHook(hook, hookList, params) {
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 (global.env === 'development') {
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);
next();
}, callback);
});
}
function fireStaticHook(hook, hookList, params, callback) {
async function fireStaticHook(hook, hookList, params) {
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') {
let timedOut = false;
const timeoutId = setTimeout(function () {
@ -201,14 +180,14 @@ module.exports = function (Plugins) {
} else {
next();
}
}, callback);
});
}
function fireResponseHook(hook, hookList, params, callback) {
async function fireResponseHook(hook, hookList, params) {
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 (global.env === 'development') {
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);
next();
}, callback);
});
}
Plugins.hasListeners = function (hook) {

@ -1,16 +1,19 @@
'use strict';
var fs = require('fs');
var path = require('path');
var async = require('async');
var winston = require('winston');
var semver = require('semver');
var nconf = require('nconf');
const fs = require('fs');
const path = require('path');
const async = require('async');
const winston = require('winston');
const semver = require('semver');
const nconf = require('nconf');
const util = require('util');
const readdirAsync = util.promisify(fs.readdir);
var app;
var middleware;
var Plugins = module.exports;
const Plugins = module.exports;
require('./install')(Plugins);
require('./load')(Plugins);
@ -65,10 +68,9 @@ Plugins.requireLibrary = function (pluginID, libraryPath) {
Plugins.libraryPaths.push(libraryPath);
};
Plugins.init = function (nbbApp, nbbMiddleware, callback) {
callback = callback || function () {};
Plugins.init = async function (nbbApp, nbbMiddleware) {
if (Plugins.initialized) {
return callback();
return;
}
if (nbbApp) {
@ -80,22 +82,15 @@ Plugins.init = function (nbbApp, nbbMiddleware, callback) {
winston.verbose('[plugins] Initializing plugins system');
}
Plugins.reload(function (err) {
if (err) {
winston.error('[plugins] NodeBB encountered a problem while loading plugins', err);
return callback(err);
}
if (global.env === 'development') {
winston.info('[plugins] Plugins OK');
}
await Plugins.reload();
if (global.env === 'development') {
winston.info('[plugins] Plugins OK');
}
Plugins.initialized = true;
callback();
});
Plugins.initialized = true;
};
Plugins.reload = function (callback) {
Plugins.reload = async function () {
// Resetting all local plugin data
Plugins.libraries = {};
Plugins.loadedHooks = {};
@ -109,266 +104,213 @@ Plugins.reload = function (callback) {
Plugins.libraryPaths.length = 0;
Plugins.loadedPlugins.length = 0;
async.waterfall([
Plugins.getPluginPaths,
function (paths, next) {
async.eachSeries(paths, Plugins.loadPlugin, next);
},
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) {
var hooks = Plugins.loadedHooks[hook];
hooks.sort(function (a, b) {
return a.priority - b.priority;
});
});
next();
},
], callback);
};
const paths = await Plugins.getPluginPaths();
for (const path of paths) {
/* eslint-disable no-await-in-loop */
await Plugins.loadPlugin(path);
}
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);
// 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('');
}
winston.verbose('[plugins] All plugins reloaded and rerouted');
callback();
Object.keys(Plugins.loadedHooks).forEach(function (hook) {
Plugins.loadedHooks[hook].sort((a, b) => a.priority - b.priority);
});
};
Plugins.get = function (id, callback) {
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + id;
Plugins.reloadRoutes = async function (router) {
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, {
json: true,
}, function (err, res, body) {
if (res.statusCode === 404 || !body.payload) {
if (res.statusCode === 404 || !body) {
return callback(err, {});
}
Plugins.normalise([body.payload], function (err, normalised) {
normalised = normalised.filter(function (plugin) {
return plugin.id === id;
});
return callback(err, !err ? normalised[0] : undefined);
});
callback(err, body);
});
}
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) {
if (arguments.length === 1 && typeof matching === 'function') {
callback = matching;
Plugins.list = async function (matching) {
if (matching === undefined) {
matching = true;
}
var 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 : '');
require('request')(url, {
json: true,
}, function (err, res, body) {
if (err || (res && res.statusCode !== 200)) {
winston.error('Error loading ' + url, err || body);
return Plugins.normalise([], callback);
}
Plugins.normalise(body, callback);
});
const version = require(path.join(nconf.get('base_dir'), 'package.json')).version;
const url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins' + (matching !== false ? '?version=' + version : '');
try {
const body = await requestAsync(url);
return await Plugins.normalise(body);
} catch (err) {
winston.error('Error loading ' + url, err);
return await Plugins.normalise([]);
}
};
Plugins.normalise = function (apiReturn, callback) {
var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/;
var pluginMap = {};
var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies;
Plugins.normalise = async function (apiReturn) {
const themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/;
const pluginMap = {};
const dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies;
apiReturn = Array.isArray(apiReturn) ? apiReturn : [];
for (var i = 0; i < apiReturn.length; i += 1) {
apiReturn[i].id = apiReturn[i].name;
apiReturn[i].installed = false;
apiReturn[i].active = false;
apiReturn[i].url = apiReturn[i].url || (apiReturn[i].repository ? apiReturn[i].repository.url : '');
pluginMap[apiReturn[i].name] = apiReturn[i];
}
Plugins.showInstalled(function (err, installedPlugins) {
if (err) {
return callback(err);
}
apiReturn.forEach(function (packageData) {
packageData.id = packageData.name;
packageData.installed = false;
packageData.active = false;
packageData.url = packageData.url || (packageData.repository ? packageData.repository.url : '');
pluginMap[packageData.name] = packageData;
});
installedPlugins = installedPlugins.filter(function (plugin) {
return plugin && !plugin.system;
});
async.each(installedPlugins, function (plugin, next) {
// 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 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 (plugin.error) {
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);
}
pluginMap[plugin.id].error = true;
return;
}
var pluginArray = [];
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);
});
for (var key in pluginMap) {
if (pluginMap.hasOwnProperty(key)) {
pluginArray.push(pluginMap[key]);
}
}
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;
});
callback(null, pluginArray);
});
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.showInstalled = function (callback) {
var pluginNamePattern = /^(@.*?\/)?nodebb-(theme|plugin|widget|rewards)-.*$/;
async.waterfall([
function (next) {
fs.readdir(Plugins.nodeModulesPath, next);
},
function (dirs, next) {
var pluginPaths = [];
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);
}, 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.showInstalled = async function () {
const dirs = await readdirAsync(Plugins.nodeModulesPath);
let pluginPaths = await findNodeBBModules(dirs);
pluginPaths = pluginPaths.map(dir => path.join(Plugins.nodeModulesPath, dir));
async function load(file) {
try {
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 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();
}
plugins.push(pluginData);
next();
if (dirname[0] !== '@') {
return next();
}
fs.readdir(dirPath, cb);
});
}, function (err) {
next(err, plugins);
});
},
], callback);
};
},
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);

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

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

Loading…
Cancel
Save