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

@@ -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');
}
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('');
}
const paths = await Plugins.getPluginPaths();
for (const path of paths) {
/* eslint-disable no-await-in-loop */
await Plugins.loadPlugin(path);
}
Object.keys(Plugins.loadedHooks).forEach(function (hook) {
var hooks = Plugins.loadedHooks[hook];
hooks.sort(function (a, b) {
return a.priority - b.priority;
});
});
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);
// 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];
}
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;
});
Plugins.showInstalled(function (err, installedPlugins) {
if (err) {
return callback(err);
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;
}
installedPlugins = installedPlugins.filter(function (plugin) {
return plugin && !plugin.system;
});
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;
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();
}
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);
});
// 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);
});
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.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) {
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.push(pluginData);
next();
});
}, function (err) {
next(err, plugins);
});
},
], callback);
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();
}
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);