mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 02:55:58 +01:00 
			
		
		
		
	Conflicts: public/templates/accountedit.tpl public/templates/header.tpl src/routes/meta.js src/webserver.js
		
			
				
	
	
		
			499 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			499 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| var fs = require('fs'),
 | |
| 	path = require('path'),
 | |
| 	async = require('async'),
 | |
| 	winston = require('winston'),
 | |
| 	nconf = require('nconf'),
 | |
| 	eventEmitter = require('events').EventEmitter,
 | |
| 	semver = require('semver'),
 | |
| 
 | |
| 	db = require('./database'),
 | |
| 	meta = require('./meta'),
 | |
| 	utils = require('./../public/src/utils'),
 | |
| 	pkg = require('../package.json');
 | |
| 
 | |
| (function(Plugins) {
 | |
| 
 | |
| 	Plugins.libraries = {};
 | |
| 	Plugins.loadedHooks = {};
 | |
| 	Plugins.staticDirs = {};
 | |
| 	Plugins.cssFiles = [];
 | |
| 	Plugins.lessFiles = [];
 | |
| 	Plugins.clientScripts = [];
 | |
| 
 | |
| 	Plugins.initialized = false;
 | |
| 
 | |
| 	// Events
 | |
| 	Plugins.readyEvent = new eventEmitter;
 | |
| 
 | |
| 	Plugins.init = function() {
 | |
| 		if (Plugins.initialized) {
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		if (global.env === 'development') {
 | |
| 			winston.info('[plugins] Initializing plugins system');
 | |
| 		}
 | |
| 
 | |
| 		Plugins.reload(function(err) {
 | |
| 			if (err) {
 | |
| 				if (global.env === 'development') {
 | |
| 					winston.info('[plugins] NodeBB encountered a problem while loading plugins', err.message);
 | |
| 				}
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			if (global.env === 'development') {
 | |
| 				winston.info('[plugins] Plugins OK');
 | |
| 			}
 | |
| 			Plugins.initialized = true;
 | |
| 			Plugins.readyEvent.emit('ready');
 | |
| 		});
 | |
| 	};
 | |
| 
 | |
| 	Plugins.ready = function(callback) {
 | |
| 		if (!Plugins.initialized) {
 | |
| 			Plugins.readyEvent.once('ready', callback);
 | |
| 		} else {
 | |
| 			callback();
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Plugins.reload = function(callback) {
 | |
| 		// Resetting all local plugin data
 | |
| 		Plugins.loadedHooks = {};
 | |
| 		Plugins.staticDirs = {};
 | |
| 		Plugins.cssFiles.length = 0;
 | |
| 		Plugins.lessFiles.length = 0;
 | |
| 		Plugins.clientScripts.length = 0;
 | |
| 
 | |
| 		// Read the list of activated plugins and require their libraries
 | |
| 		async.waterfall([
 | |
| 			function(next) {
 | |
| 				db.getSetMembers('plugins:active', next);
 | |
| 			},
 | |
| 			function(plugins, next) {
 | |
| 				if (plugins && Array.isArray(plugins)) {
 | |
| 					plugins.push(meta.config['theme:id']);
 | |
| 
 | |
| 					async.each(plugins, function(plugin, next) {
 | |
| 						if (!plugin || typeof plugin !== 'string') {
 | |
| 							return next();
 | |
| 						}
 | |
| 
 | |
| 						var modulePath = path.join(__dirname, '../node_modules/', plugin);
 | |
| 						if (fs.existsSync(modulePath)) {
 | |
| 							Plugins.loadPlugin(modulePath, next);
 | |
| 						} else {
 | |
| 							if (global.env === 'development') {
 | |
| 								winston.warn('[plugins] Plugin \'' + plugin + '\' not found');
 | |
| 							}
 | |
| 							next(); // Ignore this plugin silently
 | |
| 						}
 | |
| 					}, next);
 | |
| 				} else next();
 | |
| 			},
 | |
| 			function(next) {
 | |
| 				if (global.env === 'development') winston.info('[plugins] Sorting hooks to fire in priority sequence');
 | |
| 				Object.keys(Plugins.loadedHooks).forEach(function(hook) {
 | |
| 					var hooks = Plugins.loadedHooks[hook];
 | |
| 					hooks = hooks.sort(function(a, b) {
 | |
| 						return a.priority - b.priority;
 | |
| 					});
 | |
| 				});
 | |
| 
 | |
| 				next();
 | |
| 			}
 | |
| 		], callback);
 | |
| 	};
 | |
| 
 | |
| 	Plugins.loadPlugin = function(pluginPath, callback) {
 | |
| 		fs.readFile(path.join(pluginPath, 'plugin.json'), function(err, data) {
 | |
| 			if (err) {
 | |
| 				return callback(pluginPath.match('nodebb-theme') ? null  : err);
 | |
| 			}
 | |
| 
 | |
| 			var pluginData = JSON.parse(data),
 | |
| 				libraryPath, staticDir;
 | |
| 
 | |
| 			if (pluginData.minver && semver.validRange(pluginData.minver)) {
 | |
| 				if (!semver.satisfies(pkg.version, pluginData.minver)) {
 | |
| 					// If NodeBB is not new enough to run this plugin
 | |
| 					winston.warn('[plugins/' + pluginData.id + '] This plugin may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing.');
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			async.parallel([
 | |
| 				function(next) {
 | |
| 					if (pluginData.library) {
 | |
| 						libraryPath = path.join(pluginPath, pluginData.library);
 | |
| 
 | |
| 						fs.exists(libraryPath, function(exists) {
 | |
| 							if (exists) {
 | |
| 								if (!Plugins.libraries[pluginData.id]) {
 | |
| 									Plugins.libraries[pluginData.id] = require(libraryPath);
 | |
| 								}
 | |
| 
 | |
| 								// Register hooks for this plugin
 | |
| 								if (pluginData.hooks && Array.isArray(pluginData.hooks) && pluginData.hooks.length > 0) {
 | |
| 									async.each(pluginData.hooks, function(hook, next) {
 | |
| 										Plugins.registerHook(pluginData.id, hook, next);
 | |
| 									}, next);
 | |
| 								} else {
 | |
| 									next(null);
 | |
| 								}
 | |
| 							} else {
 | |
| 								winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id);
 | |
| 								next();
 | |
| 							}
 | |
| 						});
 | |
| 					} else {
 | |
| 						winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id);
 | |
| 						next();
 | |
| 					}
 | |
| 				},
 | |
| 				function(next) {
 | |
| 					// Static Directories for Plugins
 | |
| 					var	realPath,
 | |
| 						validMappedPath = /^[\w\-_]+$/;
 | |
| 
 | |
| 					pluginData.staticDirs = pluginData.staticDirs || {};
 | |
| 
 | |
| 					// Deprecated, to be removed v0.5
 | |
| 					if (pluginData.staticDir) {
 | |
| 						winston.warn('[plugins/' + pluginData.id + '] staticDir is deprecated, use staticDirs instead');
 | |
| 						Plugins.staticDirs[pluginData.id] = path.join(pluginPath, pluginData.staticDir);
 | |
| 					}
 | |
| 
 | |
| 					for(key in pluginData.staticDirs) {
 | |
| 						(function(mappedPath) {
 | |
| 							if (pluginData.staticDirs.hasOwnProperty(mappedPath)) {
 | |
| 								if (Plugins.staticDirs[mappedPath]) {
 | |
| 									winston.warn('[plugins/' + pluginData.id + '] Mapped path (' + mappedPath + ') already specified!');
 | |
| 								} else if (!validMappedPath.test(mappedPath)) {
 | |
| 									winston.warn('[plugins/' + pluginData.id + '] Invalid mapped path specified: ' + mappedPath + '. Path must adhere to: ' + validMappedPath.toString());
 | |
| 								} else {
 | |
| 									realPath = pluginData.staticDirs[mappedPath];
 | |
| 									staticDir = path.join(pluginPath, realPath);
 | |
| 
 | |
| 									(function(staticDir) {
 | |
| 										fs.exists(staticDir, function(exists) {
 | |
| 											if (exists) {
 | |
| 												Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir;
 | |
| 											} else {
 | |
| 												winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + mappedPath + ' => ' + staticDir + '\' not found.');
 | |
| 											}
 | |
| 										});
 | |
| 									}(staticDir));
 | |
| 								}
 | |
| 							}
 | |
| 						}(key));
 | |
| 					}
 | |
| 
 | |
| 					next();
 | |
| 				},
 | |
| 				function(next) {
 | |
| 					// CSS Files for plugins
 | |
| 					if (pluginData.css && pluginData.css instanceof Array) {
 | |
| 						if (global.env === 'development') {
 | |
| 							winston.info('[plugins] Found ' + pluginData.css.length + ' CSS file(s) for plugin ' + pluginData.id);
 | |
| 						}
 | |
| 
 | |
| 						Plugins.cssFiles = Plugins.cssFiles.concat(pluginData.css.map(function(file) {
 | |
| 							if (fs.existsSync(path.join(__dirname, '../node_modules', pluginData.id, file))) {
 | |
| 								return path.join(pluginData.id, file);
 | |
| 							} else {
 | |
| 								// Backwards compatibility with < v0.4.0, remove this for v0.5.0
 | |
| 								if (pluginData.staticDir) {
 | |
| 									return path.join(pluginData.id, pluginData.staticDir, file);
 | |
| 								} else {
 | |
| 									winston.error('[plugins/' + pluginData.id + '] This plugin\'s CSS is incorrectly configured, please contact the plugin author.');
 | |
| 									return null;
 | |
| 								}
 | |
| 							}
 | |
| 						}).filter(function(path) { return path }));	// Filter out nulls, remove this for v0.5.0
 | |
| 					}
 | |
| 
 | |
| 					next();
 | |
| 				},
 | |
| 				function(next) {
 | |
| 					// LESS files for plugins
 | |
| 					if (pluginData.less && pluginData.less instanceof Array) {
 | |
| 						if (global.env === 'development') {
 | |
| 							winston.info('[plugins] Found ' + pluginData.less.length + ' LESS file(s) for plugin ' + pluginData.id);
 | |
| 						}
 | |
| 
 | |
| 						Plugins.lessFiles = Plugins.lessFiles.concat(pluginData.less.map(function(file) {
 | |
| 							return path.join(pluginData.id, file);
 | |
| 						}));
 | |
| 					}
 | |
| 
 | |
| 					next();
 | |
| 				},
 | |
| 				function(next) {
 | |
| 					// Client-side scripts
 | |
| 					if (pluginData.scripts && pluginData.scripts instanceof Array) {
 | |
| 						if (global.env === 'development') {
 | |
| 							winston.info('[plugins] Found ' + pluginData.scripts.length + ' js file(s) for plugin ' + pluginData.id);
 | |
| 						}
 | |
| 
 | |
| 						Plugins.clientScripts = Plugins.clientScripts.concat(pluginData.scripts.map(function(file) {
 | |
| 							return path.join(__dirname, '../node_modules/', pluginData.id, file);
 | |
| 						}));
 | |
| 					}
 | |
| 
 | |
| 					next();
 | |
| 				}
 | |
| 			], function(err) {
 | |
| 				if (!err) {
 | |
| 					if (global.env === 'development') {
 | |
| 						winston.info('[plugins] Loaded plugin: ' + pluginData.id);
 | |
| 					}
 | |
| 					callback();
 | |
| 				} else {
 | |
| 					callback(new Error('Could not load plugin system'));
 | |
| 				}
 | |
| 			});
 | |
| 		});
 | |
| 	};
 | |
| 
 | |
| 	Plugins.registerHook = function(id, data, callback) {
 | |
| 		/*
 | |
| 			`data` is an object consisting of (* is required):
 | |
| 				`data.hook`*, the name of the NodeBB hook
 | |
| 				`data.method`*, the method called in that plugin
 | |
| 				`data.callbacked`, whether or not the hook expects a callback (true), or a return (false). Only used for filters. (Default: false)
 | |
| 				`data.priority`, the relative priority of the method when it is eventually called (default: 10)
 | |
| 		*/
 | |
| 
 | |
| 		var method;
 | |
| 
 | |
| 		if (data.hook && data.method && typeof data.method === 'string' && data.method.length > 0) {
 | |
| 			data.id = id;
 | |
| 			if (!data.priority) data.priority = 10;
 | |
| 			method = data.method.split('.').reduce(function(memo, prop) {
 | |
| 				if (memo !== null && memo[prop]) {
 | |
| 					return memo[prop];
 | |
| 				} else {
 | |
| 					// Couldn't find method by path, aborting
 | |
| 					return null;
 | |
| 				}
 | |
| 			}, Plugins.libraries[data.id]);
 | |
| 
 | |
| 			if (method === null) {
 | |
| 				winston.warn('[plugins/' + id + '] Hook method mismatch: ' + data.hook + ' => ' + data.method);
 | |
| 				return callback();
 | |
| 			}
 | |
| 
 | |
| 			// Write the actual method reference to the hookObj
 | |
| 			data.method = method;
 | |
| 
 | |
| 			Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || [];
 | |
| 			Plugins.loadedHooks[data.hook].push(data);
 | |
| 
 | |
| 			callback();
 | |
| 		} else return;
 | |
| 	};
 | |
| 
 | |
| 	Plugins.hasListeners = function(hook) {
 | |
| 		return (Plugins.loadedHooks[hook] && Plugins.loadedHooks[hook].length > 0);
 | |
| 	};
 | |
| 
 | |
| 	Plugins.fireHook = function(hook) {
 | |
| 		var callback = typeof arguments[arguments.length-1] === "function" ? arguments[arguments.length-1] : null,
 | |
| 			args = arguments.length ? Array.prototype.slice.call(arguments, 1) : [];
 | |
| 
 | |
| 		if (callback) {
 | |
| 			args.pop();
 | |
| 		}
 | |
| 		
 | |
| 		hookList = Plugins.loadedHooks[hook];
 | |
| 
 | |
| 		if (hookList && Array.isArray(hookList)) {
 | |
| 			//if (global.env === 'development') winston.info('[plugins] Firing hook: \'' + hook + '\'');
 | |
| 			var hookType = hook.split(':')[0];
 | |
| 			switch (hookType) {
 | |
| 				case 'filter':
 | |
| 					async.reduce(hookList, args, function(value, hookObj, next) {
 | |
| 						if (hookObj.method) {
 | |
| 							if (hookObj.callbacked) {
 | |
| 								hookObj.method.apply(Plugins, value.concat(function() {
 | |
| 									next(arguments[0], Array.prototype.slice.call(arguments, 1));
 | |
| 								}));
 | |
| 							} else {
 | |
| 								winston.warn('[plugins] "callbacked" deprecated as of 0.4x. Use asynchronous method instead for hook: ' + hook);
 | |
| 								value = hookObj.method.apply(Plugins, value);
 | |
| 								next(null, [value]);
 | |
| 							}
 | |
| 						} else {
 | |
| 							if (global.env === 'development') {
 | |
| 								winston.info('[plugins] Expected method for hook \'' + hook + '\' in plugin \'' + hookObj.id + '\' not found, skipping.');
 | |
| 							}
 | |
| 							next(null, [value]);
 | |
| 						}
 | |
| 					}, function(err, values) {
 | |
| 						if (err) {
 | |
| 							if (global.env === 'development') {
 | |
| 								winston.info('[plugins] Problem executing hook: ' + hook);
 | |
| 							}
 | |
| 						}
 | |
| 
 | |
| 						callback.apply(Plugins, [err].concat(values));
 | |
| 					});
 | |
| 					break;
 | |
| 				case 'action':
 | |
| 					async.each(hookList, function(hookObj) {
 | |
| 						if (hookObj.method) {
 | |
| 							hookObj.method.apply(Plugins, args);
 | |
| 						} else {
 | |
| 							if (global.env === 'development') {
 | |
| 								winston.info('[plugins] Expected method \'' + hookObj.method + '\' in plugin \'' + hookObj.id + '\' not found, skipping.');
 | |
| 							}
 | |
| 						}
 | |
| 					});
 | |
| 					break;
 | |
| 				default:
 | |
| 					// Do nothing...
 | |
| 					break;
 | |
| 			}
 | |
| 		} else {
 | |
| 			// Otherwise, this hook contains no methods
 | |
| 			if (callback) {
 | |
| 				callback.apply(this, [null].concat(args));
 | |
| 			}
 | |
| 
 | |
| 			return args[0];
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Plugins.isActive = function(id, callback) {
 | |
| 		db.isSetMember('plugins:active', id, callback);
 | |
| 	};
 | |
| 
 | |
| 	Plugins.toggleActive = function(id, callback) {
 | |
| 		Plugins.isActive(id, function(err, active) {
 | |
| 			if (err) {
 | |
| 				if (global.env === 'development') winston.info('[plugins] Could not toggle active state on plugin \'' + id + '\'');
 | |
| 				return;
 | |
| 			}
 | |
| 
 | |
| 			db[(active ? 'setRemove' : 'setAdd')]('plugins:active', id, function(err, success) {
 | |
| 				if (err) {
 | |
| 					if (global.env === 'development') winston.info('[plugins] Could not toggle active state on plugin \'' + id + '\'');
 | |
| 					return;
 | |
| 				}
 | |
| 
 | |
| 
 | |
| 				if(active) {
 | |
| 					Plugins.fireHook('action:plugin.deactivate', id);
 | |
| 				}
 | |
| 
 | |
| 				// Reload meta data
 | |
| 				Plugins.reload(function() {
 | |
| 
 | |
| 					if(!active) {
 | |
| 						Plugins.fireHook('action:plugin.activate', id);
 | |
| 					}
 | |
| 
 | |
| 					if (callback) {
 | |
| 						callback({
 | |
| 							id: id,
 | |
| 							active: !active
 | |
| 						});
 | |
| 					}
 | |
| 				});
 | |
| 			});
 | |
| 		});
 | |
| 	};
 | |
| 
 | |
| 	Plugins.getTemplates = function(callback) {
 | |
| 		var templates = {};
 | |
| 
 | |
| 		Plugins.showInstalled(function(err, plugins) {
 | |
| 			async.each(plugins, function(plugin, next) {
 | |
| 				if (plugin.templates && plugin.id) {
 | |
| 					var templatesPath = path.join(__dirname, '../node_modules', plugin.id, plugin.templates);
 | |
| 					utils.walk(templatesPath, function(err, pluginTemplates) {
 | |
| 						pluginTemplates.forEach(function(pluginTemplate) {
 | |
| 							templates[pluginTemplate.replace(templatesPath, '').substring(1)] = pluginTemplate;
 | |
| 						});
 | |
| 
 | |
| 						next(err);
 | |
| 					});
 | |
| 				} else {
 | |
| 					next(false);
 | |
| 				}
 | |
| 			}, function(err) {
 | |
| 				callback(err, templates);
 | |
| 			});
 | |
| 		});
 | |
| 	};
 | |
| 
 | |
| 	Plugins.showInstalled = function(callback) {
 | |
| 		npmPluginPath = path.join(__dirname, '../node_modules');
 | |
| 
 | |
| 		async.waterfall([
 | |
| 			function(next) {
 | |
| 				fs.readdir(npmPluginPath, function(err, dirs) {
 | |
| 					dirs = dirs.map(function(file) {
 | |
| 						return path.join(npmPluginPath, file);
 | |
| 					}).filter(function(file) {
 | |
| 						if (fs.existsSync(file)) {
 | |
| 							var stats = fs.statSync(file),
 | |
| 								isPlugin =  file.substr(npmPluginPath.length + 1, 14) === 'nodebb-plugin-' || file.substr(npmPluginPath.length + 1, 14) === 'nodebb-widget-';
 | |
| 
 | |
| 							if (stats.isDirectory() && isPlugin) return true;
 | |
| 							else return false;
 | |
| 						} else {
 | |
| 							return false;
 | |
| 						}
 | |
| 					});
 | |
| 
 | |
| 					next(err, dirs);
 | |
| 				});
 | |
| 			},
 | |
| 			function(files, next) {
 | |
| 				var plugins = [];
 | |
| 
 | |
| 				async.each(files, function(file, next) {
 | |
| 					var configPath;
 | |
| 
 | |
| 					async.waterfall([
 | |
| 						function(next) {
 | |
| 							fs.readFile(path.join(file, 'plugin.json'), next);
 | |
| 						},
 | |
| 						function(configJSON, next) {
 | |
| 							try {
 | |
| 								var config = JSON.parse(configJSON);
 | |
| 							} catch (err) {
 | |
| 								winston.warn("Plugin: " + file + " is corrupted or invalid. Please check plugin.json for errors.")
 | |
| 								return next(err, null);
 | |
| 							}
 | |
| 
 | |
| 							Plugins.isActive(config.id, function(err, active) {
 | |
| 								if (err) {
 | |
| 									next(new Error('no-active-state'));
 | |
| 								}
 | |
| 
 | |
| 								delete config.library;
 | |
| 								delete config.hooks;
 | |
| 								config.active = active;
 | |
| 								config.activeText = '<i class="fa fa-power-off"></i> ' + (active ? 'Dea' : 'A') + 'ctivate';
 | |
| 								next(null, config);
 | |
| 							});
 | |
| 						}
 | |
| 					], function(err, config) {
 | |
| 						if (err) return next(); // Silently fail
 | |
| 
 | |
| 						plugins.push(config);
 | |
| 						next();
 | |
| 					});
 | |
| 				}, function(err) {
 | |
| 					next(null, plugins);
 | |
| 				});
 | |
| 			}
 | |
| 		], function(err, plugins) {
 | |
| 			callback(err, plugins);
 | |
| 		});
 | |
| 	};
 | |
| }(exports));
 |