You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nodebb/src/plugins.js

442 lines
13 KiB
JavaScript

11 years ago
'use strict';
var fs = require('fs'),
path = require('path'),
async = require('async'),
winston = require('winston'),
11 years ago
semver = require('semver'),
express = require('express'),
nconf = require('nconf'),
11 years ago
db = require('./database'),
emitter = require('./emitter'),
11 years ago
meta = require('./meta'),
translator = require('../public/src/modules/translator'),
11 years ago
utils = require('../public/src/utils'),
hotswap = require('./hotswap'),
9 years ago
file = require('./file'),
controllers = require('./controllers'),
app, middleware;
11 years ago
11 years ago
(function(Plugins) {
require('./plugins/install')(Plugins);
require('./plugins/load')(Plugins);
require('./plugins/hooks')(Plugins);
11 years ago
Plugins.libraries = {};
Plugins.loadedHooks = {};
Plugins.staticDirs = {};
Plugins.cssFiles = [];
Plugins.lessFiles = [];
Plugins.clientScripts = [];
9 years ago
Plugins.acpScripts = [];
Plugins.customLanguages = [];
Plugins.customLanguageFallbacks = {};
10 years ago
Plugins.libraryPaths = [];
Plugins.versionWarning = [];
11 years ago
Plugins.initialized = false;
10 years ago
Plugins.requireLibrary = function(pluginID, libraryPath) {
Plugins.libraries[pluginID] = require(libraryPath);
Plugins.libraryPaths.push(libraryPath);
};
Plugins.init = function(nbbApp, nbbMiddleware, callback) {
callback = callback || function() {};
11 years ago
if (Plugins.initialized) {
return callback();
11 years ago
}
app = nbbApp;
middleware = nbbMiddleware;
hotswap.prepare(nbbApp);
11 years ago
if (global.env === 'development') {
winston.verbose('[plugins] Initializing plugins system');
11 years ago
}
Plugins.reload(function(err) {
if (err) {
winston.error('[plugins] NodeBB encountered a problem while loading plugins', err.message);
return callback(err);
11 years ago
}
11 years ago
if (global.env === 'development') {
winston.info('[plugins] Plugins OK');
}
11 years ago
Plugins.initialized = true;
emitter.emit('plugins:loaded');
callback();
11 years ago
});
};
Plugins.reload = function(callback) {
// Resetting all local plugin data
10 years ago
Plugins.libraries = {};
11 years ago
Plugins.loadedHooks = {};
Plugins.staticDirs = {};
Plugins.versionWarning = [];
11 years ago
Plugins.cssFiles.length = 0;
Plugins.lessFiles.length = 0;
Plugins.clientScripts.length = 0;
9 years ago
Plugins.acpScripts.length = 0;
10 years ago
Plugins.libraryPaths.length = 0;
11 years ago
Plugins.registerHook('core', {
hook: 'static:app.load',
method: addLanguages
});
11 years ago
async.waterfall([
function(next) {
db.getSortedSetRange('plugins:active', 0, -1, next);
11 years ago
},
function(plugins, next) {
if (!Array.isArray(plugins)) {
11 years ago
return next();
}
plugins = plugins.filter(function(plugin){
return plugin && typeof plugin === 'string';
}).map(function(plugin){
return path.join(__dirname, '../node_modules/', plugin);
});
9 years ago
async.filter(plugins, file.exists, function(plugins) {
10 years ago
async.eachSeries(plugins, Plugins.loadPlugin, next);
});
11 years ago
},
function(next) {
// If some plugins are incompatible, throw the warning here
10 years ago
if (Plugins.versionWarning.length && nconf.get('isPrimary') === 'true') {
process.stdout.write('\n');
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++) {
process.stdout.write(' * '.yellow + Plugins.versionWarning[x].reset + '\n');
}
process.stdout.write('\n');
}
11 years ago
Object.keys(Plugins.loadedHooks).forEach(function(hook) {
var hooks = Plugins.loadedHooks[hook];
hooks = hooks.sort(function(a, b) {
return a.priority - b.priority;
});
});
11 years ago
next();
}
11 years ago
], callback);
};
Plugins.reloadRoutes = function(callback) {
callback = callback || function() {};
var router = express.Router();
router.hotswapId = 'plugins';
router.render = function() {
app.render.apply(app, arguments);
};
Plugins.fireHook('static:app.load', {app: app, router: router, middleware: middleware, controllers: controllers}, function(err) {
if (err) {
return winston.error('[plugins] Encountered error while executing post-router plugins hooks: ' + err.message);
}
hotswap.replace('plugins', router);
winston.verbose('[plugins] All plugins reloaded and rerouted');
callback();
});
};
Plugins.getTemplates = function(callback) {
9 years ago
var templates = {},
tplName;
async.waterfall([
async.apply(db.getSortedSetRange, 'plugins:active', 0, -1),
function(plugins, next) {
var pluginBasePath = path.join(__dirname, '../node_modules');
var paths = plugins.map(function(plugin) {
return path.join(pluginBasePath, plugin);
});
// Filter out plugins with invalid paths
async.filter(paths, function(path, next) {
fs.access(path, fs.R_OK, function(err) {
next(!err);
});
}, function(paths) {
next(null, paths);
});
},
function(paths, next) {
async.map(paths, Plugins.loadPluginInfo, next);
}
], function(err, plugins) {
if (err) {
return callback(err);
}
async.each(plugins, function(plugin, next) {
9 years ago
if (plugin.templates || plugin.id.startsWith('nodebb-theme-')) {
winston.verbose('[plugins] Loading templates (' + plugin.id + ')');
var templatesPath = path.join(__dirname, '../node_modules', plugin.id, plugin.templates || 'templates');
utils.walk(templatesPath, function(err, pluginTemplates) {
if (pluginTemplates) {
pluginTemplates.forEach(function(pluginTemplate) {
9 years ago
tplName = "/" + pluginTemplate.replace(templatesPath, '').substring(1);
if (templates.hasOwnProperty(tplName)) {
winston.verbose('[plugins] ' + tplName + ' replaced by ' + plugin.id);
}
templates[tplName] = pluginTemplate;
});
} else {
winston.warn('[plugins/' + plugin.id + '] A templates directory was defined for this plugin, but was not found.');
}
next(false);
});
} else {
next(false);
}
}, function(err) {
callback(err, templates);
});
});
};
10 years ago
Plugins.get = function(id, callback) {
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + id;
require('request')(url, {
json: true
}, function(err, res, body) {
if (res.statusCode === 404 || !body.payload) {
return callback(err, {});
}
10 years ago
Plugins.normalise([body.payload], function(err, normalised) {
normalised = normalised.filter(function(plugin) {
return plugin.id === id;
});
10 years ago
return callback(err, !err ? normalised[0] : undefined);
});
});
};
Plugins.list = function(matching, callback) {
if (arguments.length === 1 && typeof matching === 'function') {
callback = matching;
matching = true;
}
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins' + (matching !== false ? '?version=' + require('../package.json').version : '');
10 years ago
10 years ago
require('request')(url, {
json: true
}, function(err, res, body) {
if (err) {
winston.error('Error parsing plugins : ' + err.message);
}
10 years ago
Plugins.normalise(body, callback);
});
};
Plugins.normalise = function(apiReturn, callback) {
9 years ago
var pluginMap = {};
9 years ago
var dependencies = require.main.require('./package.json').dependencies;
apiReturn = apiReturn || [];
10 years ago
for(var i=0; i<apiReturn.length; ++i) {
apiReturn[i].id = apiReturn[i].name;
apiReturn[i].installed = false;
apiReturn[i].active = false;
apiReturn[i].url = apiReturn[i].url ? apiReturn[i].url : apiReturn[i].repository ? apiReturn[i].repository.url : '';
apiReturn[i].latest = apiReturn[i].latest;
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;
});
10 years ago
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 = !!plugin.id.match('nodebb-theme-');
pluginMap[plugin.id].error = plugin.error || false;
pluginMap[plugin.id].active = plugin.active;
pluginMap[plugin.id].version = plugin.version;
// 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;
}
10 years ago
pluginMap[plugin.id].outdated = semver.gt(pluginMap[plugin.id].latest, pluginMap[plugin.id].version);
next();
}, function(err) {
if (err) {
return callback(err);
}
10 years ago
var pluginArray = [];
10 years ago
10 years ago
for (var key in pluginMap) {
if (pluginMap.hasOwnProperty(key)) {
pluginArray.push(pluginMap[key]);
}
10 years ago
}
10 years ago
pluginArray.sort(function(a, b) {
if (a.name > b.name ) {
return 1;
} else if (a.name < b.name ){
return -1;
} else {
return 0;
}
});
10 years ago
callback(null, pluginArray);
});
});
};
11 years ago
Plugins.showInstalled = function(callback) {
var npmPluginPath = path.join(__dirname, '../node_modules');
11 years ago
async.waterfall([
async.apply(fs.readdir, npmPluginPath),
function(dirs, next) {
dirs = dirs.filter(function(dir){
10 years ago
return dir.startsWith('nodebb-plugin-') ||
dir.startsWith('nodebb-widget-') ||
dir.startsWith('nodebb-rewards-') ||
10 years ago
dir.startsWith('nodebb-theme-');
}).map(function(dir){
return path.join(npmPluginPath, dir);
});
async.filter(dirs, function(dir, callback){
fs.stat(dir, function(err, stats){
10 years ago
callback(!err && stats.isDirectory());
});
}, function(plugins){
next(null, plugins);
11 years ago
});
},
11 years ago
function(files, next) {
var plugins = [];
async.each(files, function(file, next) {
async.waterfall([
function(next) {
10 years ago
Plugins.loadPluginInfo(file, next);
11 years ago
},
10 years ago
function(pluginData, next) {
Plugins.isActive(pluginData.name, function(err, active) {
11 years ago
if (err) {
10 years ago
return next(new Error('no-active-state'));
11 years ago
}
10 years ago
delete pluginData.hooks;
delete pluginData.library;
pluginData.active = active;
pluginData.installed = true;
pluginData.error = false;
next(null, pluginData);
11 years ago
});
}
10 years ago
], function(err, pluginData) {
11 years ago
if (err) {
return next(); // Silently fail
}
10 years ago
plugins.push(pluginData);
11 years ago
next();
});
}, function(err) {
next(null, plugins);
});
}
], callback);
};
10 years ago
Plugins.clearRequireCache = function(next) {
10 years ago
var cached = Object.keys(require.cache);
async.waterfall([
async.apply(async.map, Plugins.libraryPaths, fs.realpath),
function(paths, next) {
paths = paths.map(function(pluginLib) {
var parent = path.dirname(pluginLib);
return cached.filter(function(libPath) {
return libPath.indexOf(parent) !== -1;
});
}).reduce(function(prev, cur) {
return prev.concat(cur);
});
10 years ago
Plugins.fireHook('filter:plugins.clearRequireCache', {paths: paths}, next);
},
function(data, next) {
for (var x=0,numPaths=data.paths.length;x<numPaths;x++) {
delete require.cache[data.paths[x]];
}
winston.verbose('[plugins] Plugin libraries removed from Node.js cache');
next();
10 years ago
}
10 years ago
], next);
10 years ago
};
function addLanguages(params, callback) {
Plugins.customLanguages.forEach(function(lang) {
params.router.get('/language' + lang.route, function(req, res, next) {
res.json(lang.file);
});
var components = lang.route.split('/'),
language = components[1],
filename = components[2].replace('.json', '');
translator.addTranslation(language, filename, lang.file);
});
10 years ago
for(var resource in Plugins.customLanguageFallbacks) {
params.router.get('/language/:lang/' + resource + '.json', function(req, res, next) {
winston.verbose('[translator] No resource file found for ' + req.params.lang + '/' + path.basename(req.path, '.json') + ', using provided fallback language file');
res.sendFile(Plugins.customLanguageFallbacks[path.basename(req.path, '.json')]);
10 years ago
});
}
callback(null);
}
11 years ago
}(exports));