Merge remote-tracking branch 'origin/master' into mongodb-3.0-driver-2.0

This commit is contained in:
Barış Soner Uşaklı
2015-03-13 13:14:43 -04:00
152 changed files with 1790 additions and 1314 deletions

View File

@@ -14,15 +14,16 @@ module.exports = function(Categories) {
}
var slug = cid + '/' + utils.slugify(data.name),
order = data.order || cid; // If no order provided, place it at the end
order = data.order || cid, // If no order provided, place it at the end
colours = Categories.assignColours();
var category = {
cid: cid,
name: data.name,
description: data.description,
icon: data.icon,
bgColor: data.bgColor,
color: data.color,
bgColor: data.bgColor || colours[0],
color: data.color || colours[1],
slug: slug,
parentCid: 0,
topic_count: 0,
@@ -52,4 +53,12 @@ module.exports = function(Categories) {
});
});
};
Categories.assignColours = function() {
var backgrounds = ['#AB4642', '#DC9656', '#F7CA88', '#A1B56C', '#86C1B9', '#7CAFC2', '#BA8BAF', '#A16946'],
text = ['#fff', '#fff', '#333', '#fff', '#333', '#fff', '#fff', '#fff'],
index = Math.floor(Math.random() * backgrounds.length);
return [backgrounds[index], text[index]];
};
};

View File

@@ -31,7 +31,7 @@ uploadsController.uploadFavicon = function(req, res, next) {
var allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon'];
if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
file.saveFileToLocal('favicon.ico', 'files', uploadedFile.path, function(err, image) {
file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function(err, image) {
fs.unlink(uploadedFile.path);
if (err) {
return next(err);
@@ -55,7 +55,7 @@ function upload(name, req, res, next) {
var allowedTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif'];
if (validateUpload(req, res, next, uploadedFile, allowedTypes)) {
var filename = name + path.extname(uploadedFile.name);
uploadImage(filename, 'files', uploadedFile, req, res, next);
uploadImage(filename, 'system', uploadedFile, req, res, next);
}
}

View File

@@ -51,7 +51,7 @@ apiController.getConfig = function(req, res, next) {
config.disableSocialButtons = parseInt(meta.config.disableSocialButtons, 10) === 1;
config.disableChat = parseInt(meta.config.disableChat, 10) === 1;
config.maxReconnectionAttempts = meta.config.maxReconnectionAttempts || 5;
config.reconnectionDelay = meta.config.reconnectionDelay || 200;
config.reconnectionDelay = meta.config.reconnectionDelay || 1500;
config.tagsPerTopic = meta.config.tagsPerTopic || 5;
config.minimumTagLength = meta.config.minimumTagLength || 3;
config.maximumTagLength = meta.config.maximumTagLength || 15;

View File

@@ -55,7 +55,7 @@ categoriesController.popular = function(req, res, next) {
var data = {
topics: topics,
'feeds:disableRSS': parseInt(meta.config['feeds:disableRSS'], 10) === 1,
rssFeedUrl: nconf.get('relative_path') + '/popular.rss',
rssFeedUrl: nconf.get('relative_path') + '/popular/' + (req.params.term || 'daily') + '.rss',
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[global:header.popular]]'}])
};
@@ -200,8 +200,7 @@ categoriesController.get = function(req, res, next) {
var topicCount = parseInt(results.categoryData.topic_count, 10);
if (topicIndex < 0 || topicIndex > Math.max(topicCount - 1, 0)) {
var url = '/category/' + cid + '/' + req.params.slug + (topicIndex > topicCount ? '/' + topicCount : '');
return res.locals.isAPI ? res.status(302).json(url) : res.redirect(url);
return helpers.redirect(res, '/category/' + cid + '/' + req.params.slug + (topicIndex > topicCount ? '/' + topicCount : ''));
}
userPrivileges = results.privileges;

View File

@@ -51,31 +51,35 @@ groupsController.details = function(req, res, next) {
}
}
], function(err, ok) {
if (ok) {
async.parallel({
group: function(next) {
groups.get(res.locals.groupName, {
expand: true,
uid: uid
}, next);
},
posts: function(next) {
groups.getLatestMemberPosts(res.locals.groupName, 10, uid, next);
}
}, function(err, results) {
if (err) {
return next(err);
}
if (!results.group) {
return helpers.notFound(req, res);
}
res.render('groups/details', results);
});
} else {
return res.locals.isAPI ? res.status(302).json('/groups') : res.redirect('/groups');
if (err) {
return next(err);
}
if (!ok) {
return helpers.redirect(res, '/groups');
}
async.parallel({
group: function(next) {
groups.get(res.locals.groupName, {
expand: true,
uid: uid
}, next);
},
posts: function(next) {
groups.getLatestMemberPosts(res.locals.groupName, 10, uid, next);
}
}, function(err, results) {
if (err) {
return next(err);
}
if (!results.group) {
return helpers.notFound(req, res);
}
res.render('groups/details', results);
});
});
};

View File

@@ -6,12 +6,19 @@ var nconf = require('nconf'),
translator = require('../../public/src/translator'),
categories = require('../categories'),
plugins = require('../plugins'),
meta = require('../meta');
var helpers = {};
helpers.notFound = function(req, res, error) {
if (res.locals.isAPI) {
if (plugins.hasListeners('action:meta.override404')) {
plugins.fireHook('action:meta.override404', {
req: req,
res: res,
error: error
});
} else if (res.locals.isAPI) {
res.status(404).json({path: req.path.replace(/^\/api/, ''), error: error});
} else {
res.status(404).render('404', {path: req.path, error: error});
@@ -38,6 +45,14 @@ helpers.notAllowed = function(req, res, error) {
}
};
helpers.redirect = function(res, url) {
if (res.locals.isAPI) {
res.status(302).json(url);
} else {
res.redirect(nconf.get('relative_path') + url);
}
};
helpers.buildCategoryBreadcrumbs = function(cid, callback) {
var breadcrumbs = [];

View File

@@ -1,90 +0,0 @@
"use strict";
var async = require('async'),
nconf = require('nconf'),
fs = require('fs'),
path = require('path'),
meta = require('../meta'),
plugins = require('../plugins'),
utils = require('../../public/src/utils'),
templatesController = {};
var availableTemplatesCache = null;
var configCache = null;
templatesController.getTemplatesListing = function(req, res, next) {
async.parallel({
availableTemplates: function(next) {
getAvailableTemplates(next);
},
templatesConfig: function(next) {
async.waterfall([
function(next) {
readConfigFile(next);
},
function(config, next) {
config.custom_mapping['^/?$'] = meta.config.homePageRoute || 'categories';
plugins.fireHook('filter:templates.get_config', config, next);
}
], next);
},
}, function(err, results) {
if (err) {
return next(err);
}
res.json(results);
});
};
function readConfigFile(callback) {
if (configCache) {
return callback(null, configCache);
}
fs.readFile(path.join(nconf.get('views_dir'), 'config.json'), function(err, config) {
if (err) {
return callback(err);
}
try {
config = JSON.parse(config.toString());
} catch (err) {
return callback(err);
}
configCache = config;
callback(null, config);
});
}
function getAvailableTemplates(callback) {
if (availableTemplatesCache) {
return callback(null, availableTemplatesCache);
}
async.parallel({
views: function(next) {
utils.walk(nconf.get('views_dir'), next);
},
extended: function(next) {
plugins.fireHook('filter:templates.get_virtual', [], next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
var availableTemplates = results.views.filter(function(value, index, self) {
return value && self.indexOf(value) === index;
}).map(function(el) {
return el && el.replace(nconf.get('views_dir') + '/', '');
});
availableTemplatesCache = availableTemplates.concat(results.extended);
callback(null, availableTemplatesCache);
});
}
module.exports = templatesController;

View File

@@ -56,12 +56,8 @@ topicsController.get = function(req, res, next) {
var postCount = parseInt(results.topic.postcount, 10);
var pageCount = Math.max(1, Math.ceil((postCount - 1) / settings.postsPerPage));
if (utils.isNumber(req.params.post_index)) {
var url = '';
if (req.params.post_index < 1 || req.params.post_index > postCount) {
url = '/topic/' + req.params.topic_id + '/' + req.params.slug + (req.params.post_index > postCount ? '/' + postCount : '');
return res.locals.isAPI ? res.status(302).json(url) : res.redirect(url);
}
if (utils.isNumber(req.params.post_index) && (req.params.post_index < 1 || req.params.post_index > postCount)) {
return helpers.redirect(res, '/topic/' + req.params.topic_id + '/' + req.params.slug + (req.params.post_index > postCount ? '/' + postCount : ''));
}
if (settings.usePagination && (req.query.page < 1 || req.query.page > pageCount)) {
@@ -266,7 +262,7 @@ topicsController.get = function(req, res, next) {
});
topics.increaseViewCount(tid);
plugins.fireHook('filter:topic.build', {req: req, res: res, templateData: data}, function(err, data) {
if (err) {
return next(err);

View File

@@ -5,6 +5,7 @@ var usersController = {};
var async = require('async'),
user = require('../user'),
meta = require('../meta'),
pagination = require('../pagination'),
plugins = require('../plugins'),
db = require('../database');
@@ -67,11 +68,13 @@ usersController.getUsers = function(set, count, req, res, next) {
if (err) {
return next(err);
}
var pageCount = Math.ceil(data.count / (parseInt(meta.config.userSearchResultsPerPage, 10) || 20));
var userData = {
search_display: 'hidden',
loadmore_display: data.count > count ? 'block' : 'hide',
users: data.users,
show_anon: 'hide'
show_anon: 'hide',
pagination: pagination.create(1, pageCount)
};
res.render('users', userData);
@@ -94,7 +97,7 @@ function getUsersAndCount(set, uid, count, callback) {
return user && parseInt(user.uid, 10);
});
callback(null, {users: results.users, count: results.count});
callback(null, results);
});
}

View File

@@ -204,14 +204,17 @@ module.exports = function(db, module) {
if (!key) {
return callback();
}
var scoreQuery = {};
var query = {_key: key};
if (min !== '-inf') {
scoreQuery.$gte = min;
query.score = {$gte: min};
}
if (max !== '+inf') {
scoreQuery.$lte = max;
query.score = query.score || {};
query.score.$lte = max;
}
db.collection('objects').count({_key: key, score: scoreQuery}, function(err, count) {
db.collection('objects').count(query, function(err, count) {
callback(err, count ? count : 0);
});
};

View File

@@ -27,10 +27,7 @@ file.saveFileToLocal = function(filename, folder, tempPath, callback) {
});
});
os.on('error', function (err) {
winston.error(err.message);
callback(err);
});
os.on('error', callback);
is.pipe(os);
};

View File

@@ -16,6 +16,7 @@ var async = require('async'),
posts = require('./posts'),
privileges = require('./privileges'),
utils = require('../public/src/utils'),
util = require('util'),
uploadsController = require('./controllers/uploads');
@@ -1084,11 +1085,51 @@ var async = require('async'),
case 'alpha': // intentional fall-through
default:
groups = groups.sort(function(a, b) {
return a.slug > b.slug;
return a.slug > b.slug ? 1 : -1;
});
}
next(null, groups);
};
Groups.searchMembers = function(data, callback) {
function findUids(query, searchBy, startsWith, callback) {
if (!query) {
return Groups.getMembers(data.groupName, 0, -1, callback);
}
async.waterfall([
function(next) {
Groups.getMembers(data.groupName, 0, -1, next);
},
function(members, next) {
user.getMultipleUserFields(members, ['uid'].concat(searchBy), next);
},
function(users, next) {
var uids = [];
for(var k=0; k<searchBy.length; ++k) {
for(var i=0; i<users.length; ++i) {
var field = users[i][searchBy[k]];
if ((startsWith && field.toLowerCase().startsWith(query)) || (!startsWith && field.toLowerCase().indexOf(query) !== -1)) {
uids.push(users[i].uid);
}
}
}
if (searchBy.length > 1) {
uids = uids.filter(function(uid, index, array) {
return array.indexOf(uid) === index;
});
}
next(null, uids);
}
], callback);
}
data.findUids = findUids;
user.search(data, callback);
};
}(module.exports));

View File

@@ -38,6 +38,7 @@ module.exports = function(Meta) {
'public/vendor/xregexp/unicode/unicode-base.js',
'public/vendor/buzz/buzz.min.js',
'public/vendor/mousetrap/mousetrap.js',
'public/vendor/autosize.js',
'./node_modules/templates.js/lib/templates.js',
'public/src/utils.js',
'public/src/app.js',
@@ -209,17 +210,24 @@ module.exports = function(Meta) {
Meta.js.getFromFile = function(minify, callback) {
var scriptPath = path.join(__dirname, '../../public/nodebb.min.js'),
mapPath = path.join(__dirname, '../../public/nodebb.min.js.map');
mapPath = path.join(__dirname, '../../public/nodebb.min.js.map'),
paths = [scriptPath];
fs.exists(scriptPath, function(exists) {
if (exists) {
if (nconf.get('isPrimary') === 'true') {
winston.verbose('[meta/js] Reading client-side scripts from file');
async.map([scriptPath, mapPath], fs.readFile, function(err, files) {
Meta.js.cache = files[0];
Meta.js.map = files[1];
fs.exists(mapPath, function(exists) {
if (exists) {
paths.push(mapPath);
}
emitter.emit('meta:js.compiled');
callback();
winston.verbose('[meta/js] Reading client-side scripts from file');
async.map(paths, fs.readFile, function(err, files) {
Meta.js.cache = files[0];
Meta.js.map = files[1] || '';
emitter.emit('meta:js.compiled');
callback();
});
});
} else {
callback();

View File

@@ -32,77 +32,86 @@ middleware.isAdmin = function(req, res, next) {
};
middleware.buildHeader = function(req, res, next) {
res.locals.renderAdminHeader = true;
async.parallel({
config: function(next) {
controllers.api.getConfig(req, res, next);
},
footer: function(next) {
app.render('admin/footer', {}, next);
}
}, function(err, results) {
if (err) {
return next(err);
}
res.locals.config = results.config;
res.locals.adminFooter = results.footer;
next();
});
};
middleware.renderHeader = function(req, res, next) {
var uid = req.user ? req.user.uid : 0;
async.parallel([
function(next) {
var custom_header = {
'plugins': [],
'authentication': []
};
user.getUserFields(uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function(err, userData) {
if (err) {
return next(err);
}
var custom_header = {
'plugins': [],
'authentication': []
};
userData.uid = uid;
userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1;
async.parallel({
scripts: function(next) {
plugins.fireHook('filter:admin.scripts.get', [], function(err, scripts) {
if (err) {
return next(err);
}
var arr = [];
scripts.forEach(function(script) {
arr.push({src: nconf.get('url') + script});
});
user.getUserFields(uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function(err, userData) {
if (err) {
return next(err);
}
next(null, arr);
});
},
custom_header: function(next) {
plugins.fireHook('filter:admin.header.build', custom_header, next);
},
config: function(next) {
controllers.api.getConfig(req, res, next);
}
}, function(err, results) {
userData.uid = uid;
userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1;
async.parallel({
scripts: function(next) {
plugins.fireHook('filter:admin.scripts.get', [], function(err, scripts) {
if (err) {
return next(err);
}
res.locals.config = results.config;
var data = {
relative_path: nconf.get('relative_path'),
configJSON: JSON.stringify(results.config),
user: userData,
userJSON: JSON.stringify(userData),
plugins: results.custom_header.plugins,
authentication: results.custom_header.authentication,
scripts: results.scripts,
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
env: process.env.NODE_ENV ? true : false
};
app.render('admin/header', data, function(err, template) {
if (err) {
return next(err);
}
res.locals.adminHeader = template;
next();
var arr = [];
scripts.forEach(function(script) {
arr.push({src: nconf.get('url') + script});
});
next(null, arr);
});
});
},
function(next) {
app.render('admin/footer', {}, function(err, template) {
res.locals.adminFooter = template;
next(err);
});
}
], next);
},
custom_header: function(next) {
plugins.fireHook('filter:admin.header.build', custom_header, next);
},
config: function(next) {
controllers.api.getConfig(req, res, next);
}
}, function(err, results) {
if (err) {
return next(err);
}
res.locals.config = results.config;
var data = {
relative_path: nconf.get('relative_path'),
configJSON: JSON.stringify(results.config),
user: userData,
userJSON: JSON.stringify(userData),
plugins: results.custom_header.plugins,
authentication: results.custom_header.authentication,
scripts: results.scripts,
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
env: process.env.NODE_ENV ? true : false,
};
data.template = {name: res.locals.template};
data.template[res.locals.template] = true;
app.render('admin/header', data, next);
});
});
};
module.exports = function(webserver) {

View File

@@ -60,19 +60,14 @@ middleware.redirectToAccountIfLoggedIn = function(req, res, next) {
if (err) {
return next(err);
}
if (res.locals.isAPI) {
res.status(302).json(nconf.get('relative_path') + '/user/' + userslug);
} else {
res.redirect(nconf.get('relative_path') + '/user/' + userslug);
}
controllers.helpers.redirect(res, '/user/' + userslug);
});
};
middleware.redirectToLoginIfGuest = function(req, res, next) {
if (!req.user || parseInt(req.user.uid, 10) === 0) {
req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, '');
return res.redirect(nconf.get('relative_path') + '/login');
return controllers.helpers.redirect(res, '/login');
} else {
next();
}
@@ -85,13 +80,7 @@ middleware.addSlug = function(req, res, next) {
return next(err);
}
var url = nconf.get('relative_path') + name + encodeURI(slug);
if (res.locals.isAPI) {
res.status(302).json(url);
} else {
res.redirect(url);
}
controllers.helpers.redirect(res, name + encodeURI(slug));
});
}
@@ -165,17 +154,9 @@ middleware.checkAccountPermissions = function(req, res, next) {
};
middleware.isAdmin = function(req, res, next) {
function render() {
if (res.locals.isAPI) {
return controllers.helpers.notAllowed(req, res);
}
middleware.buildHeader(req, res, function() {
controllers.helpers.notAllowed(req, res);
});
}
if (!req.user) {
return render();
req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, '');
return controllers.helpers.redirect(res, '/login');
}
user.isAdministrator((req.user && req.user.uid) ? req.user.uid : 0, function (err, isAdmin) {
@@ -183,7 +164,13 @@ middleware.isAdmin = function(req, res, next) {
return next(err);
}
render();
if (res.locals.isAPI) {
return controllers.helpers.notAllowed(req, res);
}
middleware.buildHeader(req, res, function() {
controllers.helpers.notAllowed(req, res);
});
});
};
@@ -284,7 +271,6 @@ middleware.renderHeader = function(req, res, callback) {
href: nconf.get('relative_path') + '/favicon.ico'
});
async.parallel({
customCSS: function(next) {
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1;
@@ -339,7 +325,7 @@ middleware.renderHeader = function(req, res, callback) {
results.user.isAdmin = results.isAdmin || false;
results.user.uid = parseInt(results.user.uid, 10);
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
templateValues.browserTitle = results.title;
templateValues.isAdmin = results.user.isAdmin;
templateValues.user = results.user;
@@ -348,6 +334,9 @@ middleware.renderHeader = function(req, res, callback) {
templateValues.customJS = results.customJS;
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
templateValues.template = {name: res.locals.template};
templateValues.template[res.locals.template] = true;
app.render('header', templateValues, callback);
});
});
@@ -376,8 +365,9 @@ middleware.processRender = function(req, res, next) {
}
options.loggedIn = req.user ? parseInt(req.user.uid, 10) !== 0 : false;
options.template = {};
options.template = {name: template};
options.template[template] = true;
res.locals.template = template;
if ('function' !== typeof fn) {
fn = defaultFn;
@@ -402,20 +392,18 @@ middleware.processRender = function(req, res, next) {
str = str + res.locals.adminFooter;
}
if (res.locals.renderHeader) {
middleware.renderHeader(req, res, function(err, template) {
if (res.locals.renderHeader || res.locals.renderAdminHeader) {
var method = res.locals.renderHeader ? middleware.renderHeader : middleware.admin.renderHeader;
method(req, res, function(err, template) {
if (err) {
return fn(err);
}
str = template + str;
var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB';
translator.translate(str, language, function(translated) {
fn(err, translated);
});
});
} else if (res.locals.adminHeader) {
str = res.locals.adminHeader + str;
var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB';
translator.translate(str, language, function(translated) {
fn(err, translated);
});
} else {
fn(err, str);
}
@@ -458,7 +446,6 @@ middleware.maintenanceMode = function(req, res, next) {
'/nodebb.min.js',
'/vendor/fontawesome/fonts/fontawesome-webfont.woff',
'/src/modules/[\\w]+\.js',
'/api/get_templates_listing',
'/api/login',
'/api/?',
'/language/.+'

View File

@@ -207,12 +207,12 @@ var async = require('async'),
Notifications.pushGroup = function(notification, groupName, callback) {
callback = callback || function() {};
groups.get(groupName, {}, function(err, groupObj) {
if (err || !groupObj || !Array.isArray(groupObj.members) || !groupObj.members.length) {
groups.getMembers(groupName, 0, -1, function(err, members) {
if (err || !Array.isArray(members) || !members.length) {
return callback(err);
}
Notifications.push(notification, groupObj.members, callback);
Notifications.push(notification, members, callback);
});
};

View File

@@ -168,7 +168,7 @@ var fs = require('fs'),
Plugins.getAll = function(callback) {
var url = (nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins?version=' + require('../package.json').version;
require('request')(url, function(err, res, body) {
var plugins = [];
@@ -262,9 +262,9 @@ var fs = require('fs'),
function(dirs, next) {
dirs = dirs.filter(function(dir){
return dir.startsWith('nodebb-plugin-') ||
dir.startsWith('nodebb-widget-') ||
dir.startsWith('nodebb-rewards-') ||
return dir.startsWith('nodebb-plugin-') ||
dir.startsWith('nodebb-widget-') ||
dir.startsWith('nodebb-rewards-') ||
dir.startsWith('nodebb-theme-');
}).map(function(dir){
return path.join(npmPluginPath, dir);
@@ -272,11 +272,7 @@ var fs = require('fs'),
async.filter(dirs, function(dir, callback){
fs.stat(dir, function(err, stats){
if (err) {
return callback(false);
}
callback(stats.isDirectory());
callback(!err && stats.isDirectory());
});
}, function(plugins){
next(null, plugins);
@@ -287,25 +283,11 @@ var fs = require('fs'),
var plugins = [];
async.each(files, function(file, next) {
var configPath;
async.waterfall([
function(next) {
Plugins.loadPluginInfo(file, next);
},
function(pluginData, next) {
var packageName = path.basename(file);
if (!pluginData) {
winston.warn("Plugin `" + packageName + "` is corrupted or invalid. Please check either package.json or plugin.json for errors.");
return next(null, {
id: packageName,
installed: true,
error: true,
active: null
});
}
Plugins.isActive(pluginData.name, function(err, active) {
if (err) {
return next(new Error('no-active-state'));
@@ -319,12 +301,12 @@ var fs = require('fs'),
next(null, pluginData);
});
}
], function(err, config) {
], function(err, pluginData) {
if (err) {
return next(); // Silently fail
}
plugins.push(config);
plugins.push(pluginData);
next();
});
}, function(err) {

View File

@@ -113,7 +113,16 @@ module.exports = function(Plugins) {
require('npm').load({}, next);
},
function(res, next) {
require('npm').commands.install([id + '@' + (version || 'latest')], next);
require('npm').commands.install([id + '@' + (version || 'latest')], function(err, a, b) {
next(err);
});
},
function(next) {
Plugins.isActive(id, next);
},
function(isActive, next) {
meta.reloadRequired = isActive;
next(null, isActive);
}
], callback);
}

View File

@@ -14,23 +14,13 @@ module.exports = function(Plugins) {
Plugins.loadPlugin = function(pluginPath, callback) {
Plugins.loadPluginInfo(pluginPath, function(err, pluginData) {
if (err) {
if (err.message === '[[error:parse-error]]') {
return callback();
}
return callback(pluginPath.match('nodebb-theme') ? null : err);
}
var staticDir;
if (!pluginData) {
return callback();
}
if (pluginData.compatibility && semver.validRange(pluginData.compatibility)) {
if (!semver.gtr(pkg.version, pluginData.compatibility)) {
// NodeBB may not be new enough to run this plugin
process.stdout.write('\n');
winston.warn('[plugins/' + pluginData.id + '] This plugin may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing.');
winston.warn('[plugins/' + pluginData.id + '] In the event of an unresponsive NodeBB caused by this plugin, run ./nodebb reset plugin="' + pluginData.id + '".');
process.stdout.write('\n');
}
}
versionWarning(pluginData);
async.parallel([
function(next) {
@@ -63,6 +53,23 @@ module.exports = function(Plugins) {
});
};
function versionWarning(pluginData) {
function display() {
process.stdout.write('\n');
winston.warn('[plugins/' + pluginData.id + '] This plugin may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing.');
winston.warn('[plugins/' + pluginData.id + '] In the event of an unresponsive NodeBB caused by this plugin, run ./nodebb reset plugin="' + pluginData.id + '".');
process.stdout.write('\n');
}
if (pluginData.nbbpm && pluginData.nbbpm.compatibility && semver.validRange(pluginData.nbbpm.compatibility)) {
if (!semver.gtr(pkg.version, pluginData.nbbpm.compatibility)) {
display();
}
} else {
display();
}
}
function registerHooks(pluginData, pluginPath, callback) {
function libraryNotFound() {
winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id);
@@ -220,9 +227,9 @@ module.exports = function(Plugins) {
var pluginDir = pluginPath.split(path.sep);
pluginDir = pluginDir[pluginDir.length -1];
winston.error('[plugins/' + pluginDir + '] Error in plugin.json/package.json! ' + err.message);
winston.error('[plugins/' + pluginDir + '] Error in plugin.json or package.json! ' + err.message);
callback();
callback(new Error('[[error:parse-error]]'));
}
});
};

View File

@@ -14,7 +14,14 @@ var winston = require('winston'),
utils = require('../public/src/utils'),
plugins = require('./plugins'),
events = require('./events'),
meta = require('./meta');
meta = require('./meta'),
LRU = require('lru-cache');
var cache = LRU({
max: 1048576,
length: function (n) { return n.length },
maxAge: 1000 * 60 * 60
});
(function(PostTools) {
@@ -100,6 +107,7 @@ var winston = require('winston'),
});
},
postData: function(next) {
cache.del(postData.pid);
PostTools.parsePost(postData, data.uid, next);
}
}, function(err, results) {
@@ -148,6 +156,7 @@ var winston = require('winston'),
}
if (isDelete) {
cache.del(postData.pid);
posts.delete(pid, callback);
} else {
posts.restore(pid, function(err, postData) {
@@ -165,7 +174,7 @@ var winston = require('winston'),
if (err || !canEdit) {
return callback(err || new Error('[[error:no-privileges]]'));
}
cache.del(pid);
posts.purge(pid, callback);
});
};
@@ -173,8 +182,18 @@ var winston = require('winston'),
PostTools.parsePost = function(postData, uid, callback) {
postData.content = postData.content || '';
var cachedContent = cache.get(postData.pid);
if (cachedContent) {
postData.content = cachedContent;
return callback(null, postData);
}
plugins.fireHook('filter:parse.post', {postData: postData, uid: uid}, function(err, data) {
callback(err, data ? data.postData : null);
if (err) {
return callback(err);
}
cache.set(data.postData.pid, data.postData.content);
callback(null, data.postData);
});
};
@@ -184,4 +203,8 @@ var winston = require('winston'),
plugins.fireHook('filter:parse.signature', {userData: userData, uid: uid}, callback);
};
PostTools.resetCache = function() {
cache.reset();
};
}(exports));

View File

@@ -16,6 +16,7 @@ var async = require('async'),
require('./posts/create')(Posts);
require('./posts/delete')(Posts);
require('./posts/user')(Posts);
require('./posts/topics')(Posts);
require('./posts/category')(Posts);
require('./posts/summary')(Posts);
require('./posts/recent')(Posts);
@@ -25,20 +26,6 @@ var async = require('async'),
db.isSortedSetMember('posts:pid', pid, callback);
};
Posts.getPostsByTid = function(tid, set, start, end, uid, reverse, callback) {
Posts.getPidsFromSet(set, start, end, reverse, function(err, pids) {
if(err) {
return callback(err);
}
if(!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
Posts.getPostsByPids(pids, uid, callback);
});
};
Posts.getPidsFromSet = function(set, start, end, reverse, callback) {
if (isNaN(start) || isNaN(end)) {
return callback(null, []);
@@ -243,17 +230,6 @@ var async = require('async'),
});
};
Posts.isMain = function(pid, callback) {
Posts.getPostField(pid, 'tid', function(err, tid) {
if (err) {
return callback(err);
}
topics.getTopicField(tid, 'mainPid', function(err, mainPid) {
callback(err, parseInt(pid, 10) === parseInt(mainPid, 10));
});
});
};
Posts.updatePostVoteCount = function(pid, voteCount, callback) {
async.parallel([
function(next) {

44
src/posts/topics.js Normal file
View File

@@ -0,0 +1,44 @@
'use strict';
var async = require('async'),
topics = require('../topics');
module.exports = function(Posts) {
Posts.getPostsByTid = function(tid, set, start, end, uid, reverse, callback) {
Posts.getPidsFromSet(set, start, end, reverse, function(err, pids) {
if (err) {
return callback(err);
}
if (!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
Posts.getPostsByPids(pids, uid, callback);
});
};
Posts.isMain = function(pid, callback) {
Posts.getPostField(pid, 'tid', function(err, tid) {
if (err) {
return callback(err);
}
topics.getTopicField(tid, 'mainPid', function(err, mainPid) {
callback(err, parseInt(pid, 10) === parseInt(mainPid, 10));
});
});
};
Posts.getTopicFields = function(pid, fields, callback) {
Posts.getPostField(pid, 'tid', function(err, tid) {
if (err) {
return callback(err);
}
topics.getTopicFields(tid, fields, callback);
});
};
};

View File

@@ -33,10 +33,18 @@ module.exports = function(Posts) {
var userData = results.userData;
userData.forEach(function(userData, i) {
userData.groups = results.groups[i];
userData.groups = [];
results.groups[i].forEach(function(group, index) {
group.selected = group.name === results.userSettings[i].groupTitle;
userData.groups[index] = {
name: group.name,
slug: group.slug,
labelColor: group.labelColor,
icon: group.icon,
userTitle: group.userTitle,
userTitleEnabled: group.userTitleEnabled,
selected: group.name === results.userSettings[i].groupTitle
};
});
userData.status = user.getStatus(userData.status, results.online[i]);
});

View File

@@ -3,19 +3,18 @@
var express = require('express');
function apiRoutes(app, middleware, controllers) {
// todo, needs to be in api namespace
app.get('/users/csv', middleware.authenticate, controllers.admin.users.getCSV);
function apiRoutes(router, middleware, controllers) {
router.get('/users/csv', middleware.authenticate, controllers.admin.users.getCSV);
var multipart = require('connect-multiparty');
var multipartMiddleware = multipart();
var middlewares = [multipartMiddleware, middleware.validateFiles, middleware.applyCSRF, middleware.authenticate];
app.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture);
app.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon);
app.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
app.post('/uploadgravatardefault', middlewares, controllers.admin.uploads.uploadGravatarDefault);
router.post('/category/uploadpicture', middlewares, controllers.admin.uploads.uploadCategoryPicture);
router.post('/uploadfavicon', middlewares, controllers.admin.uploads.uploadFavicon);
router.post('/uploadlogo', middlewares, controllers.admin.uploads.uploadLogo);
router.post('/uploadgravatardefault', middlewares, controllers.admin.uploads.uploadGravatarDefault);
}
function adminRouter(middleware, controllers) {
@@ -25,8 +24,6 @@ function adminRouter(middleware, controllers) {
addRoutes(router, middleware, controllers);
apiRoutes(router, middleware, controllers);
return router;
}
@@ -35,6 +32,8 @@ function apiRouter(middleware, controllers) {
addRoutes(router, middleware, controllers);
apiRoutes(router, middleware, controllers);
return router;
}

View File

@@ -4,8 +4,7 @@ var express = require('express'),
posts = require('../posts'),
categories = require('../categories'),
uploadsController = require('../controllers/uploads'),
templatesController = require('../controllers/templates');
uploadsController = require('../controllers/uploads');
module.exports = function(app, middleware, controllers) {
@@ -17,7 +16,6 @@ module.exports = function(app, middleware, controllers) {
router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.accounts.getUserByUID);
router.get('/post/:pid', controllers.posts.getPost);
router.get('/get_templates_listing', templatesController.getTemplatesListing);
router.get('/categories/:cid/moderators', getModerators);
router.get('/recent/posts/:term?', getRecentPosts);

View File

@@ -135,14 +135,17 @@ function generateForCategory(req, res, next) {
return next(err);
}
var feed = generateTopicsFeed({
generateTopicsFeed({
title: categoryData.name,
description: categoryData.description,
feed_url: '/category/' + cid + '.rss',
site_url: '/category/' + categoryData.cid,
}, categoryData.topics);
sendFeed(feed, res);
}, categoryData.topics, function(err, feed) {
if (err) {
return next(err);
}
sendFeed(feed, res);
});
});
}
@@ -156,12 +159,32 @@ function generateForRecent(req, res, next) {
}
function generateForPopular(req, res, next) {
generateForTopics({
title: 'Popular Topics',
description: 'A list of topics that are sorted by post count',
feed_url: '/popular.rss',
site_url: '/popular'
}, 'topics:posts', req, res, next);
var uid = req.user ? req.user.uid : 0;
var terms = {
daily: 'day',
weekly: 'week',
monthly: 'month',
alltime: 'alltime'
};
var term = terms[req.params.term] || 'day';
topics.getPopular(term, uid, 19, function(err, topics) {
if (err) {
return next(err);
}
generateTopicsFeed({
title: 'Popular Topics',
description: 'A list of topics that are sorted by post count',
feed_url: '/popular/' + (req.params.term || 'daily') + '.rss',
site_url: '/popular/' + (req.params.term || 'daily')
}, topics, function(err, feed) {
if (err) {
return next(err);
}
sendFeed(feed, res);
});
});
}
function disabledRSS(req, res, next) {
@@ -178,35 +201,58 @@ function generateForTopics(options, set, req, res, next) {
if (err) {
return next(err);
}
var feed = generateTopicsFeed(options, data.topics);
sendFeed(feed, res);
generateTopicsFeed(options, data.topics, function(err, feed) {
if (err) {
return next(err);
}
sendFeed(feed, res);
});
});
}
function generateTopicsFeed(feedOptions, topics) {
feedOptions.ttl = 60;
feedOptions.feed_url = nconf.get('url') + feedOptions.feed_url;
feedOptions.site_url = nconf.get('url') + feedOptions.site_url;
var feed = new rss(feedOptions);
if (topics.length > 0) {
feed.pubDate = new Date(parseInt(topics[0].lastposttime, 10)).toUTCString();
}
topics.forEach(function(topicData) {
feed.item({
title: topicData.title,
url: nconf.get('url') + '/topic/' + topicData.slug,
author: topicData.username,
date: new Date(parseInt(topicData.lastposttime, 10)).toUTCString()
});
function generateTopicsFeed(feedOptions, feedTopics, callback) {
var tids = feedTopics.map(function(topic) {
return topic ? topic.tid : null;
});
topics.getMainPids(tids, function(err, pids) {
if (err) {
return callback(err);
}
posts.getPostsFields(pids, ['content'], function(err, posts) {
if (err) {
return callback(err);
}
return feed;
feedTopics.forEach(function(topic, index) {
if (topic && posts[index]) {
topic.mainPost = posts[index].content;
}
});
feedOptions.ttl = 60;
feedOptions.feed_url = nconf.get('url') + feedOptions.feed_url;
feedOptions.site_url = nconf.get('url') + feedOptions.site_url;
var feed = new rss(feedOptions);
if (feedTopics.length > 0) {
feed.pubDate = new Date(parseInt(feedTopics[0].lastposttime, 10)).toUTCString();
}
feedTopics.forEach(function(topicData) {
feed.item({
title: topicData.title,
description: topicData.mainPost,
url: nconf.get('url') + '/topic/' + topicData.slug,
author: topicData.username,
date: new Date(parseInt(topicData.lastposttime, 10)).toUTCString()
});
});
callback(null, feed);
});
});
}
function generateForRecentPosts(req, res, next) {
@@ -291,6 +337,7 @@ module.exports = function(app, middleware, controllers){
app.get('/category/:category_id.rss', hasCategoryPrivileges, disabledRSS, generateForCategory);
app.get('/recent.rss', disabledRSS, generateForRecent);
app.get('/popular.rss', disabledRSS, generateForPopular);
app.get('/popular/:term.rss', disabledRSS, generateForPopular);
app.get('/recentposts.rss', disabledRSS, generateForRecentPosts);
app.get('/category/:category_id/recentposts.rss', hasCategoryPrivileges, disabledRSS, generateForCategoryRecentPosts);
app.get('/user/:userslug/topics.rss', disabledRSS, generateForUserTopics);

View File

@@ -173,30 +173,38 @@ module.exports = function(app, middleware) {
function handle404(app, middleware) {
app.use(function(req, res, next) {
var relativePath = nconf.get('relative_path');
var isLanguage = new RegExp('^' + relativePath + '/language/[\\w]{2,}/.*.json'),
isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js');
if (!plugins.hasListeners('action:meta.override404')) {
var relativePath = nconf.get('relative_path');
var isLanguage = new RegExp('^' + relativePath + '/language/[\\w]{2,}/.*.json'),
isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js');
if (isClientScript.test(req.url)) {
res.type('text/javascript').status(200).send('');
} else if (isLanguage.test(req.url)) {
res.status(200).json({});
} else if (req.accepts('html')) {
if (process.env.NODE_ENV === 'development') {
winston.warn('Route requested but not found: ' + req.url);
if (isClientScript.test(req.url)) {
res.type('text/javascript').status(200).send('');
} else if (isLanguage.test(req.url)) {
res.status(200).json({});
} else if (req.accepts('html')) {
if (process.env.NODE_ENV === 'development') {
winston.warn('Route requested but not found: ' + req.url);
}
res.status(404);
if (res.locals.isAPI) {
return res.json({path: req.path, error: 'not-found'});
}
middleware.buildHeader(req, res, function() {
res.render('404', {path: req.path});
});
} else {
res.status(404).type('txt').send('Not found');
}
res.status(404);
if (res.locals.isAPI) {
return res.json({path: req.path, error: 'not-found'});
}
middleware.buildHeader(req, res, function() {
res.render('404', {path: req.path});
});
} else {
res.status(404).type('txt').send('Not found');
plugins.fireHook('action:meta.override404', {
req: req,
res: res,
error: {}
});
}
});
}
@@ -210,7 +218,7 @@ function handleErrors(app, middleware) {
}
if (parseInt(err.status, 10) === 302 && err.path) {
return res.locals.isAPI ? res.status(302).json(err) : res.redirect(err.path);
return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path);
}
res.status(err.status || 500);

View File

@@ -1,16 +1,11 @@
"use strict";
var path = require('path'),
nconf = require('nconf'),
meta = require('../meta'),
db = require('../database'),
plugins = require('../plugins'),
var meta = require('../meta'),
middleware = require('../middleware');
function sendMinifiedJS(req, res, next) {
return res.type('text/javascript').send(meta.js.cache);
res.type('text/javascript').send(meta.js.cache);
}
function sendStylesheet(req, res, next) {

View File

@@ -104,10 +104,12 @@ SocketAdmin.themes.updateBranding = function(socket, data, callback) {
};
SocketAdmin.plugins.toggleActive = function(socket, plugin_id, callback) {
require('../postTools').resetCache();
plugins.toggleActive(plugin_id, callback);
};
SocketAdmin.plugins.toggleInstall = function(socket, data, callback) {
require('../postTools').resetCache();
plugins.toggleInstall(data.id, data.version, callback);
};
@@ -121,7 +123,7 @@ SocketAdmin.plugins.orderActivePlugins = function(socket, data, callback) {
db.sortedSetAdd('plugins:active', plugin.order || 0, plugin.name, next);
} else {
next();
}
}
}, callback);
};
@@ -339,12 +341,12 @@ SocketAdmin.getMoreFlags = function(socket, data, callback) {
posts.getUserFlags(byUsername, sortBy, socket.uid, start, end, function(err, posts) {
callback(err, {posts: posts, next: end + 1});
});
} else {
} else {
var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged';
posts.getFlags(set, socket.uid, start, end, function(err, posts) {
callback(err, {posts: posts, next: end + 1});
});
}
}
};
SocketAdmin.takeHeapSnapshot = function(socket, data, callback) {

View File

@@ -68,7 +68,7 @@ Categories.getPrivilegeSettings = function(socket, cid, callback) {
async.reduce(privileges, [], function(members, privilege, next) {
groups.get('cid:' + cid + ':privileges:' + privilege, { expand: true }, function(err, groupObj) {
if (err || !groupObj) {
return next(err, members);
return next(null, members);
}
members = members.concat(groupObj.members);

View File

@@ -184,6 +184,15 @@ SocketGroups.search = function(socket, data, callback) {
groups.search(data.query || '', data.options || {}, callback);
};
SocketGroups.searchMembers = function(socket, data, callback) {
if (!data) {
return callback(null, []);
}
data.uid = socket.uid;
groups.searchMembers(data, callback);
};
SocketGroups.kick = function(socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));

View File

@@ -70,7 +70,7 @@ function onConnect(socket) {
if (err || !userData) {
return;
}
socket.emit('event:connect');
if (userData.status !== 'offline') {
socket.broadcast.emit('event:user_status_change', {uid: socket.uid, status: userData.status || 'online'});
@@ -163,34 +163,31 @@ function requireModules() {
});
}
function authorize(socket, next) {
var handshake = socket.request,
sessionID;
function authorize(socket, callback) {
var handshake = socket.request;
if (!handshake) {
return next(new Error('[[error:not-authorized]]'));
return callback(new Error('[[error:not-authorized]]'));
}
cookieParser(handshake, {}, function(err) {
if (err) {
return next(err);
async.waterfall([
function(next) {
cookieParser(handshake, {}, next);
},
function(next) {
db.sessionStore.get(handshake.signedCookies['express.sid'], function(err, sessionData) {
if (err) {
return next(err);
}
if (sessionData && sessionData.passport && sessionData.passport.user) {
socket.uid = parseInt(sessionData.passport.user, 10);
} else {
socket.uid = 0;
}
next();
});
}
var sessionID = handshake.signedCookies['express.sid'];
db.sessionStore.get(sessionID, function(err, sessionData) {
if (err) {
return next(err);
}
if (sessionData && sessionData.passport && sessionData.passport.user) {
socket.uid = parseInt(sessionData.passport.user, 10);
} else {
socket.uid = 0;
}
next();
});
});
], callback);
}
function addRedisAdapter(io) {
@@ -201,7 +198,7 @@ function addRedisAdapter(io) {
var sub = redis.connect({return_buffers: true});
io.adapter(redisAdapter({pubClient: pub, subClient: sub}));
} else {
} else if (nconf.get('isCluster') === 'true') {
winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.');
}
}

View File

@@ -117,7 +117,7 @@ SocketModules.composer.stopNotifyTyping = function(socket, data) {
SocketModules.composer.getFormattingOptions = function(socket, data, callback) {
plugins.fireHook('filter:composer.formatting', {
options: [
// { className: 'fa fa-bold' } Just an example of what needs to be set via plugins
{ name: 'tags', className: 'fa fa-tags', mobile: true }
]
}, function(err, payload) {
callback(err, payload.options);

View File

@@ -16,6 +16,7 @@ var async = require('async'),
groups = require('../groups'),
user = require('../user'),
websockets = require('./index'),
socketTopics = require('./topics'),
events = require('../events'),
utils = require('../../public/src/utils'),
@@ -345,27 +346,64 @@ function deleteOrRestore(command, socket, data, callback) {
}
SocketPosts.purge = function(socket, data, callback) {
if(!data || !parseInt(data.pid, 10)) {
function purgePost() {
postTools.purge(socket.uid, data.pid, function(err) {
if (err) {
return callback(err);
}
websockets.in('topic_' + data.tid).emit('event:post_purged', data.pid);
events.log({
type: 'post-purge',
uid: socket.uid,
pid: data.pid,
ip: socket.ip
});
callback();
});
}
if (!data || !parseInt(data.pid, 10)) {
return callback(new Error('[[error:invalid-data]]'));
}
postTools.purge(socket.uid, data.pid, function(err) {
if(err) {
isMainAndLastPost(data.pid, function(err, results) {
if (err) {
return callback(err);
}
websockets.in('topic_' + data.tid).emit('event:post_purged', data.pid);
if (!results.isMain) {
return purgePost();
}
events.log({
type: 'post-purge',
uid: socket.uid,
pid: data.pid,
ip: socket.ip
if (!results.isLast) {
return callback(new Error('[[error:cant-purge-main-post]]'));
}
posts.getTopicFields(data.pid, ['tid', 'cid'], function(err, topic) {
if (err) {
return callback(err);
}
socketTopics.doTopicAction('delete', 'event:topic_deleted', socket, {tids: [topic.tid], cid: topic.cid}, callback);
});
callback();
});
};
function isMainAndLastPost(pid, callback) {
async.parallel({
isMain: function(next) {
posts.isMain(pid, next);
},
isLast: function(next) {
posts.getTopicFields(pid, ['postcount'], function(err, topic) {
next(err, topic ? parseInt(topic.postcount, 10) === 1 : false);
});
}
}, callback);
}
SocketPosts.getPrivileges = function(socket, pids, callback) {
privileges.posts.get(pids, socket.uid, function(err, privileges) {
if (err) {

View File

@@ -205,34 +205,34 @@ SocketTopics.markAsUnreadForAll = function(socket, tids, callback) {
};
SocketTopics.delete = function(socket, data, callback) {
doTopicAction('delete', 'event:topic_deleted', socket, data, callback);
SocketTopics.doTopicAction('delete', 'event:topic_deleted', socket, data, callback);
};
SocketTopics.restore = function(socket, data, callback) {
doTopicAction('restore', 'event:topic_restored', socket, data, callback);
SocketTopics.doTopicAction('restore', 'event:topic_restored', socket, data, callback);
};
SocketTopics.purge = function(socket, data, callback) {
doTopicAction('purge', 'event:topic_purged', socket, data, callback);
SocketTopics.doTopicAction('purge', 'event:topic_purged', socket, data, callback);
};
SocketTopics.lock = function(socket, data, callback) {
doTopicAction('lock', 'event:topic_locked', socket, data, callback);
SocketTopics.doTopicAction('lock', 'event:topic_locked', socket, data, callback);
};
SocketTopics.unlock = function(socket, data, callback) {
doTopicAction('unlock', 'event:topic_unlocked', socket, data, callback);
SocketTopics.doTopicAction('unlock', 'event:topic_unlocked', socket, data, callback);
};
SocketTopics.pin = function(socket, data, callback) {
doTopicAction('pin', 'event:topic_pinned', socket, data, callback);
SocketTopics.doTopicAction('pin', 'event:topic_pinned', socket, data, callback);
};
SocketTopics.unpin = function(socket, data, callback) {
doTopicAction('unpin', 'event:topic_unpinned', socket, data, callback);
SocketTopics.doTopicAction('unpin', 'event:topic_unpinned', socket, data, callback);
};
function doTopicAction(action, event, socket, data, callback) {
SocketTopics.doTopicAction = function(action, event, socket, data, callback) {
if (!socket.uid) {
return;
}
@@ -274,7 +274,7 @@ function doTopicAction(action, event, socket, data, callback) {
});
});
}, callback);
}
};
function emitToTopicAndCategory(event, data) {
websockets.in('topic_' + data.tid).emit(event, data);

View File

@@ -216,12 +216,7 @@ var async = require('async'),
}
async.parallel({
mainPost: function(next) {
getMainPosts([topicData.mainPid], uid, next);
},
posts: function(next) {
Topics.getTopicPosts(tid, set, start, end, uid, reverse, next);
},
posts: async.apply(getMainPostAndReplies, topicData, set, uid, start, end, reverse),
category: async.apply(Topics.getCategoryData, tid),
threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', {topic: topicData, uid: uid, tools: []}),
tags: async.apply(Topics.getTopicTagsObjects, tid),
@@ -231,7 +226,7 @@ var async = require('async'),
return callback(err);
}
topicData.posts = Array.isArray(results.mainPost) && results.mainPost.length ? [results.mainPost[0]].concat(results.posts) : results.posts;
topicData.posts = results.posts;
topicData.category = results.category;
topicData.thread_tools = results.threadTools.tools;
topicData.tags = results.tags;
@@ -249,13 +244,49 @@ var async = require('async'),
});
};
function getMainPostAndReplies(topic, set, uid, start, end, reverse, callback) {
async.waterfall([
function(next) {
posts.getPidsFromSet(set, start, end, reverse, next);
},
function(pids, next) {
if ((!Array.isArray(pids) || !pids.length) && !topic.mainPid) {
return callback(null, []);
}
if (topic.mainPid) {
pids.unshift(topic.mainPid);
}
posts.getPostsByPids(pids, uid, next);
},
function(posts, next) {
if (!posts.length) {
return next(null, []);
}
if (topic.mainPid) {
posts[0].index = 0;
}
var indices = Topics.calculatePostIndices(start, end, topic.postcount, reverse);
for (var i=1; i<posts.length; ++i) {
if (posts[i]) {
posts[i].index = indices[i - 1];
}
}
Topics.addPostData(posts, uid, callback);
}
]);
}
Topics.getMainPost = function(tid, uid, callback) {
Topics.getMainPosts([tid], uid, function(err, mainPosts) {
callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null);
});
};
Topics.getMainPosts = function(tids, uid, callback) {
Topics.getMainPids = function(tids, callback) {
Topics.getTopicsFields(tids, ['mainPid'], function(err, topicData) {
if (err) {
return callback(err);
@@ -264,7 +295,15 @@ var async = require('async'),
var mainPids = topicData.map(function(topic) {
return topic ? topic.mainPid : null;
});
callback(null, mainPids);
});
};
Topics.getMainPosts = function(tids, uid, callback) {
Topics.getMainPids(tids, function(err, mainPids) {
if (err) {
return callback(err);
}
getMainPosts(mainPids, uid, callback);
});
};

View File

@@ -166,6 +166,7 @@ module.exports = function(Topics) {
data.topicData = data.topicData[0];
data.topicData.unreplied = 1;
data.topicData.mainPost = data.postData;
plugins.fireHook('action:topic.post', data.topicData);
@@ -280,6 +281,10 @@ module.exports = function(Topics) {
Topics.notifyFollowers(postData, uid);
}
if (postData.index > 0) {
plugins.fireHook('action:topic.reply', postData);
}
postData.topic.title = validator.escape(postData.topic.title);
next(null, postData);
}

View File

@@ -15,53 +15,70 @@ module.exports = function(User) {
var startsWith = data.hasOwnProperty('startsWith') ? data.startsWith : true;
var page = data.page || 1;
var uid = data.uid || 0;
var paginate = data.hasOwnProperty('paginate') ? data.paginate : true;
if (searchBy.indexOf('ip') !== -1) {
return searchByIP(query, uid, callback);
}
var startTime = process.hrtime();
var keys = searchBy.map(function(searchBy) {
return searchBy + ':uid';
});
var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20;
var start = Math.max(0, page - 1) * resultsPerPage;
var end = start + resultsPerPage;
var pageCount = 1;
var matchCount = 0;
var filterBy = Array.isArray(data.filterBy) ? data.filterBy : [];
var searchResult = {};
async.waterfall([
function(next) {
findUids(query, keys, startsWith, next);
if (data.findUids) {
data.findUids(query, searchBy, startsWith, next);
} else {
findUids(query, searchBy, startsWith, next);
}
},
function(uids, next) {
var filterBy = Array.isArray(data.filterBy) ? data.filterBy : [];
filterAndSortUids(uids, filterBy, data.sortBy, next);
},
function(uids, next) {
matchCount = uids.length;
uids = uids.slice(start, end);
searchResult.matchCount = uids.length;
if (paginate) {
var pagination = user.paginate(page, uids);
uids = pagination.data;
searchResult.pagination = pagination.pagination;
}
User.getUsers(uids, uid, next);
},
function(userData, next) {
var data = {
timing: (process.elapsedTimeSince(startTime) / 1000).toFixed(2),
users: userData,
matchCount: matchCount
};
searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2);
searchResult.users = userData;
var currentPage = Math.max(1, Math.ceil((start + 1) / resultsPerPage));
pageCount = Math.ceil(matchCount / resultsPerPage);
data.pagination = pagination.create(currentPage, pageCount);
next(null, data);
next(null, searchResult);
}
], callback);
};
function findUids(query, keys, startsWith, callback) {
User.paginate = function(page, data) {
var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20;
var start = Math.max(0, page - 1) * resultsPerPage;
var end = start + resultsPerPage;
var pageCount = Math.ceil(data.length / resultsPerPage);
var currentPage = Math.max(1, Math.ceil((start + 1) / resultsPerPage));
return {
pagination: pagination.create(currentPage, pageCount),
data: data.slice(start, end)
};
};
function findUids(query, searchBy, startsWith, callback) {
if (!query) {
return db.getSortedSetRevRange('users:joindate', 0, -1, callback);
}
var keys = searchBy.map(function(searchBy) {
return searchBy + ':uid';
});
db.getObjects(keys, function(err, hashes) {
if (err || !hashes) {
return callback(err, []);

View File

@@ -1,7 +1,7 @@
<div class="alert alert-danger">
<strong>[[global:500.title]]</strong>
<p>[[global:500.message]]</p>
<p>{path}<p>
<p>{path}</p>
<!-- IF error --><p>{error}</p><!-- ENDIF error -->
</div>
</div>

View File

@@ -6,7 +6,7 @@
<ul class="nav nav-pills">
<!-- BEGIN templates -->
<li class="<!-- IF @first -->active<!-- ENDIF @first -->"><a href="#" data-template="{templates.template}" data-toggle="pill">{templates.template}</a></li>
<li class="<!-- IF @first -->active<!-- ENDIF @first -->"><a href="#" data-template="{template}" data-toggle="pill">{template}</a></li>
<!-- END templates -->
</ul>
@@ -14,10 +14,10 @@
<div class="col-xs-12">
<div class="tab-content">
<!-- BEGIN templates -->
<div class="tab-pane <!-- IF @first -->active<!-- ENDIF @first -->" data-template="{templates.template}">
<div class="tab-pane <!-- IF @first -->active<!-- ENDIF @first -->" data-template="{template}">
<!-- BEGIN areas -->
<div class="area" data-template="{templates.template}" data-location="{templates.areas.location}">
<h4>{templates.areas.name} <small>{templates.template} / {templates.areas.location}</small></h4>
<div class="area" data-template="{template}" data-location="{areas.location}">
<h4>{areas.name} <small>{template} / {areas.location}</small></h4>
<div class="well widget-area">
</div>

View File

@@ -1,5 +1,6 @@
<div class="row dashboard">
<div class="col-lg-9">
<!-- Override for now, until the right sidebar graphs are fixed (pending socket.io resolution) -->
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">Forum Traffic</div>
<div class="panel-body">
@@ -87,7 +88,8 @@
</div>
</div>
</div>
<div class="col-lg-3">
<!-- Override for now, until the right sidebar graphs are fixed (pending socket.io resolution) -->
<div class="col-lg-3 hide">
<div class="panel panel-default">
<div class="panel-heading">Anonymous vs Registered Users</div>
<div class="panel-body">

View File

@@ -11,7 +11,9 @@
<script>
var RELATIVE_PATH = "{relative_path}";
var config = JSON.parse('{configJSON}');
var app = {};
var app = {
template: "{template.name}"
};
app.user = JSON.parse('{userJSON}');
</script>

View File

@@ -129,7 +129,7 @@
<div class="panel-heading">Users Control Panel</div>
<div class="panel-body">
<button id="createUser" class="btn btn-primary">New User</button>
<a target="_blank" href="/admin/users/csv" class="btn btn-primary">Download CSV</a>
<a target="_blank" href="{relative_path}/api/admin/users/csv" class="btn btn-primary">Download CSV</a>
</div>
</div>
</div>

View File

@@ -34,7 +34,7 @@
<form>
<label>Site Logo</label>
<input id="logoUrl" type="text" class="form-control" placeholder="Path to a logo to display on forum header" data-field="brand:logo" /><br />
<input data-action="upload" data-target="logoUrl" data-route="{relative_path}/admin/uploadlogo" type="button" class="btn btn-default" value="Upload Logo"></input>
<input data-action="upload" data-target="logoUrl" data-route="{relative_path}/api/admin/uploadlogo" type="button" class="btn btn-default" value="Upload Logo"></input>
</form>
</div>
</div>
@@ -45,7 +45,7 @@
<form>
<label>Favicon</label><br />
<input id="faviconUrl" type="text" class="form-control" placeholder="favicon.ico" data-field="brand:favicon" /><br />
<input data-action="upload" data-target="faviconUrl" data-route="{relative_path}/admin/uploadfavicon" type="button" class="btn btn-default" value="Upload"></input>
<input data-action="upload" data-target="faviconUrl" data-route="{relative_path}/api/admin/uploadfavicon" type="button" class="btn btn-default" value="Upload"></input>
</form>
</div>
</div>

View File

@@ -6,11 +6,11 @@
<form>
<div class="form-group">
<label for="maxReconnectionAttempts">Max Reconnection Attempts</label>
<input class="form-control" id="maxReconnectionAttempts" type="text" value="5" data-field="maxReconnectionAttempts" />
<input class="form-control" id="maxReconnectionAttempts" type="text" value="5" placeholder="Default: 5" data-field="maxReconnectionAttempts" />
</div>
<div class="form-group">
<label for="reconnectionDelay">Reconnection Delay</label>
<input class="form-control" id="reconnectionDelay" type="text" value="200" data-field="reconnectionDelay" />
<input class="form-control" id="reconnectionDelay" type="text" value="1500" placeholder="Default: 1500" data-field="reconnectionDelay" />
</div>
</form>
</div>

View File

@@ -67,7 +67,7 @@
<div class="form-group">
<label>Custom Gravatar Default Image</label>
<input id="customGravatarDefaultImage" type="text" class="form-control" placeholder="A custom image to use instead of gravatar defaults" data-field="customGravatarDefaultImage" /><br />
<input data-action="upload" data-target="customGravatarDefaultImage" data-route="{relative_path}/admin/uploadgravatardefault" type="button" class="btn btn-default" value="Upload"></input>
<input data-action="upload" data-target="customGravatarDefaultImage" data-route="{relative_path}/api/admin/uploadgravatardefault" type="button" class="btn btn-default" value="Upload"></input>
</div>
<div class="form-group">

View File

@@ -1,27 +0,0 @@
{
"custom_mapping": {
"^\/?$": "categories",
"^admin?$": "admin/general/dashboard",
"^users/sort-posts": "users",
"^users/latest": "users",
"^users/sort-reputation": "users",
"^users/search": "users",
"^user/.*/edit": "account/edit",
"^user/.*/following": "account/following",
"^user/.*/followers": "account/followers",
"^user/.*/settings": "account/settings",
"^user/.*/favourites": "account/favourites",
"^user/.*/watched": "account/watched",
"^user/.*/posts": "account/posts",
"^user/.*/topics": "account/topics",
"^user/.*/groups": "account/groups",
"^user/[^\/]+": "account/profile",
"^reset/.*": "reset_code",
"^tags/.*": "tag",
"^groups/?$": "groups/list",
"^groups/.*": "groups/details"
},
"force_refresh": {
"logout": true
}
}

View File

@@ -9,7 +9,6 @@ var async = require('async'),
(function(Widgets) {
Widgets.render = function(uid, area, callback) {
if (!area.locations || !area.template) {
return callback(new Error('[[error:invalid-data]]'));