Merge commit 'f7e43877482f6efc276ea71e61d7c7524a8f5a44' into v1.x.x

This commit is contained in:
NodeBB Misty
2016-03-18 13:40:33 -04:00
50 changed files with 482 additions and 124 deletions

6
nodebb
View File

@@ -118,8 +118,12 @@ var getRunningPid = function(callback) {
version: async.apply(getCurrentVersion)
}),
function(payload, next) {
var toCheck = Object.keys(payload.plugins);
if (!payload.plugins.length) {
process.stdout.write('OK'.green + '\n'.reset);
return next(null, []); // no extraneous plugins installed
}
var toCheck = Object.keys(payload.plugins);
request({
method: 'GET',
url: 'https://packages.nodebb.org/api/v1/suggest?version=' + payload.version + '&package[]=' + toCheck.join('&package[]='),

View File

@@ -33,6 +33,7 @@
"express-session": "^1.8.2",
"express-useragent": "0.2.4",
"html-to-text": "2.0.0",
"ip": "1.1.2",
"jimp": "0.2.21",
"less": "^2.0.0",
"logrotate-stream": "^0.2.3",
@@ -43,7 +44,7 @@
"mongodb": "~2.1.3",
"morgan": "^1.3.2",
"nconf": "~0.8.2",
"nodebb-plugin-composer-default": "3.0.7",
"nodebb-plugin-composer-default": "3.0.10",
"nodebb-plugin-dbsearch": "1.0.0",
"nodebb-plugin-emoji-extended": "1.0.3",
"nodebb-plugin-markdown": "4.0.17",
@@ -52,9 +53,9 @@
"nodebb-plugin-spam-be-gone": "0.4.5",
"nodebb-rewards-essentials": "0.0.8",
"nodebb-theme-lavender": "3.0.9",
"nodebb-theme-persona": "4.0.93",
"nodebb-theme-vanilla": "5.0.54",
"nodebb-widget-essentials": "2.0.7",
"nodebb-theme-persona": "4.0.99",
"nodebb-theme-vanilla": "5.0.56",
"nodebb-widget-essentials": "2.0.8",
"nodemailer": "2.0.0",
"nodemailer-sendmail-transport": "1.0.0",
"nodemailer-smtp-transport": "^2.4.1",

View File

@@ -34,6 +34,7 @@
"user-banned": "User banned",
"user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post",
"blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.",
"no-category": "Category does not exist",
"no-topic": "Topic does not exist",

View File

@@ -103,6 +103,8 @@
"enable_topic_searching": "Enable In-Topic Searching",
"topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen",
"scroll_to_my_post": "After posting a reply, show the new post",
"follow_topics_you_reply_to": "Follow topics that you reply to",
"follow_topics_you_create": "Follow topics you create",

View File

@@ -21,6 +21,7 @@
@import "./settings";
@import "../flags";
@import "../blacklist";
@import "./modules/alerts";
@import "./modules/selectable";
@@ -238,5 +239,5 @@
}
[class^="col-"] .mdl-switch__label {
padding-right: 15px;
padding-right: 15px;
}

View File

@@ -0,0 +1,6 @@
#blacklist-rules {
width: 100%;
height: 450px;
display: block;
border: 1px solid #eee;
}

View File

@@ -152,28 +152,45 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri
* @param parentId {number} parent category identifier
*/
function renderList(categories, container, parentId){
templates.parse('admin/partials/categories/category-rows', {
cid: parentId,
categories: categories
}, function(html) {
container.append(html);
// Translate category names if needed
var count = 0;
categories.forEach(function(category, idx, parent) {
translator.translate(category.name, function(translated) {
if (category.name !== translated) {
category.name = translated;
}
++count;
// Handle and children categories in this level have
for(var x=0,numCategories=categories.length;x<numCategories;x++) {
renderList(categories[x].children, $('li[data-cid="' + categories[x].cid + '"]'), categories[x].cid);
}
// Make list sortable
sortables[parentId] = Sortable.create($('ul[data-cid="' + parentId + '"]')[0], {
group: 'cross-categories',
animation: 150,
handle: '.icon',
dataIdAttr: 'data-cid',
ghostClass: "placeholder",
onAdd: itemDidAdd,
onEnd: itemDragDidEnd
if (count === parent.length) {
continueRender();
}
});
});
function continueRender() {
templates.parse('admin/partials/categories/category-rows', {
cid: parentId,
categories: categories
}, function(html) {
container.append(html);
// Handle and children categories in this level have
for(var x=0,numCategories=categories.length;x<numCategories;x++) {
renderList(categories[x].children, $('li[data-cid="' + categories[x].cid + '"]'), categories[x].cid);
}
// Make list sortable
sortables[parentId] = Sortable.create($('ul[data-cid="' + parentId + '"]')[0], {
group: 'cross-categories',
animation: 150,
handle: '.icon',
dataIdAttr: 'data-cid',
ghostClass: "placeholder",
onAdd: itemDidAdd,
onEnd: itemDragDidEnd
});
});
}
}
return Categories;

View File

@@ -155,6 +155,12 @@ define('admin/manage/category', [
} else {
$('a[href="#analytics"]').on('shown.bs.tab', Category.setupGraphs);
}
// Fix the input field for the category name, as it can contain a translation key
var nameInput = $('#cid-' + ajaxify.data.cid + '-name');
if (ajaxify.data.category.name !== nameInput.val()) {
$('#cid-' + ajaxify.data.category.cid + '-name').val(ajaxify.data.category.name);
}
};
Category.setupPrivilegeTable = function() {

View File

@@ -0,0 +1,40 @@
'use strict';
/* globals $, app, socket, templates, define, bootbox */
define('admin/manage/ip-blacklist', [], function() {
var Blacklist = {};
Blacklist.init = function() {
var blacklist = $('#blacklist-rules');
blacklist.on('keyup', function() {
$('#blacklist-rules-holder').val(blacklist.val());
});
$('[data-action="apply"]').on('click', function() {
socket.emit('blacklist.save', blacklist.val(), function(err) {
if (err) {
return app.alertError(err.message);
}
app.alert({
type: 'success',
alert_id: 'blacklist-saved',
title: 'Blacklist Applied',
});
});
});
$('[data-action="test"]').on('click', function() {
socket.emit('blacklist.validate', {
rules: blacklist.val()
}, function(err, data) {
templates.parse('admin/partials/blacklist-validate', data, function(html) {
bootbox.alert(html);
});
});
});
};
return Blacklist;
});

View File

@@ -22,10 +22,16 @@ $(document).ready(function() {
$(window).on('popstate', function (ev) {
ev = ev.originalEvent;
if (ev !== null && ev.state && ev.state.url !== undefined) {
ajaxify.go(ev.state.url, function() {
$(window).trigger('action:popstate', {url: ev.state.url});
}, true);
if (ev !== null && ev.state) {
if (ev.state.url === null && ev.state.returnPath !== undefined) {
window.history.replaceState({
url: ev.state.returnPath
}, ev.state.returnPath, config.relative_path + '/' + ev.state.returnPath);
} else if (ev.state.url !== undefined) {
ajaxify.go(ev.state.url, function() {
$(window).trigger('action:popstate', {url: ev.state.url});
}, true);
}
}
});
@@ -148,6 +154,7 @@ $(document).ready(function() {
templates.parse(tpl_url, data, function(template) {
translator.translate(template, function(translatedTemplate) {
translatedTemplate = translator.unescape(translatedTemplate);
$('body').addClass(data.bodyClass);
$('#content').html(translatedTemplate);

View File

@@ -521,6 +521,7 @@ app.cacheBuster = null;
if (typeof blockName === 'string') {
templates.parse(template, blockName, data, function(html) {
translator.translate(html, function(translatedHTML) {
translatedHTML = translator.unescape(translatedHTML);
callback($(translatedHTML));
});
});
@@ -528,6 +529,7 @@ app.cacheBuster = null;
callback = data, data = blockName;
templates.parse(template, data, function(html) {
translator.translate(html, function(translatedHTML) {
translatedHTML = translator.unescape(translatedHTML);
callback($(translatedHTML));
});
});

View File

@@ -17,7 +17,8 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'],
$('#inputBirthday').datepicker({
changeMonth: true,
changeYear: true,
yearRange: '1900:+0'
yearRange: '1900:-5y',
defaultDate: '-13y'
});
});

View File

@@ -114,7 +114,7 @@ define('forum/topic/events', [
}
editedPostEl.fadeOut(250, function() {
editedPostEl.html(data.post.content);
editedPostEl.html(translator.unescape(data.post.content));
editedPostEl.find('img:not(.not-responsive)').addClass('img-responsive');
app.replaceSelfLinks(editedPostEl.find('a'));
posts.wrapImagesInLinks(editedPostEl.parent());
@@ -186,7 +186,7 @@ define('forum/topic/events', [
if (isDeleted) {
postEl.find('[component="post/content"]').translateHtml('[[topic:post_is_deleted]]');
} else {
postEl.find('[component="post/content"]').html(data.content);
postEl.find('[component="post/content"]').html(translator.unescape(data.content));
}
}
}

View File

@@ -33,10 +33,12 @@ define('forum/topic/posts', [
ajaxify.data.postcount ++;
postTools.updatePostCount(ajaxify.data.postcount);
if (config.usePagination) {
onNewPostPagination(data);
} else {
onNewPostInfiniteScroll(data);
if (ajaxify.data.scrollToMyPost) {
if (config.usePagination) {
onNewPostPagination(data);
} else {
onNewPostInfiniteScroll(data);
}
}
};
@@ -140,7 +142,7 @@ define('forum/topic/posts', [
}
data.slug = ajaxify.data.slug;
$(window).trigger('action:posts.loading', {posts: data.posts, after: after, before: before});
app.parseAndTranslate('topic', 'posts', data, function(html) {

View File

@@ -113,7 +113,7 @@
'<i style="color:' + child.color + ';" class="fa fa-stack-1x ' + child.icon + '"></i>' +
'</span><small>' + child.name + '</small></a> ';
});
html = html ? ('<br/><span class="category-children">' + html + '</span>') : html;
html = html ? ('<span class="category-children">' + html + '</span>') : html;
return html;
};

View File

@@ -17,15 +17,15 @@ module.exports = function(Categories) {
db.incrObjectField('global', 'nextCid', next);
},
function(cid, next) {
var slug = cid + '/' + utils.slugify(data.name),
order = data.order || cid, // If no order provided, place it at the end
colours = Categories.assignColours();
var slug = cid + '/' + utils.slugify(data.name);
var order = data.order || cid; // If no order provided, place it at the end
var colours = Categories.assignColours();
category = {
cid: cid,
name: data.name,
description: ( data.description ? data.description : '' ),
icon: ( data.icon ? data.icon : '' ),
description: data.description ? data.description : '',
icon: data.icon ? data.icon : '',
bgColor: data.bgColor || colours[0],
color: data.color || colours[1],
slug: slug,
@@ -49,6 +49,7 @@ module.exports = function(Categories) {
async.series([
async.apply(db.setObject, 'category:' + category.cid, category),
async.apply(Categories.parseDescription, category.cid, category.description),
async.apply(db.sortedSetAdd, 'categories:cid', category.order, category.cid),
async.apply(db.sortedSetAdd, 'cid:' + parentCid + ':children', category.order, category.cid),
async.apply(privileges.categories.give, defaultPrivileges, category.cid, 'administrators'),

View File

@@ -4,6 +4,7 @@
var async = require('async'),
db = require('../database'),
utils = require('../../public/src/utils'),
translator = require('../../public/src/modules/translator'),
plugins = require('../plugins');
module.exports = function(Categories) {
@@ -27,7 +28,9 @@ module.exports = function(Categories) {
if (modifiedFields.hasOwnProperty('name')) {
modifiedFields.slug = cid + '/' + utils.slugify(modifiedFields.name);
translator.translate(modifiedFields.name, function(translated) {
modifiedFields.slug = cid + '/' + utils.slugify(translated);
});
}
plugins.fireHook('filter:category.update', {category: modifiedFields}, function(err, categoryData) {
@@ -69,7 +72,7 @@ module.exports = function(Categories) {
if (key === 'order') {
updateOrder(cid, value, callback);
} else if (key === 'description') {
parseDescription(cid, value, callback);
Categories.parseDescription(cid, value, callback);
} else {
callback();
}
@@ -97,7 +100,7 @@ module.exports = function(Categories) {
function (next) {
db.setObjectField('category:' + cid, 'parentCid', newParent, next);
}
], function(err, results) {
], function(err) {
callback(err);
});
});
@@ -121,13 +124,13 @@ module.exports = function(Categories) {
});
}
function parseDescription(cid, description, callback) {
Categories.parseDescription = function(cid, description, callback) {
plugins.fireHook('filter:parse.raw', description, function(err, parsedDescription) {
if (err) {
return callback(err);
}
Categories.setCategoryField(cid, 'descriptionParsed', parsedDescription, callback);
});
}
};
};

View File

@@ -42,7 +42,7 @@ settingsController.get = function(req, res, callback) {
getHomePageRoutes(next);
},
ips: function (next) {
user.getIPs(req.uid, 4, next);
user.getIPs(userData.uid, 4, next);
},
sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID)
}, next);

View File

@@ -5,6 +5,7 @@ var adminController = {
categories: require('./admin/categories'),
tags: require('./admin/tags'),
flags: require('./admin/flags'),
blacklist: require('./admin/blacklist'),
groups: require('./admin/groups'),
appearance: require('./admin/appearance'),
extend: {

View File

@@ -0,0 +1,16 @@
"use strict";
var meta = require('../../meta');
var blacklistController = {};
blacklistController.get = function(req, res, next) {
meta.blacklist.get(function(err, rules) {
if (err) {
return next(err);
}
res.render('admin/manage/ip-blacklist', {rules: rules, title: 'IP Blacklist'});
});
};
module.exports = blacklistController;

View File

@@ -51,7 +51,7 @@ categoryController.get = function(req, res, callback) {
return helpers.notAllowed(req, res);
}
if ((!req.params.slug || results.categoryData.slug !== cid + '/' + req.params.slug) && (results.categoryData.slug && results.categoryData.slug !== cid + '/')) {
if (!res.locals.isAPI && (!req.params.slug || results.categoryData.slug !== cid + '/' + req.params.slug) && (results.categoryData.slug && results.categoryData.slug !== cid + '/')) {
return helpers.redirect(res, '/category/' + encodeURI(results.categoryData.slug));
}

View File

@@ -0,0 +1,29 @@
"use strict";
var user = require('../user');
var adminFlagsController = require('./admin/flags');
var adminBlacklistController = require('./admin/blacklist');
var globalModsController = {};
globalModsController.flagged = function(req, res, next) {
user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) {
if (err || !isAdminOrGlobalMod) {
return next(err);
}
adminFlagsController.get(req, res, next);
});
};
globalModsController.ipBlacklist = function(req, res, next) {
user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) {
if (err || !isAdminOrGlobalMod) {
return next(err);
}
adminBlacklistController.get(req, res, next);
});
};
module.exports = globalModsController;

View File

@@ -12,7 +12,6 @@ var helpers = require('./helpers');
var Controllers = {
topics: require('./topics'),
posts: require('./posts'),
categories: require('./categories'),
category: require('./category'),
unread: require('./unread'),
@@ -25,7 +24,8 @@ var Controllers = {
accounts: require('./accounts'),
authentication: require('./authentication'),
api: require('./api'),
admin: require('./admin')
admin: require('./admin'),
globalMods: require('./globalmods')
};

View File

@@ -1,19 +0,0 @@
"use strict";
var user = require('../user');
var adminFlagsController = require('./admin/flags');
var postsController = {};
postsController.flagged = function(req, res, next) {
user.isAdminOrGlobalMod(req.uid, function(err, isAdminOrGlobalMod) {
if (err || !isAdminOrGlobalMod) {
return next(err);
}
adminFlagsController.get(req, res, next);
});
};
module.exports = postsController;

View File

@@ -4,7 +4,6 @@
var async = require('async');
var S = require('string');
var nconf = require('nconf');
var validator = require('validator');
var user = require('../user');
var meta = require('../meta');
@@ -24,6 +23,7 @@ topicsController.get = function(req, res, callback) {
var currentPage = parseInt(req.query.page, 10) || 1;
var pageCount = 1;
var userPrivileges;
var settings;
if ((req.params.post_index && !utils.isNumber(req.params.post_index)) || !utils.isNumber(tid)) {
return callback();
@@ -54,7 +54,7 @@ topicsController.get = function(req, res, callback) {
return helpers.notAllowed(req, res);
}
if ((!req.params.slug || results.topic.slug !== tid + '/' + req.params.slug) && (results.topic.slug && results.topic.slug !== tid + '/')) {
if (!res.locals.isAPI && (!req.params.slug || results.topic.slug !== tid + '/' + req.params.slug) && (results.topic.slug && results.topic.slug !== tid + '/')) {
var url = '/topic/' + encodeURI(results.topic.slug);
if (req.params.post_index){
url += '/'+req.params.post_index;
@@ -62,7 +62,7 @@ topicsController.get = function(req, res, callback) {
return helpers.redirect(res, url);
}
var settings = results.settings;
settings = results.settings;
var postCount = parseInt(results.topic.postcount, 10);
pageCount = Math.max(1, Math.ceil((postCount - 1) / settings.postsPerPage));
@@ -261,6 +261,7 @@ topicsController.get = function(req, res, callback) {
data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
data['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1;
data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
data.scrollToMyPost = settings.scrollToMyPost;
data.rssFeedUrl = nconf.get('relative_path') + '/topic/' + data.tid + '.rss';
data.pagination = pagination.create(currentPage, pageCount);
data.pagination.rel.forEach(function(rel) {

View File

@@ -1,7 +1,5 @@
"use strict";
var winston = require('winston');
module.exports = function(db, module) {
var helpers = module.helpers.mongo;
@@ -121,9 +119,8 @@ module.exports = function(db, module) {
}
var map = helpers.toMap(items);
var returnData = [],
index = 0,
item;
var returnData = [];
var item;
for (var i=0; i<keys.length; ++i) {
item = map[keys[i]] || {};
@@ -219,7 +216,7 @@ module.exports = function(db, module) {
data[field] = '';
});
db.collection('objects').update({_key: key}, {$unset : data}, function(err, res) {
db.collection('objects').update({_key: key}, {$unset : data}, function(err) {
callback(err);
});
};

View File

@@ -37,7 +37,7 @@ module.exports = function(db, module) {
bulk.find({_key: key, value: values[i]}).upsert().updateOne({$set: {score: parseInt(scores[i], 10)}});
}
bulk.execute(function(err, result) {
bulk.execute(function(err) {
callback(err);
});
}
@@ -55,7 +55,7 @@ module.exports = function(db, module) {
bulk.find({_key: keys[i], value: value}).upsert().updateOne({$set: {score: parseInt(score, 10)}});
}
bulk.execute(function(err, result) {
bulk.execute(function(err) {
callback(err);
});
};
@@ -85,7 +85,7 @@ module.exports = function(db, module) {
}
value = helpers.valueToString(value);
db.collection('objects').remove({_key: {$in: keys}, value: value}, function(err, res) {
db.collection('objects').remove({_key: {$in: keys}, value: value}, function(err) {
callback(err);
});
};
@@ -95,16 +95,17 @@ module.exports = function(db, module) {
if (!Array.isArray(keys) || !keys.length) {
return callback();
}
var query = {_key: {$in: keys}};
var scoreQuery = {};
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').remove({_key: {$in: keys}, score: scoreQuery}, function(err) {
db.collection('objects').remove(query, function(err) {
callback(err);
});
};
@@ -182,12 +183,14 @@ module.exports = function(db, module) {
count = 0;
}
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;
}
var fields = {_id: 0, value: 1};
@@ -195,7 +198,7 @@ module.exports = function(db, module) {
fields.score = 1;
}
db.collection('objects').find({_key:key, score: scoreQuery}, {fields: fields})
db.collection('objects').find(query, {fields: fields})
.limit(count)
.skip(start)
.sort({score: sort})

View File

@@ -26,6 +26,7 @@ var async = require('async'),
require('./meta/tags')(Meta);
require('./meta/dependencies')(Meta);
Meta.templates = require('./meta/templates');
Meta.blacklist = require('./meta/blacklist');
/* Assorted */
Meta.userOrGroupExists = function(slug, callback) {

126
src/meta/blacklist.js Normal file
View File

@@ -0,0 +1,126 @@
'use strict';
var ip = require('ip');
var winston = require('winston');
var async = require('async');
var db = require('../database');
var Blacklist = {
_rules: []
};
Blacklist.load = function(callback) {
async.waterfall([
async.apply(db.get, 'ip-blacklist-rules'),
async.apply(Blacklist.validate)
], function(err, rules) {
if (err) {
return callback(err);
}
winston.verbose('[meta/blacklist] Loading ' + rules.valid.length + ' blacklist rules');
if (rules.invalid.length) {
winston.warn('[meta/blacklist] ' + rules.invalid.length + ' invalid blacklist rule(s) were ignored.');
}
Blacklist._rules = {
ipv4: rules.ipv4,
ipv6: rules.ipv6,
cidr: rules.cidr
};
callback();
});
};
Blacklist.save = function(rules, callback) {
db.set('ip-blacklist-rules', rules, function(err) {
if (err) {
return callback(err);
}
Blacklist.load(callback);
});
};
Blacklist.get = function(callback) {
db.get('ip-blacklist-rules', callback);
};
Blacklist.test = function(clientIp, callback) {
if (
Blacklist._rules.ipv4.indexOf(clientIp) === -1 // not explicitly specified in ipv4 list
&& Blacklist._rules.ipv6.indexOf(clientIp) === -1 // not explicitly specified in ipv6 list
&& !Blacklist._rules.cidr.some(function(subnet) {
return ip.cidrSubnet(subnet).contains(clientIp);
}) // not in a blacklisted cidr range
) {
if (typeof callback === 'function') {
callback();
} else {
return false;
}
} else {
var err = new Error('[[error:blacklisted-ip]]');
err.code = 'blacklisted-ip';
if (typeof callback === 'function') {
callback(err);
} else {
return true;
}
}
};
Blacklist.validate = function(rules, callback) {
rules = (rules || '').split('\n');
var ipv4 = [];
var ipv6 = [];
var cidr = [];
var invalid = [];
var isCidrSubnet = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/,
inlineCommentMatch = /#.*$/,
whitelist = ['127.0.0.1', '::1', '::ffff:0:127.0.0.1'];
// Filter out blank lines and lines starting with the hash character (comments)
// Also trim inputs and remove inline comments
rules = rules.map(function(rule) {
rule = rule.replace(inlineCommentMatch, '').trim();
return rule.length && !rule.startsWith('#') ? rule : null;
}).filter(Boolean);
// Filter out invalid rules
rules = rules.filter(function(rule) {
if (whitelist.indexOf(rule) !== -1) {
invalid.push(rule);
return false;
}
if (ip.isV4Format(rule)) {
ipv4.push(rule);
return true;
} else if (ip.isV6Format(rule)) {
ipv6.push(rule);
return true;
} else if (isCidrSubnet.test(rule)) {
cidr.push(rule);
return true;
} else {
invalid.push(rule);
return false;
}
return true;
});
callback(null, {
numRules: rules.length + invalid.length,
ipv4: ipv4,
ipv6: ipv6,
cidr: cidr,
valid: rules,
invalid: invalid
});
};
module.exports = Blacklist;

View File

@@ -70,6 +70,7 @@ module.exports = function(Meta) {
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/textcomplete/jquery.textcomplete.css";';
source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/flags.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/blacklist.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";';
source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";';

View File

@@ -26,7 +26,7 @@ var app,
helpers: require('../controllers/helpers')
};
toobusy.maxLag(parseInt(meta.config.eventLoopLagThreshold, 10) || 70);
toobusy.maxLag(parseInt(meta.config.eventLoopLagThreshold, 10) || 100);
toobusy.interval(parseInt(meta.config.eventLoopInterval, 10) || 500);
middleware.authenticate = function(req, res, next) {
@@ -257,6 +257,13 @@ middleware.busyCheck = function(req, res, next) {
}
};
middleware.applyBlacklist = function(req, res, next) {
meta.blacklist.test(req.ip, function(err) {
console.log('blacklist returned:', err);
next(err);
});
};
module.exports = function(webserver) {
app = webserver;
middleware.admin = require('./admin')(webserver);

View File

@@ -72,6 +72,7 @@ module.exports = function(middleware) {
var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB';
language = req.query.lang || language;
translator.translate(str, language, function(translated) {
translated = translator.unescape(translated);
translated = translated + '<script id="ajaxify-data" type="application/json">' + ajaxifyData + '</script>';
fn(err, translated);
});

View File

@@ -3,6 +3,7 @@
var cache = require('./cache');
var plugins = require('../plugins');
var translator = require('../../public/src/modules/translator');
module.exports = function(Posts) {
@@ -24,6 +25,8 @@ module.exports = function(Posts) {
return callback(err);
}
data.postData.content = translator.escape(data.postData.content);
if (global.env === 'production' && data.postData.pid) {
cache.set(data.postData.pid, data.postData.content);
}

View File

@@ -54,8 +54,8 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/categories/:category_id', middlewares, controllers.admin.categories.get);
router.get('/manage/tags', middlewares, controllers.admin.tags.get);
router.get('/manage/flags', middlewares, controllers.admin.flags.get);
router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get);
router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate);
router.get('/manage/users/search', middlewares, controllers.admin.users.search);

View File

@@ -62,8 +62,8 @@
}));
});
router.post('/register', Auth.middleware.applyCSRF, controllers.authentication.register);
router.post('/login', Auth.middleware.applyCSRF, controllers.authentication.login);
router.post('/register', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.register);
router.post('/login', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.login);
router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout);
hotswap.replace('auth', router);

View File

@@ -36,8 +36,9 @@ function mainRoutes(app, middleware, controllers) {
setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse);
}
function postRoutes(app, middleware, controllers) {
setupPageRoute(app, '/posts/flags', middleware, [], controllers.posts.flagged);
function globalModRoutes(app, middleware, controllers) {
setupPageRoute(app, '/ip-blacklist', middleware, [], controllers.globalMods.ipBlacklist);
setupPageRoute(app, '/posts/flags', middleware, [], controllers.globalMods.flagged);
}
function topicRoutes(app, middleware, controllers) {
@@ -111,7 +112,7 @@ module.exports = function(app, middleware) {
mainRoutes(router, middleware, controllers);
topicRoutes(router, middleware, controllers);
postRoutes(router, middleware, controllers);
globalModRoutes(router, middleware, controllers);
tagRoutes(router, middleware, controllers);
categoryRoutes(router, middleware, controllers);
accountRoutes(router, middleware, controllers);
@@ -187,9 +188,12 @@ function handle404(app, middleware) {
function handleErrors(app, middleware) {
app.use(function(err, req, res, next) {
if (err.code === 'EBADCSRFTOKEN') {
winston.error(req.path + '\n', err.message);
return res.sendStatus(403);
switch (err.code) {
case 'EBADCSRFTOKEN':
winston.error(req.path + '\n', err.message);
return res.sendStatus(403);
case 'blacklisted-ip':
return res.status(403).type('text/plain').send(err.message);
}
if (parseInt(err.status, 10) === 302 && err.path) {

View File

@@ -85,7 +85,9 @@ function searchInContent(data, callback) {
topics.getMainPids(results.tids, next);
},
function(mainPids, next) {
results.pids = mainPids.concat(results.pids).filter(function(pid, index, array) {
results.pids = mainPids.concat(results.pids).map(function(pid) {
return pid && pid.toString();
}).filter(function(pid, index, array) {
return pid && array.indexOf(pid) === index;
});

View File

@@ -3,16 +3,19 @@
var meta = require('./meta');
function expandObjBy(obj1, obj2) {
var key, val1, val2, changed = false;
var key, val1, val2, xorValIsArray, changed = false;
for (key in obj2) {
if (obj2.hasOwnProperty(key)) {
val2 = obj2[key];
val1 = obj1[key];
if (!obj1.hasOwnProperty(key) || typeof val2 !== typeof val1) {
xorValIsArray = Array.isArray(val1) ^ Array.isArray(val2);
if (xorValIsArray || !obj1.hasOwnProperty(key) || typeof val2 !== typeof val1) {
obj1[key] = val2;
changed = true;
} else if (typeof val2 === 'object' && expandObjBy(val1, val2)) {
changed = true;
} else if (typeof val2 === 'object' && !Array.isArray(val2)) {
if (expandObjBy(val1, val2)) {
changed = true;
}
}
}
}
@@ -26,10 +29,10 @@ function trim(obj1, obj2) {
val1 = obj1[key];
if (!obj2.hasOwnProperty(key)) {
delete obj1[key];
} else if (typeof val1 === 'object') {
} else if (typeof val1 === 'object' && !Array.isArray(val1)) {
trim(val1, obj2[key]);
}
}
}
}
}

View File

@@ -29,15 +29,16 @@ pubsub.on('sync:stats:end', function(data) {
stats[data.id] = data.stats;
});
pubsub.on('sync:stats:guests', function() {
var io = require('../index').server;
var roomClients = io.sockets.adapter.rooms;
var guestCount = roomClients.online_guests ? roomClients.online_guests.length : 0;
pubsub.publish('sync:stats:guests:end', guestCount);
});
SocketRooms.getTotalGuestCount = function(callback) {
var count = 0;
pubsub.once('sync:stats:guests', function() {
var io = require('../index').server;
var roomClients = io.sockets.adapter.rooms;
var guestCount = roomClients.online_guests ? roomClients.online_guests.length : 0;
pubsub.publish('sync:stats:guests:end', guestCount);
});
pubsub.on('sync:stats:guests:end', function(guestCount) {
count += guestCount;

View File

@@ -0,0 +1,27 @@
'use strict';
var async = require('async');
var winston = require('winston');
var user = require('../user');
var meta = require('../meta');
var SocketBlacklist = {};
SocketBlacklist.validate = function(socket, data, callback) {
meta.blacklist.validate(data.rules, callback);
};
SocketBlacklist.save = function(socket, rules, callback) {
user.isAdminOrGlobalMod(socket.uid, function(err, isAdminOrGlobalMod) {
if (err || !isAdminOrGlobalMod) {
return callback(err || new Error('[[error:no-privileges]]'));
}
meta.blacklist.save(rules, callback);
});
};
module.exports = SocketBlacklist;

View File

@@ -121,7 +121,7 @@ function onMessage(socket, payload) {
function requireModules() {
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
'notifications', 'plugins', 'posts', 'topics', 'user'
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist'
];
modules.forEach(function(module) {

View File

@@ -3,6 +3,7 @@
var async = require('async');
var user = require('../user');
var notifications = require('../notifications');
var utils = require('../../public/src/utils');
var SocketNotifs = {};
@@ -15,7 +16,7 @@ SocketNotifs.get = function(socket, data, callback) {
};
SocketNotifs.loadMore = function(socket, data, callback) {
if (!data || !parseInt(data.after, 10)) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) {
return callback(new Error('[[error:invalid-data]]'));
}
if (!socket.uid) {

View File

@@ -88,7 +88,7 @@ module.exports = function(SocketTopics) {
};
SocketTopics.loadMoreUnreadTopics = function(socket, data, callback) {
if (!data || !data.after) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -99,7 +99,7 @@ module.exports = function(SocketTopics) {
};
SocketTopics.loadMoreFromSet = function(socket, data, callback) {
if (!data || !data.after || !data.set) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0 || !data.set) {
return callback(new Error('[[error:invalid-data]]'));
}

View File

@@ -76,6 +76,7 @@ module.exports = function(User) {
settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1;
settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1;
settings.bootswatchSkin = settings.bootswatchSkin || 'default';
settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) ===1;
callback(null, settings);
});
@@ -120,7 +121,8 @@ module.exports = function(User) {
restrictChat: data.restrictChat,
topicSearchEnabled: data.topicSearchEnabled,
groupTitle: data.groupTitle,
homePageRoute: data.homePageCustom || data.homePageRoute
homePageRoute: data.homePageCustom || data.homePageRoute,
scrollToMyPost: data.scrollToMyPost
};
if (data.bootswatchSkin) {

View File

@@ -9,12 +9,12 @@
<fieldset>
<label for="change-group-desc">Description</label>
<input type="text" class="form-control" id="change-group-desc" placeholder="A short description about your group" value="{group.description}" /><br />
<input type="text" class="form-control" id="change-group-desc" placeholder="A short description about your group" value="{group.description}" maxlength="255" /><br />
</fieldset>
<fieldset>
<label for="change-group-user-title">Title of Members</label>
<input type="text" class="form-control" id="change-group-user-title" placeholder="The title of users if they are a member of this group" value="{group.userTitle}"/><br />
<input type="text" class="form-control" id="change-group-user-title" placeholder="The title of users if they are a member of this group" value="{group.userTitle}" maxlength="40" /><br />
</fieldset>
<fieldset>

View File

@@ -0,0 +1,40 @@
<div class="flags">
<div class="col-lg-12">
<p class="lead">
Configure your IP blacklist here.
</p>
<p>
Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs
is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and
they will be prevented from logging in to or registering a new account.
</p>
<div class="row">
<div class="col-sm-6">
<textarea id="blacklist-rules">{rules}</textarea>
</div>
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">Active Rules</div>
<div class="panel-body">
<button type="button" class="btn btn-warning" data-action="test"><i class="fa fa-bomb"></i> Validate Blacklist</button>
<button type="button" class="btn btn-primary" data-action="apply"><i class="fa fa-save"></i> Apply Blacklist</button>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Syntax Hints</div>
<div class="panel-body">
<p>
Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g.
<code>192.168.100.0/22</code>).
</p>
<p>
You can add in comments by starting lines with the <code>#</code> symbol.
</p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,14 @@
<p class="lead">
<strong>{valid.length}</strong> out of <strong>{numRules}</strong> rule(s) valid.
</p>
<!-- IF invalid.length -->
<p>
The following <strong>{invalid.length}</strong> rules are invalid:
</p>
<ul>
<!-- BEGIN invalid -->
<li><code>@value</code></li>
<!-- END invalid -->
</ul>
<!-- ENDIF invalid.length -->

View File

@@ -20,6 +20,7 @@
<li><a href="{relative_path}/admin/manage/registration">Registration Queue</a></li>
<li><a href="{relative_path}/admin/manage/groups">Groups</a></li>
<li><a href="{relative_path}/admin/manage/flags">Flags</a></li>
<li><a href="{relative_path}/admin/manage/ip-blacklist">IP Blacklist</a></li>
</ul>
</section>
@@ -173,6 +174,7 @@
<li><a href="{relative_path}/admin/manage/registration">Registration Queue</a></li>
<li><a href="{relative_path}/admin/manage/groups">Groups</a></li>
<li><a href="{relative_path}/admin/manage/flags">Flags</a></li>
<li><a href="{relative_path}/admin/manage/ip-blacklist">IP Blacklist</a></li>
</ul>
</li>
<li class="dropdown menu-item">

File diff suppressed because one or more lines are too long

View File

@@ -95,7 +95,8 @@ function initializeNodeBB(callback) {
async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'nodebb.min.js'),
async.apply(!skipJS ? meta.js.minify : meta.js.getFromFile, 'acp.min.js'),
async.apply(!skipLess ? meta.css.minify : meta.css.getFromFile),
async.apply(meta.sounds.init)
async.apply(meta.sounds.init),
async.apply(meta.blacklist.load)
], next);
},
function(results, next) {