Merge branch 'plugins'

Conflicts:
	src/posts.js
v1.18.x
Julian Lam 12 years ago
commit 235553eaf6

1
.gitignore vendored

@ -11,3 +11,4 @@ public/css/*.css
public/themes/*
*.sublime-project
*.sublime-workspace
plugins/*

@ -57,6 +57,7 @@ if (!nconf.get('setup') && nconf.get('base_url')) {
templates = require('./public/src/templates.js'),
webserver = require('./src/webserver.js'),
websockets = require('./src/websockets.js'),
plugins = require('./src/plugins'),
admin = {
'categories': require('./src/admin/categories.js')
};

@ -141,4 +141,22 @@
}
}
}
.plugins {
li {
list-style-type: none;
background: rgba(64, 64, 64, 0.05);
padding: 1em;
border-left: 5px solid #08c;
h2 {
font-size: 16px;
margin: 0;
}
p {
font-size: 12px;
}
}
}
}

@ -0,0 +1,38 @@
var nodebb_admin = nodebb_admin || {};
(function() {
var plugins = {
init: function() {
var pluginsList = $('.plugins'),
numPlugins = pluginsList[0].querySelectorAll('li').length,
pluginID, pluginTgl;
if (numPlugins > 0) {
pluginsList.on('click', 'button[data-action="toggleActive"]', function() {
pluginID = $(this).parents('li').attr('data-plugin-id');
socket.emit('api:admin.plugins.toggle', pluginID);
});
socket.on('api:admin.plugins.toggle', function(status) {
pluginTgl = document.querySelector('.plugins li[data-plugin-id="' + status.id + '"] button');
pluginTgl.innerHTML = '<i class="icon-off"></i> ' + (status.active ? 'Dea' : 'A') + 'ctivate';
app.alert({
alert_id: 'plugin_toggled_' + status.id,
title: 'Plugin Enabled',
message: 'You may need to restart NodeBB in order for these changes to be reflected.',
type: 'notify',
timeout: 5000
})
});
} else {
pluginsList.append('<li><p><i>No plugins found.</i></p></li>');
}
}
};
jQuery(document).ready(function() {
nodebb_admin.plugins = plugins;
nodebb_admin.plugins.init();
});
})();

@ -74,6 +74,7 @@
<li class=''><a href='{relative_path}/admin/users/latest'><i class='icon-user'></i> Users</a></li>
<li class=''><a href='{relative_path}/admin/topics'><i class='icon-book'></i> Topics</a></li>
<li class=''><a href='{relative_path}/admin/themes'><i class='icon-th'></i> Themes</a></li>
<li class=''><a href='{relative_path}/admin/plugins'><i class='icon-code-fork'></i> Plugins</a></li>
<li class=''><a href='{relative_path}/admin/settings'><i class='icon-cogs'></i> Settings</a></li>
<li class=''><a href='{relative_path}/admin/redis'><i class='icon-hdd'></i> Redis</a></li>
<li class=''><a href="{relative_path}/admin/motd"><i class="icon-comment"></i> MOTD</a></li>

@ -0,0 +1,25 @@
<h1>Plugins</h1>
<ul class="plugins">
<!-- BEGIN plugins -->
<li data-plugin-id="{plugins.id}">
<h2>{plugins.name}</h2>
<div class="pull-right">
<button data-action="toggleActive" class="btn btn-primary">{plugins.activeText}</button>
</div>
<p>{plugins.description}</p>
<p>For more information: <a href="{plugins.url}">{plugins.url}</a></p>
</li>
<!-- END plugins -->
</ul>
<div class="alert">
<p>
<strong>Interesed in writing plugins for NodeBB?</strong>
</p>
<p>
Full documentation regarding plugin authoring can be found in the <a target="_blank" href="https://github.com/designcreateplay/NodeBB/wiki/Writing-Plugins-for-NodeBB">NodeBB Wiki</a>.
</p>
</div>
<script type="text/javascript" src="{relative_path}/src/forum/admin/plugins.js"></script>

@ -7,6 +7,7 @@
"admin/redis[^]*": "admin/redis",
"admin/index[^]*": "admin/index",
"admin/themes[^]*": "admin/themes",
"admin/plugins[^]*": "admin/plugins",
"admin/settings[^]*": "admin/settings",
"admin/twitter[^]*": "admin/twitter",
"admin/facebook[^]*": "admin/facebook",

@ -0,0 +1,203 @@
var fs = require('fs'),
path = require('path'),
RDB = require('./redis.js'),
async = require('async'),
plugins = {
libraries: [],
loadedHooks: {},
init: function() {
if (this.initialized) return;
if (global.env === 'development') console.log('Info: [plugins] Initializing plugins system');
var _self = this;
// Read the list of activated plugins and require their libraries
async.waterfall([
function(next) {
RDB.smembers('plugins:active', next);
},
function(plugins, next) {
async.each(plugins, function(plugin) {
// TODO: Update this check to also check node_modules
var pluginPath = path.join(__dirname, '../plugins/', plugin);
fs.exists(pluginPath, function(exists) {
if (exists) {
fs.readFile(path.join(pluginPath, 'plugin.json'), function(err, data) {
if (err) return next(err);
var pluginData = JSON.parse(data);
_self.libraries[pluginData.id] = require(path.join(pluginPath, pluginData.library));
if (pluginData.hooks) {
for(var x=0,numHooks=pluginData.hooks.length;x<numHooks;x++) {
_self.registerHook(pluginData.id, pluginData.hooks[x]);
}
}
if (global.env === 'development') console.log('Info: [plugins] Loaded plugin: ' + pluginData.id);
next();
});
} else {
if (global.env === 'development') console.log('Info: [plugins] Plugin \'' + plugin + '\' not found');
next(); // Ignore this plugin silently
}
})
}, next);
}
], function(err) {
if (err) {
if (global.env === 'development') console.log('Info: [plugins] NodeBB encountered a problem while loading plugins', err.message);
return;
}
if (global.env === 'development') console.log('Info: [plugins] Plugins OK');
});
},
initialized: false,
registerHook: function(id, data) {
/*
`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)
(Not implemented) `data.priority`, the relative priority of the method when it is eventually called (default: 10)
*/
var _self = this;
if (data.hook && data.method) {
_self.loadedHooks[data.hook] = _self.loadedHooks[data.hook] || [];
_self.loadedHooks[data.hook].push([id, data.method]);
if (global.env === 'development') console.log('Info: [plugins] Hook registered: ' + data.hook + ' will call ' + id);
} else return;
},
fireHook: function(hook, args, callback) {
// TODO: Implement priority hook firing
var _self = this
hookList = this.loadedHooks[hook];
if (hookList && Array.isArray(hookList)) {
if (global.env === 'development') console.log('Info: [plugins] Firing hook: \'' + hook + '\'');
var hookType = hook.split(':')[0];
switch(hookType) {
case 'filter':
// Filters only take one argument, so only args[0] will be passed in
var returnVal = (Array.isArray(args) ? args[0] : args);
async.each(hookList, function(hookObj, next) {
if (hookObj.callbacked) {
_self.libraries[hookObj[0]][hookObj[1]](returnVal, function(err, afterVal) {
returnVal = afterVal;
next(err);
});
} else {
returnVal = _self.libraries[hookObj[0]][hookObj[1]](returnVal);
next();
}
}, function(err) {
if (err) {
if (global.env === 'development') console.log('Info: [plugins] Problem executing hook: ' + hook);
}
callback(returnVal);
});
break;
case 'action':
async.each(hookList, function(hookObj) {
if (
_self.libraries[hookObj[0]] &&
_self.libraries[hookObj[0]][hookObj[1]] &&
typeof _self.libraries[hookObj[0]][hookObj[1]] === 'function'
) {
_self.libraries[hookObj[0]][hookObj[1]].apply(_self.libraries[hookObj[0]], args);
} else {
if (global.env === 'development') console.log('Info: [plugins] Expected method \'' + hookObj[1] + '\' in plugin \'' + hookObj[0] + '\' not found, skipping.');
}
});
break;
default:
// Do nothing...
break;
}
} else {
// Otherwise, this hook contains no methods
var returnVal = (Array.isArray(args) ? args[0] : args);
if (callback) callback(returnVal);
}
},
isActive: function(id, callback) {
RDB.sismember('plugins:active', id, callback);
},
toggleActive: function(id, callback) {
this.isActive(id, function(err, active) {
if (err) {
if (global.env === 'development') console.log('Info: [plugins] Could not toggle active state on plugin \'' + id + '\'');
return;
}
RDB[(active ? 'srem' : 'sadd')]('plugins:active', id, function(err, success) {
if (err) {
if (global.env === 'development') console.log('Info: [plugins] Could not toggle active state on plugin \'' + id + '\'');
return;
}
callback({
id: id,
active: !active
});
});
});
},
showInstalled: function(callback) {
// TODO: Also check /node_modules
var _self = this;
moduleBasePath = path.join(__dirname, '../plugins');
async.waterfall([
function(next) {
fs.readdir(moduleBasePath, next);
},
function(files, next) {
var plugins = [];
async.each(files, function(file, next) {
var modulePath = path.join(moduleBasePath, file),
configPath;
async.waterfall([
function(next) {
fs.stat(path.join(moduleBasePath, file), next);
},
function(stats, next) {
if (stats.isDirectory()) fs.readFile(path.join(modulePath, 'plugin.json'), next);
else next(new Error('not-a-directory'));
},
function(configJSON, next) {
var config = JSON.parse(configJSON);
_self.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="icon-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);
});
}
}
plugins.init();
module.exports = plugins;

@ -5,7 +5,8 @@ var RDB = require('./redis.js'),
user = require('./user.js'),
async = require('async'),
utils = require('../public/src/utils');
utils = require('../public/src/utils'),
plugins = require('./plugins');
(function(PostTools) {
PostTools.isMain = function(pid, tid, callback) {
@ -71,7 +72,10 @@ var RDB = require('./redis.js'),
PostTools.privileges(pid, uid, function(privileges) {
if (privileges.editable) {
plugins.fireHook('filter:save_post_content', content, function(parsedContent) {
content = parsedContent;
success();
});
}
});
}

@ -7,7 +7,8 @@ var RDB = require('./redis.js'),
threadTools = require('./threadTools.js'),
postTools = require('./postTools'),
feed = require('./feed.js'),
async = require('async');
async = require('async'),
plugins = require('./plugins');
(function(Posts) {
@ -275,14 +276,13 @@ var RDB = require('./redis.js'),
}
topics.isLocked(tid, function(locked) {
if (!locked || locked === '0') {
RDB.incr('global:next_post_id', function(err, pid) {
RDB.handle(err);
var timestamp = Date.now();
var postData = {
plugins.fireHook('filter:save_post_content', content, function(content) {
var timestamp = Date.now(),
postData = {
'pid': pid,
'uid': uid,
'tid': tid,
@ -307,7 +307,7 @@ var RDB = require('./redis.js'),
feed.updateTopic(tid, cid);
RDB.zadd('categories:recent_posts:cid:' + cid, timestamp, pid);
RDB.zadd('categories:recent_posts:cid:' + cid, Date.now(), pid);
// this is a bit of a naive implementation, defn something to look at post-MVP
RDB.scard('cid:' + cid + ':active_users', function(amount) {
@ -343,6 +343,8 @@ var RDB = require('./redis.js'),
});
}
plugins.fireHook('action:save_post_content', [pid, content]);
if(!images) {
postData.uploadedImages = JSON.stringify(uploadedImages);
Posts.setPostField(pid, 'uploadedImages', postData.uploadedImages);
@ -361,6 +363,7 @@ var RDB = require('./redis.js'),
});
}
});
});
} else {
callback(null);
}

@ -3,7 +3,8 @@ var user = require('./../user.js'),
topics = require('./../topics.js'),
RDB = require('./../redis.js'),
pkg = require('./../../package.json'),
categories = require('./../categories.js');
categories = require('./../categories.js'),
plugins = require('../plugins');
(function(Admin) {
Admin.isAdmin = function(req, res, next) {
@ -23,8 +24,12 @@ var user = require('./../user.js'),
Admin.create_routes = function(app) {
(function() {
var routes = ['categories', 'users', 'topics', 'settings', 'themes', 'twitter', 'facebook', 'gplus', 'redis', 'motd',
'users/latest', 'users/sort-posts', 'users/sort-reputation', 'users/search'];
var routes = [
'categories', 'users', 'topics', 'settings', 'themes',
'twitter', 'facebook', 'gplus', 'redis', 'motd',
'users/latest', 'users/sort-posts', 'users/sort-reputation',
'users/search', 'plugins'
];
for (var i=0, ii=routes.length; i<ii; i++) {
(function(route) {
@ -136,6 +141,15 @@ var user = require('./../user.js'),
res.json(finalData);
});
break;
case 'plugins':
plugins.showInstalled(function(err, plugins) {
if (err || !Array.isArray(plugins)) plugins = [];
res.json(200, {
plugins: plugins
});
});
break;
default :
res.json({});
}

@ -25,7 +25,8 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
admin = {
'categories': require('./admin/categories.js'),
'user': require('./admin/user.js')
};
},
plugins = require('./plugins');
(function(io) {
var users = {},
@ -644,6 +645,12 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
socket.emit('api:admin:themes.getInstalled', themeArr);
});
});
socket.on('api:admin.plugins.toggle', function(plugin_id) {
plugins.toggleActive(plugin_id, function(status) {
socket.emit('api:admin.plugins.toggle', status);
});
});
});
}(SocketIO));

Loading…
Cancel
Save