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

389 lines
11 KiB
JavaScript

11 years ago
'use strict';
9 years ago
var fs = require('fs');
var path = require('path');
var async = require('async');
var winston = require('winston');
var semver = require('semver');
var express = require('express');
var nconf = require('nconf');
var db = require('./database');
var utils = require('../public/src/utils');
var hotswap = require('./hotswap');
var file = require('./file');
var languages = require('./languages');
9 years ago
var app;
var middleware;
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 = [];
10 years ago
Plugins.libraryPaths = [];
Plugins.versionWarning = [];
8 years ago
Plugins.languageCodes = [];
Plugins.soundpacks = [];
11 years ago
Plugins.initialized = false;
Plugins.requireLibrary = function (pluginID, libraryPath) {
10 years ago
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
}
if (nbbApp) {
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) {
11 years ago
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;
callback();
11 years ago
});
};
Plugins.reload = function (callback) {
11 years ago
// 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
async.waterfall([
function (next) {
// Build language code list
languages.list(function (err, languages) {
if (err) {
return next(err);
}
Plugins.languageCodes = languages.map(function (data) {
return data.code;
});
next();
});
},
async.apply(Plugins.getPluginPaths),
8 years ago
function (paths, next) {
async.eachSeries(paths, 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 += 1) {
process.stdout.write(' * '.yellow + Plugins.versionWarning[x] + '\n');
}
process.stdout.write('\n');
}
Object.keys(Plugins.loadedHooks).forEach(function (hook) {
11 years ago
var hooks = Plugins.loadedHooks[hook];
hooks.sort(function (a, b) {
11 years ago
return a.priority - b.priority;
});
});
11 years ago
next();
},
11 years ago
], callback);
};
Plugins.reloadRoutes = function (callback) {
var router = express.Router();
router.hotswapId = 'plugins';
router.render = function () {
app.render.apply(app, arguments);
};
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.message);
return callback(err);
}
hotswap.replace('plugins', router);
winston.verbose('[plugins] All plugins reloaded and rerouted');
callback();
});
};
Plugins.getTemplates = function (callback) {
var templates = {};
var tplName;
9 years ago
async.waterfall([
async.apply(db.getSortedSetRange, 'plugins:active', 0, -1),
function (plugins, next) {
9 years ago
var pluginBasePath = path.join(__dirname, '../node_modules');
var paths = plugins.map(function (plugin) {
9 years ago
return path.join(pluginBasePath, plugin);
});
// Filter out plugins with invalid paths
async.filter(paths, file.exists, function (paths) {
9 years ago
next(null, paths);
});
},
function (paths, next) {
9 years ago
async.map(paths, Plugins.loadPluginInfo, next);
},
], function (err, plugins) {
9 years ago
if (err) {
return callback(err);
}
async.eachSeries(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
if (pluginTemplate.endsWith('.tpl')) {
8 years ago
tplName = '/' + pluginTemplate.replace(templatesPath, '').substring(1);
9 years ago
9 years ago
if (templates.hasOwnProperty(tplName)) {
winston.verbose('[plugins] ' + tplName + ' replaced by ' + plugin.id);
}
9 years ago
9 years ago
templates[tplName] = pluginTemplate;
} else {
winston.warn('[plugins] Skipping ' + pluginTemplate + ' by plugin ' + plugin.id);
}
});
} else if (err) {
winston.error(err);
} 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);
});
});
};
Plugins.get = function (id, callback) {
10 years ago
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, {});
}
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 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 : '');
10 years ago
10 years ago
require('request')(url, {
json: true,
}, function (err, res, body) {
10 years ago
if (err) {
winston.error('Error parsing plugins : ' + err.message);
return callback(err);
}
10 years ago
Plugins.normalise(body, callback);
});
};
Plugins.normalise = function (apiReturn, callback) {
9 years ago
var pluginMap = {};
var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies;
9 years ago
apiReturn = apiReturn || [];
for (var i = 0; i < apiReturn.length; i += 1) {
10 years ago
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 : '');
10 years ago
pluginMap[apiReturn[i].name] = apiReturn[i];
}
Plugins.showInstalled(function (err, installedPlugins) {
10 years ago
if (err) {
return callback(err);
}
installedPlugins = installedPlugins.filter(function (plugin) {
return plugin && !plugin.system;
});
async.each(installedPlugins, function (plugin, next) {
10 years ago
// 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;
pluginMap[plugin.id].settingsRoute = plugin.settingsRoute;
// 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
}
pluginArray.sort(function (a, b) {
if (a.name > b.name) {
10 years ago
return 1;
} else if (a.name < b.name) {
10 years ago
return -1;
}
return 0;
});
10 years ago
callback(null, pluginArray);
});
});
};
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
});
},
function (files, next) {
11 years ago
var plugins = [];
async.each(files, function (file, next) {
11 years ago
async.waterfall([
function (next) {
10 years ago
Plugins.loadPluginInfo(file, next);
11 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
});
},
], 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(err, plugins);
11 years ago
});
},
], callback);
};
11 years ago
}(exports));