mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: #7743 plugins
This commit is contained in:
@@ -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,7 +43,6 @@ module.exports = function (Plugins) {
|
||||
));
|
||||
}
|
||||
|
||||
if (data.hook && data.method) {
|
||||
data.id = id;
|
||||
if (!data.priority) {
|
||||
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')) {
|
||||
// Go go gadget recursion!
|
||||
async.eachSeries(data.method, function (method, next) {
|
||||
data.method.forEach(function (method) {
|
||||
const singularData = Object.assign({}, data, { method: method });
|
||||
Plugins.registerHook(id, singularData, next);
|
||||
}, callback);
|
||||
Plugins.registerHook(id, singularData);
|
||||
});
|
||||
} 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]) {
|
||||
return memo[prop];
|
||||
}
|
||||
@@ -66,13 +66,11 @@ module.exports = function (Plugins) {
|
||||
// Write the actual method reference to the hookObj
|
||||
data.method = method;
|
||||
|
||||
Plugins.internals._register(data, callback);
|
||||
Plugins.internals._register(data);
|
||||
} else if (typeof data.method === 'function') {
|
||||
Plugins.internals._register(data, callback);
|
||||
Plugins.internals._register(data);
|
||||
} else {
|
||||
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) {
|
||||
callback = typeof callback === 'function' ? callback : function () {};
|
||||
function done(err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
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);
|
||||
}
|
||||
|
||||
if (!hookTypeToMethod[hookType]) {
|
||||
winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
|
||||
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) {
|
||||
callback(null, result);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
var hookList = Plugins.loadedHooks[hook];
|
||||
var 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:
|
||||
winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
|
||||
callback();
|
||||
break;
|
||||
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);
|
||||
}
|
||||
|
||||
await Plugins.reload();
|
||||
if (global.env === 'development') {
|
||||
winston.info('[plugins] Plugins OK');
|
||||
}
|
||||
|
||||
Plugins.initialized = true;
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Plugins.reload = function (callback) {
|
||||
Plugins.reload = async function () {
|
||||
// Resetting all local plugin data
|
||||
Plugins.libraries = {};
|
||||
Plugins.loadedHooks = {};
|
||||
@@ -109,12 +104,12 @@ 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) {
|
||||
const paths = await Plugins.getPluginPaths();
|
||||
for (const path of paths) {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
await Plugins.loadPlugin(path);
|
||||
}
|
||||
|
||||
// If some plugins are incompatible, throw the warning here
|
||||
if (Plugins.versionWarning.length && nconf.get('isPrimary') === 'true') {
|
||||
console.log('');
|
||||
@@ -126,98 +121,75 @@ Plugins.reload = function (callback) {
|
||||
}
|
||||
|
||||
Object.keys(Plugins.loadedHooks).forEach(function (hook) {
|
||||
var hooks = Plugins.loadedHooks[hook];
|
||||
hooks.sort(function (a, b) {
|
||||
return a.priority - b.priority;
|
||||
Plugins.loadedHooks[hook].sort((a, b) => a.priority - b.priority);
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Plugins.reloadRoutes = function (router, callback) {
|
||||
Plugins.reloadRoutes = async function (router) {
|
||||
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);
|
||||
}
|
||||
|
||||
await Plugins.fireHook('static:app.load', { app: app, router: router, middleware: middleware, controllers: controllers });
|
||||
winston.verbose('[plugins] All plugins reloaded and rerouted');
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Plugins.get = function (id, callback) {
|
||||
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + id;
|
||||
|
||||
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, {});
|
||||
}
|
||||
callback(err, body);
|
||||
});
|
||||
}
|
||||
const requestAsync = util.promisify(request);
|
||||
|
||||
Plugins.normalise([body.payload], function (err, normalised) {
|
||||
normalised = normalised.filter(function (plugin) {
|
||||
return plugin.id === id;
|
||||
});
|
||||
return callback(err, !err ? normalised[0] : undefined);
|
||||
});
|
||||
});
|
||||
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);
|
||||
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(body, callback);
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
installedPlugins = installedPlugins.filter(function (plugin) {
|
||||
return plugin && !plugin.system;
|
||||
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;
|
||||
});
|
||||
|
||||
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 (plugin.error) {
|
||||
pluginMap[plugin.id] = pluginMap[plugin.id] || {};
|
||||
pluginMap[plugin.id].installed = true;
|
||||
pluginMap[plugin.id].error = true;
|
||||
return next();
|
||||
return;
|
||||
}
|
||||
|
||||
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].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) {
|
||||
if (pluginMap.hasOwnProperty(key)) {
|
||||
@@ -263,24 +231,39 @@ Plugins.normalise = function (apiReturn, callback) {
|
||||
return 0;
|
||||
});
|
||||
|
||||
callback(null, pluginArray);
|
||||
});
|
||||
});
|
||||
return pluginArray;
|
||||
};
|
||||
|
||||
Plugins.nodeModulesPath = path.join(__dirname, '../../node_modules');
|
||||
|
||||
Plugins.showInstalled = function (callback) {
|
||||
var pluginNamePattern = /^(@.*?\/)?nodebb-(theme|plugin|widget|rewards)-.*$/;
|
||||
Plugins.showInstalled = async function () {
|
||||
const dirs = await readdirAsync(Plugins.nodeModulesPath);
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
fs.readdir(Plugins.nodeModulesPath, next);
|
||||
},
|
||||
function (dirs, next) {
|
||||
var pluginPaths = [];
|
||||
let pluginPaths = await findNodeBBModules(dirs);
|
||||
pluginPaths = pluginPaths.map(dir => path.join(Plugins.nodeModulesPath, dir));
|
||||
|
||||
async.each(dirs, function (dirname, next) {
|
||||
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([
|
||||
@@ -326,49 +309,8 @@ Plugins.showInstalled = function (callback) {
|
||||
}, 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);
|
||||
};
|
||||
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');
|
||||
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');
|
||||
|
||||
var db = require('../database');
|
||||
var meta = require('../meta');
|
||||
var pubsub = require('../pubsub');
|
||||
var events = require('../events');
|
||||
const db = require('../database');
|
||||
const meta = require('../meta');
|
||||
const pubsub = require('../pubsub');
|
||||
const events = require('../events');
|
||||
|
||||
var packageManager = nconf.get('package_manager') === 'yarn' ? 'yarn' : 'npm';
|
||||
var packageManagerExecutable = packageManager;
|
||||
var packageManagerCommands = {
|
||||
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;
|
||||
Plugins.toggleActive = async function (id) {
|
||||
const isActive = await Plugins.isActive(id);
|
||||
if (isActive) {
|
||||
db.sortedSetRemove('plugins:active', id, next);
|
||||
await db.sortedSetRemove('plugins:active', id);
|
||||
} else {
|
||||
db.sortedSetCard('plugins:active', function (err, count) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
const count = await db.sortedSetCard('plugins:active');
|
||||
await db.sortedSetAdd('plugins:active', count, id);
|
||||
}
|
||||
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({
|
||||
await 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 });
|
||||
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) {
|
||||
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) {
|
||||
Plugins.toggleActive(id, function (err) {
|
||||
next(err);
|
||||
});
|
||||
return;
|
||||
await Plugins.toggleActive(id);
|
||||
}
|
||||
setImmediate(next);
|
||||
},
|
||||
function (next) {
|
||||
runPackageManagerCommand(type, id, version || 'latest', next);
|
||||
},
|
||||
function (next) {
|
||||
Plugins.get(id, next);
|
||||
},
|
||||
function (pluginData, next) {
|
||||
await runPackageManagerCommandAsync(type, id, version || 'latest');
|
||||
const pluginData = await Plugins.get(id);
|
||||
Plugins.fireHook('action:plugin.' + type, { id: id, version: version });
|
||||
setImmediate(next, null, pluginData);
|
||||
},
|
||||
], callback);
|
||||
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) {
|
||||
async function upgrade(id, version) {
|
||||
await runPackageManagerCommandAsync('install', id, version || 'latest');
|
||||
const isActive = await Plugins.isActive(id);
|
||||
meta.reloadRequired = isActive;
|
||||
next(null, isActive);
|
||||
},
|
||||
], callback);
|
||||
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,20 +45,16 @@ 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);
|
||||
}
|
||||
const results = await async.parallel(methods);
|
||||
|
||||
Object.assign(Plugins.staticDirs, results.staticDirs || {});
|
||||
add(Plugins.cssFiles, results.cssFiles);
|
||||
@@ -75,13 +71,10 @@ module.exports = function (Plugins) {
|
||||
Plugins.languageData.namespaces = _.union(Plugins.languageData.namespaces, results.languageData.namespaces);
|
||||
}
|
||||
Plugins.pluginsData[pluginData.id] = pluginData;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
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,43 +109,34 @@ 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-.*$/;
|
||||
const themeNamePattern = /(@.*?\/)?nodebb-theme-.*$/;
|
||||
|
||||
Plugins.loadPlugin = function (pluginPath, callback) {
|
||||
Plugins.data.loadPluginInfo(pluginPath, function (err, pluginData) {
|
||||
if (err) {
|
||||
Plugins.loadPlugin = async function (pluginPath) {
|
||||
let pluginData;
|
||||
try {
|
||||
pluginData = await Plugins.data.loadPluginInfo(pluginPath);
|
||||
} catch (err) {
|
||||
if (err.message === '[[error:parse-error]]') {
|
||||
return callback();
|
||||
return;
|
||||
}
|
||||
|
||||
return callback(themeNamePattern.test(pluginPath) ? null : err);
|
||||
if (!themeNamePattern.test(pluginPath)) {
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
checkVersion(pluginData);
|
||||
|
||||
async.parallel([
|
||||
function (next) {
|
||||
registerHooks(pluginData, next);
|
||||
},
|
||||
function (next) {
|
||||
registerPluginAssets(pluginData, ['soundpack'], next);
|
||||
},
|
||||
], function (err) {
|
||||
if (err) {
|
||||
try {
|
||||
registerHooks(pluginData);
|
||||
await registerPluginAssets(pluginData, ['soundpack']);
|
||||
} catch (err) {
|
||||
winston.error(err.stack);
|
||||
winston.verbose('[plugins] Could not load plugin : ' + pluginData.id);
|
||||
return callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pluginData.private) {
|
||||
@@ -163,9 +147,6 @@ module.exports = function (Plugins) {
|
||||
}
|
||||
|
||||
winston.verbose('[plugins] Loaded plugin: ' + pluginData.id);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user