Files
NodeBB/src/plugins/index.js

344 lines
10 KiB
JavaScript
Raw Normal View History

2014-07-04 14:43:03 -04:00
'use strict';
2019-07-22 00:30:47 -04:00
const fs = require('fs');
const os = require('os');
2019-07-22 00:30:47 -04:00
const path = require('path');
const async = require('async');
const winston = require('winston');
const semver = require('semver');
const nconf = require('nconf');
const request = require('request-promise-native');
2019-07-22 00:30:47 -04:00
const user = require('../user');
const posts = require('../posts');
const meta = require('../meta');
2020-10-02 15:34:57 -04:00
const { pluginNamePattern, themeNamePattern, paths } = require('../constants');
2021-02-04 00:06:15 -07:00
let app;
let middleware;
2013-12-02 16:19:30 -05:00
2019-07-22 00:30:47 -04:00
const Plugins = module.exports;
2017-05-27 01:44:26 -04:00
2018-10-23 15:03:32 -04:00
require('./install')(Plugins);
require('./load')(Plugins);
require('./usage')(Plugins);
2018-10-23 15:03:32 -04:00
Plugins.data = require('./data');
Plugins.hooks = require('./hooks');
// Backwards compatibility for hooks, remove in v1.18.0
const _deprecate = async function () {
const args = Array.from(arguments);
const oldMethod = args.shift();
const newMethod = args.shift();
const method = args.shift();
const stack = new Error().stack.toString().split(os.EOL);
const context = stack[stack.findIndex(line => line.startsWith(' at Object.wrapperCallback')) + 1];
winston.warn(`[plugins/hooks] ${oldMethod} has been deprecated, call ${newMethod} instead.`);
winston.warn(`[plugins/hooks] ${context}`);
return method.apply(Plugins.hooks, args);
};
Plugins.registerHook = _deprecate.bind(null, 'Plugins.registerHook', 'Plugins.hooks.register', Plugins.hooks.register);
Plugins.unregisterHook = _deprecate.bind(null, 'Plugins.unregisterHook', 'Plugins.hooks.unregister', Plugins.hooks.unregister);
Plugins.fireHook = _deprecate.bind(null, 'Plugins.fireHook', 'Plugins.hooks.fire', Plugins.hooks.fire);
Plugins.hasListeners = _deprecate.bind(null, 'Plugins.hasListeners', 'Plugins.hooks.hasListeners', Plugins.hooks.hasListeners);
2021-01-27 17:41:07 -05:00
// end
2017-05-27 01:44:26 -04:00
Plugins.getPluginPaths = Plugins.data.getPluginPaths;
Plugins.loadPluginInfo = Plugins.data.loadPluginInfo;
Plugins.pluginsData = {};
Plugins.libraries = {};
Plugins.loadedHooks = {};
Plugins.staticDirs = {};
Plugins.cssFiles = [];
Plugins.lessFiles = [];
2017-08-21 17:48:58 -04:00
Plugins.acpLessFiles = [];
2017-05-27 01:44:26 -04:00
Plugins.clientScripts = [];
Plugins.acpScripts = [];
Plugins.libraryPaths = [];
Plugins.versionWarning = [];
Plugins.languageData = {};
Plugins.loadedPlugins = [];
2017-05-27 01:44:26 -04:00
Plugins.initialized = false;
Plugins.requireLibrary = function (pluginData) {
let libraryPath;
// attempt to load a plugin directly with `require("nodebb-plugin-*")`
// Plugins should define their entry point in the standard `main` property of `package.json`
try {
libraryPath = pluginData.path;
Plugins.libraries[pluginData.id] = require(libraryPath);
} catch (e) {
// DEPRECATED: @1.15.0, remove in version >=1.17
// for backwards compatibility
// if that fails, fall back to `pluginData.library`
if (pluginData.library) {
winston.warn(` [plugins/${pluginData.id}] The plugin.json field "library" is deprecated. Please use the package.json field "main" instead.`);
winston.verbose(`[plugins/${pluginData.id}] See https://github.com/NodeBB/NodeBB/issues/8686`);
libraryPath = path.join(pluginData.path, pluginData.library);
Plugins.libraries[pluginData.id] = require(libraryPath);
} else {
throw e;
}
}
2017-05-27 01:44:26 -04:00
Plugins.libraryPaths.push(libraryPath);
};
2019-07-22 00:30:47 -04:00
Plugins.init = async function (nbbApp, nbbMiddleware) {
2017-05-27 01:44:26 -04:00
if (Plugins.initialized) {
2019-07-22 00:30:47 -04:00
return;
2017-05-27 01:44:26 -04:00
}
if (nbbApp) {
app = nbbApp;
middleware = nbbMiddleware;
}
if (global.env === 'development') {
winston.verbose('[plugins] Initializing plugins system');
}
2019-07-22 00:30:47 -04:00
await Plugins.reload();
if (global.env === 'development') {
winston.info('[plugins] Plugins OK');
}
2013-12-03 13:36:44 -05:00
2019-07-22 00:30:47 -04:00
Plugins.initialized = true;
2017-05-27 01:44:26 -04:00
};
2013-12-03 13:36:44 -05:00
2019-07-22 00:30:47 -04:00
Plugins.reload = async function () {
2017-05-27 01:44:26 -04:00
// Resetting all local plugin data
Plugins.libraries = {};
Plugins.loadedHooks = {};
Plugins.staticDirs = {};
Plugins.versionWarning = [];
Plugins.cssFiles.length = 0;
Plugins.lessFiles.length = 0;
2017-08-21 17:48:58 -04:00
Plugins.acpLessFiles.length = 0;
2017-05-27 01:44:26 -04:00
Plugins.clientScripts.length = 0;
Plugins.acpScripts.length = 0;
Plugins.libraryPaths.length = 0;
Plugins.loadedPlugins.length = 0;
2017-05-27 01:44:26 -04:00
await user.addInterstitials();
2019-07-22 00:30:47 -04:00
const paths = await Plugins.getPluginPaths();
for (const path of paths) {
/* eslint-disable no-await-in-loop */
await Plugins.loadPlugin(path);
}
2013-12-03 13:36:44 -05:00
2019-07-22 00:30:47 -04:00
// If some plugins are incompatible, throw the warning here
if (Plugins.versionWarning.length && nconf.get('isPrimary')) {
2019-07-22 00:30:47 -04:00
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.');
2021-02-04 00:06:15 -07:00
for (let x = 0, numPlugins = Plugins.versionWarning.length; x < numPlugins; x += 1) {
2019-07-22 00:30:47 -04:00
console.log(' * '.yellow + Plugins.versionWarning[x]);
2017-05-27 01:44:26 -04:00
}
2019-07-22 00:30:47 -04:00
console.log('');
}
// Core hooks
posts.registerHooks();
meta.configs.registerHooks();
2020-10-02 15:34:57 -04:00
// Lower priority runs earlier
2021-02-04 00:01:39 -07:00
Object.keys(Plugins.loadedHooks).forEach((hook) => {
2019-07-22 00:30:47 -04:00
Plugins.loadedHooks[hook].sort((a, b) => a.priority - b.priority);
2017-05-27 01:44:26 -04:00
});
// Post-reload actions
await posts.configureSanitize();
2017-05-27 01:44:26 -04:00
};
2019-07-22 18:16:18 -04:00
Plugins.reloadRoutes = async function (params) {
2021-02-04 00:06:15 -07:00
const controllers = require('../controllers');
await Plugins.hooks.fire('static:app.load', { app: app, router: params.router, middleware: middleware, controllers: controllers });
2019-07-22 00:30:47 -04:00
winston.verbose('[plugins] All plugins reloaded and rerouted');
};
2015-07-08 17:04:21 -04:00
2019-07-22 00:30:47 -04:00
Plugins.get = async function (id) {
2021-02-03 23:59:08 -07:00
const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugins/${id}`;
const body = await request(url, {
json: true,
});
2019-07-22 00:30:47 -04:00
let normalised = await Plugins.normalise([body ? body.payload : {}]);
normalised = normalised.filter(plugin => plugin.id === id);
return normalised.length ? normalised[0] : undefined;
2017-05-27 01:44:26 -04:00
};
2019-07-22 00:30:47 -04:00
Plugins.list = async function (matching) {
if (matching === undefined) {
2017-05-27 01:44:26 -04:00
matching = true;
}
const version = require(paths.currentPackage).version;
2021-02-03 23:59:08 -07:00
const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugins${matching !== false ? `?version=${version}` : ''}`;
2019-07-22 00:30:47 -04:00
try {
const body = await request(url, {
json: true,
});
2019-07-22 00:30:47 -04:00
return await Plugins.normalise(body);
} catch (err) {
2021-02-03 23:59:08 -07:00
winston.error(`Error loading ${url}`, err);
2019-07-22 00:30:47 -04:00
return await Plugins.normalise([]);
}
2017-05-27 01:44:26 -04:00
};
Plugins.listTrending = async () => {
const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/analytics/top/week`;
return await request(url, {
json: true,
});
};
2019-07-22 00:30:47 -04:00
Plugins.normalise = async function (apiReturn) {
const pluginMap = {};
const dependencies = require(paths.currentPackage).dependencies;
apiReturn = Array.isArray(apiReturn) ? apiReturn : [];
2021-02-04 00:01:39 -07:00
apiReturn.forEach((packageData) => {
2019-07-22 00:30:47 -04:00
packageData.id = packageData.name;
packageData.installed = false;
packageData.active = false;
packageData.url = packageData.url || (packageData.repository ? packageData.repository.url : '');
pluginMap[packageData.name] = packageData;
});
2019-07-22 00:30:47 -04:00
let installedPlugins = await Plugins.showInstalled();
installedPlugins = installedPlugins.filter(plugin => plugin && !plugin.system);
2015-07-08 17:04:21 -04:00
2021-02-04 00:01:39 -07:00
installedPlugins.forEach((plugin) => {
2019-07-22 00:30:47 -04:00
// If it errored out because a package.json or plugin.json couldn't be read, no need to do this stuff
if (plugin.error) {
2017-05-27 01:44:26 -04:00
pluginMap[plugin.id] = pluginMap[plugin.id] || {};
pluginMap[plugin.id].installed = true;
2019-07-22 00:30:47 -04:00
pluginMap[plugin.id].error = true;
return;
}
2019-07-22 00:30:47 -04:00
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);
});
2015-08-13 12:33:24 -04:00
2019-07-22 00:30:47 -04:00
const pluginArray = [];
2021-02-04 00:06:15 -07:00
for (const key in pluginMap) {
2019-07-22 00:30:47 -04:00
if (pluginMap.hasOwnProperty(key)) {
pluginArray.push(pluginMap[key]);
}
}
2021-02-04 00:01:39 -07:00
pluginArray.sort((a, b) => {
2019-07-22 00:30:47 -04:00
if (a.name > b.name) {
return 1;
} else if (a.name < b.name) {
return -1;
}
return 0;
2017-05-27 01:44:26 -04:00
});
2019-07-22 00:30:47 -04:00
return pluginArray;
2017-05-27 01:44:26 -04:00
};
Plugins.nodeModulesPath = paths.nodeModules;
2017-07-19 11:38:51 -06:00
2019-07-22 00:30:47 -04:00
Plugins.showInstalled = async function () {
2020-08-14 00:05:03 -04:00
const dirs = await fs.promises.readdir(Plugins.nodeModulesPath);
2019-07-22 00:30:47 -04:00
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.stack);
2019-07-22 00:30:47 -04:00
}
}
const plugins = await Promise.all(pluginPaths.map(file => load(file)));
return plugins.filter(Boolean);
};
async function findNodeBBModules(dirs) {
const pluginPaths = [];
2021-02-04 00:01:39 -07:00
await async.each(dirs, (dirname, next) => {
2021-02-04 00:06:15 -07:00
const dirPath = path.join(Plugins.nodeModulesPath, dirname);
2019-07-22 00:30:47 -04:00
async.waterfall([
function (cb) {
2021-02-04 00:01:39 -07:00
fs.stat(dirPath, (err, stats) => {
2019-07-22 00:30:47 -04:00
if (err && err.code !== 'ENOENT') {
return cb(err);
}
if (err || !stats.isDirectory()) {
return next();
}
if (pluginNamePattern.test(dirname)) {
pluginPaths.push(dirname);
return next();
2017-05-27 01:44:26 -04:00
}
2019-07-22 00:30:47 -04:00
if (dirname[0] !== '@') {
return next();
}
fs.readdir(dirPath, cb);
2013-12-03 13:36:44 -05:00
});
2019-07-22 00:30:47 -04:00
},
function (subdirs, cb) {
2021-02-04 00:01:39 -07:00
async.each(subdirs, (subdir, next) => {
2019-07-22 00:30:47 -04:00
if (!pluginNamePattern.test(subdir)) {
return next();
}
2021-02-04 00:06:15 -07:00
const subdirPath = path.join(dirPath, subdir);
2021-02-04 00:01:39 -07:00
fs.stat(subdirPath, (err, stats) => {
2019-07-22 00:30:47 -04:00
if (err && err.code !== 'ENOENT') {
return next(err);
}
if (err || !stats.isDirectory()) {
return next();
}
2021-02-03 23:59:08 -07:00
pluginPaths.push(`${dirname}/${subdir}`);
2019-07-22 00:30:47 -04:00
next();
});
}, cb);
},
], next);
});
return pluginPaths;
}
2018-11-19 13:03:31 -05:00
require('../promisify')(Plugins);