Merge remote-tracking branch 'origin/master' into flagging-refactor

v1.18.x
Julian Lam
commit dc508fa0cb

@ -37,7 +37,7 @@ winston.add(winston.transports.Console, {
colorize: true, colorize: true,
timestamp: function () { timestamp: function () {
var date = new Date(); var date = new Date();
return (!!nconf.get('json-logging')) ? date.toJSON() : date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,5) + ' [' + global.process.pid + ']'; return (!!nconf.get('json-logging')) ? date.toJSON() : date.getDate() + '/' + (date.getMonth() + 1) + ' ' + date.toTimeString().substr(0,8) + ' [' + global.process.pid + ']';
}, },
level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose'), level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose'),
json: (!!nconf.get('json-logging')), json: (!!nconf.get('json-logging')),
@ -181,20 +181,25 @@ function start() {
async.waterfall([ async.waterfall([
async.apply(db.init), async.apply(db.init),
async.apply(db.checkCompatibility),
function (next) { function (next) {
require('./src/meta').configs.init(next); var meta = require('./src/meta');
}, async.parallel([
function (next) { async.apply(db.checkCompatibility),
if (nconf.get('dep-check') === undefined || nconf.get('dep-check') !== false) { async.apply(meta.configs.init),
require('./src/meta').dependencies.check(next); function (next) {
} else { if (nconf.get('dep-check') === undefined || nconf.get('dep-check') !== false) {
winston.warn('[init] Dependency checking skipped!'); meta.dependencies.check(next);
setImmediate(next); } else {
} winston.warn('[init] Dependency checking skipped!');
}, setImmediate(next);
function (next) { }
require('./src/upgrade').check(next); },
function (next) {
require('./src/upgrade').check(next);
}
], function (err) {
next(err);
});
}, },
function (next) { function (next) {
var webserver = require('./src/webserver'); var webserver = require('./src/webserver');

@ -52,7 +52,7 @@
"morgan": "^1.3.2", "morgan": "^1.3.2",
"mousetrap": "^1.5.3", "mousetrap": "^1.5.3",
"nconf": "~0.8.2", "nconf": "~0.8.2",
"nodebb-plugin-composer-default": "4.3.2", "nodebb-plugin-composer-default": "4.3.3",
"nodebb-plugin-dbsearch": "1.0.4", "nodebb-plugin-dbsearch": "1.0.4",
"nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-extended": "1.1.1",
"nodebb-plugin-emoji-one": "1.1.5", "nodebb-plugin-emoji-one": "1.1.5",

@ -8,44 +8,9 @@ define('admin/manage/category', [
'autocomplete' 'autocomplete'
], function (uploader, iconSelect, colorpicker, autocomplete) { ], function (uploader, iconSelect, colorpicker, autocomplete) {
var Category = {}; var Category = {};
var modified_categories = {};
Category.init = function () { Category.init = function () {
var modified_categories = {};
function modified(el) {
var cid = $(el).parents('form').attr('data-cid');
if (cid) {
modified_categories[cid] = modified_categories[cid] || {};
modified_categories[cid][$(el).attr('data-name')] = $(el).val();
app.flags = app.flags || {};
app.flags._unsaved = true;
}
}
function save(e) {
e.preventDefault();
if(Object.keys(modified_categories).length) {
socket.emit('admin.categories.update', modified_categories, function (err, result) {
if (err) {
return app.alertError(err.message);
}
if (result && result.length) {
app.flags._unsaved = false;
app.alert({
title: 'Updated Categories',
message: 'Category IDs ' + result.join(', ') + ' was successfully updated.',
type: 'success',
timeout: 2000
});
}
});
modified_categories = {};
}
}
$('.blockclass, form.category select').each(function () { $('.blockclass, form.category select').each(function () {
var $this = $(this); var $this = $(this);
@ -85,7 +50,28 @@ define('admin/manage/category', [
$('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker); $('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker);
$('#save').on('click', save); $('#save').on('click', function () {
if (Object.keys(modified_categories).length) {
socket.emit('admin.categories.update', modified_categories, function (err, result) {
if (err) {
return app.alertError(err.message);
}
if (result && result.length) {
app.flags._unsaved = false;
app.alert({
title: 'Updated Categories',
message: 'Category IDs ' + result.join(', ') + ' was successfully updated.',
type: 'success',
timeout: 2000
});
}
});
modified_categories = {};
}
return false;
});
$('.purge').on('click', function (e) { $('.purge').on('click', function (e) {
e.preventDefault(); e.preventDefault();
@ -171,8 +157,37 @@ define('admin/manage/category', [
}); });
Category.setupPrivilegeTable(); Category.setupPrivilegeTable();
handleTags();
}; };
function modified(el) {
var cid = ajaxify.data.category.cid;
if (cid) {
modified_categories[cid] = modified_categories[cid] || {};
modified_categories[cid][$(el).attr('data-name')] = $(el).val();
app.flags = app.flags || {};
app.flags._unsaved = true;
}
}
function handleTags() {
var tagEl = $('#tag-whitelist');
tagEl.tagsinput({
confirmKeys: [13, 44],
trimValue: true
});
ajaxify.data.category.tagWhitelist.forEach(function (tag) {
tagEl.tagsinput('add', tag);
});
tagEl.on('itemAdded itemRemoved', function (event) {
modified(tagEl);
});
}
Category.setupPrivilegeTable = function () { Category.setupPrivilegeTable = function () {
$('.privilege-table-container').on('change', 'input[type="checkbox"]', function () { $('.privilege-table-container').on('change', 'input[type="checkbox"]', function () {
var checkboxEl = $(this), var checkboxEl = $(this),

@ -101,7 +101,7 @@
}, },
cleanUpTag: function (tag, maxLength) { cleanUpTag: function (tag, maxLength) {
if (typeof tag !== 'string' || !tag.length ) { if (typeof tag !== 'string' || !tag.length) {
return ''; return '';
} }

@ -141,6 +141,9 @@ var privileges = require('./privileges');
parents: function (next) { parents: function (next) {
Categories.getParents(cids, next); Categories.getParents(cids, next);
}, },
tagWhitelist: function (next) {
Categories.getTagWhitelist(cids, next);
},
hasRead: function (next) { hasRead: function (next) {
Categories.hasReadCategories(cids, uid, next); Categories.hasReadCategories(cids, uid, next);
} }
@ -149,20 +152,26 @@ var privileges = require('./privileges');
return callback(err); return callback(err);
} }
var categories = results.categories;
var hasRead = results.hasRead;
uid = parseInt(uid, 10); uid = parseInt(uid, 10);
for(var i = 0; i < results.categories.length; ++i) { results.categories.forEach(function (category, i) {
if (categories[i]) { if (category) {
categories[i]['unread-class'] = (parseInt(categories[i].topic_count, 10) === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread'; category['unread-class'] = (parseInt(category.topic_count, 10) === 0 || (results.hasRead[i] && uid !== 0)) ? '' : 'unread';
categories[i].children = results.children[i]; category.children = results.children[i];
categories[i].parent = results.parents[i] || undefined; category.parent = results.parents[i] || undefined;
calculateTopicPostCount(categories[i]); category.tagWhitelist = results.tagWhitelist[i];
calculateTopicPostCount(category);
} }
} });
callback(null, results.categories);
});
};
callback(null, categories); Categories.getTagWhitelist = function (cids, callback) {
var keys = cids.map(function (cid) {
return 'cid:' + cid + ':tag:whitelist';
}); });
db.getSortedSetsMembers(keys, callback);
}; };
function calculateTopicPostCount(category) { function calculateTopicPostCount(category) {

@ -54,6 +54,7 @@ module.exports = function (Categories) {
'cid:' + cid + ':read_by_uid', 'cid:' + cid + ':read_by_uid',
'cid:' + cid + ':ignorers', 'cid:' + cid + ':ignorers',
'cid:' + cid + ':children', 'cid:' + cid + ':children',
'cid:' + cid + ':tag:whitelist',
'category:' + cid 'category:' + cid
], next); ], next);
}, },

@ -2,7 +2,9 @@
'use strict'; 'use strict';
var async = require('async'); var async = require('async');
var db = require('../database'); var db = require('../database');
var meta = require('../meta');
var utils = require('../../public/src/utils'); var utils = require('../../public/src/utils');
var translator = require('../../public/src/modules/translator'); var translator = require('../../public/src/modules/translator');
var plugins = require('../plugins'); var plugins = require('../plugins');
@ -66,6 +68,8 @@ module.exports = function (Categories) {
function updateCategoryField(cid, key, value, callback) { function updateCategoryField(cid, key, value, callback) {
if (key === 'parentCid') { if (key === 'parentCid') {
return updateParent(cid, value, callback); return updateParent(cid, value, callback);
} else if (key === 'tagWhitelist') {
return updateTagWhitelist(cid, value, callback);
} }
async.waterfall([ async.waterfall([
@ -112,6 +116,25 @@ module.exports = function (Categories) {
}); });
} }
function updateTagWhitelist(cid, tags, callback) {
tags = tags.split(',');
tags = tags.map(function (tag) {
return utils.cleanUpTag(tag, meta.config.maximumTagLength);
}).filter(Boolean);
async.waterfall([
function (next) {
db.delete('cid:' + cid + ':tag:whitelist', next);
},
function (next) {
var scores = tags.map(function (tag, index) {
return index;
});
db.sortedSetAdd('cid:' + cid + ':tag:whitelist', scores, tags, next);
}
], callback);
}
function updateOrder(cid, order, callback) { function updateOrder(cid, order, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {

@ -112,12 +112,10 @@
return callback(err); return callback(err);
} }
createSessionStore(); createSessionStore();
createIndices();
}); });
} else { } else {
winston.warn('You have no mongo password setup!'); winston.warn('You have no mongo password setup!');
createSessionStore(); createSessionStore();
createIndices();
} }
function createSessionStore() { function createSessionStore() {
@ -137,37 +135,44 @@
db: db db: db
}); });
} }
callback();
} }
});
};
function createIndices() { module.createIndices = function (callback) {
winston.info('[database] Checking database indices.'); function createIndex(collection, index, options, callback) {
async.series([ module.client.collection(collection).createIndex(index, options, callback);
async.apply(createIndex, 'objects', {_key: 1, score: -1}, {background: true}), }
async.apply(createIndex, 'objects', {_key: 1, value: -1}, {background: true, unique: true, sparse: true}),
async.apply(createIndex, 'objects', {expireAt: 1}, {expireAfterSeconds: 0, background: true})
], function (err) {
if (err) {
winston.error('Error creating index ' + err.message);
}
winston.info('[database] Checking database indices done!');
callback(err);
});
}
function createIndex(collection, index, options, callback) { if (!module.client) {
db.collection(collection).createIndex(index, options, callback); winston.warn('[database/createIndices] database not initialized');
return callback();
}
winston.info('[database] Checking database indices.');
async.series([
async.apply(createIndex, 'objects', {_key: 1, score: -1}, {background: true}),
async.apply(createIndex, 'objects', {_key: 1, value: -1}, {background: true, unique: true, sparse: true}),
async.apply(createIndex, 'objects', {expireAt: 1}, {expireAfterSeconds: 0, background: true})
], function (err) {
if (err) {
winston.error('Error creating index ' + err.message);
return callback(err);
} }
winston.info('[database] Checking database indices done!');
callback();
}); });
}; };
module.checkCompatibility = function (callback) { module.checkCompatibility = function (callback) {
var mongoPkg = require.main.require('./node_modules/mongodb/package.json'), var mongoPkg = require.main.require('./node_modules/mongodb/package.json');
err = semver.lt(mongoPkg.version, '2.0.0') ? new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.') : null;
if (err) { if (semver.lt(mongoPkg.version, '2.0.0')) {
err.stacktrace = false; return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.'));
} }
callback(err);
callback();
}; };
module.info = function (db, callback) { module.info = function (db, callback) {

@ -2,13 +2,13 @@
(function (module) { (function (module) {
var winston = require('winston'), var winston = require('winston');
nconf = require('nconf'), var nconf = require('nconf');
semver = require('semver'), var semver = require('semver');
session = require('express-session'), var session = require('express-session');
redis, var redis;
connectRedis, var connectRedis;
redisClient; var redisClient;
module.questions = [ module.questions = [
{ {
@ -107,6 +107,10 @@
return cxn; return cxn;
}; };
module.createIndices = function (callback) {
setImmediate(callback);
};
module.checkCompatibility = function (callback) { module.checkCompatibility = function (callback) {
module.info(module.client, function (err, info) { module.info(module.client, function (err, info) {
if (err) { if (err) {
@ -114,11 +118,10 @@
} }
if (semver.lt(info.redis_version, '2.8.9')) { if (semver.lt(info.redis_version, '2.8.9')) {
err = new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'); return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
err.stacktrace = false;
} }
callback(err); callback();
}); });
}; };

@ -1,16 +1,15 @@
'use strict'; 'use strict';
var async = require('async'), var async = require('async');
fs = require('fs'), var fs = require('fs');
path = require('path'), var path = require('path');
prompt = require('prompt'), var prompt = require('prompt');
winston = require('winston'), var winston = require('winston');
nconf = require('nconf'), var nconf = require('nconf');
utils = require('../public/src/utils.js'); var utils = require('../public/src/utils.js');
var install = {};
var install = {}, var questions = {};
questions = {};
questions.main = [ questions.main = [
{ {
@ -124,40 +123,33 @@ function setupConfig(next) {
prompt.delimiter = ''; prompt.delimiter = '';
prompt.colors = false; prompt.colors = false;
if (!install.values) { async.waterfall([
prompt.get(questions.main, function (err, config) { function (next) {
if (err) { if (install.values) {
process.stdout.write('\n\n'); // Use provided values, fall back to defaults
winston.warn('NodeBB setup ' + err.message); var config = {};
process.exit(); var redisQuestions = require('./database/redis').questions;
var mongoQuestions = require('./database/mongo').questions;
var allQuestions = questions.main.concat(questions.optional).concat(redisQuestions).concat(mongoQuestions);
allQuestions.forEach(function (question) {
config[question.name] = install.values[question.name] || question['default'] || undefined;
});
setImmediate(next, null, config);
} else {
prompt.get(questions.main, next);
} }
},
configureDatabases(config, function (err, config) { function (config, next) {
completeConfigSetup(err, config, next); configureDatabases(config, next);
}); },
}); function (config, next) {
} else { completeConfigSetup(config, next);
// Use provided values, fall back to defaults }
var config = {}, ], next);
redisQuestions = require('./database/redis').questions,
mongoQuestions = require('./database/mongo').questions,
allQuestions = questions.main.concat(questions.optional).concat(redisQuestions).concat(mongoQuestions);
allQuestions.forEach(function (question) {
config[question.name] = install.values[question.name] || question['default'] || undefined;
});
configureDatabases(config, function (err, config) {
completeConfigSetup(err, config, next);
});
}
} }
function completeConfigSetup(err, config, next) { function completeConfigSetup(config, next) {
if (err) {
return next(err);
}
// Add CI object // Add CI object
if (install.ciVals) { if (install.ciVals) {
config.test_database = {}; config.test_database = {};
@ -168,13 +160,17 @@ function completeConfigSetup(err, config, next) {
} }
} }
install.save(config, function (err) { async.waterfall([
if (err) { function (next) {
return next(err); install.save(config, next);
},
function (next) {
require('./database').init(next);
},
function (next) {
require('./database').createIndices(next);
} }
], next);
require('./database').init(next);
});
} }
function setupDefaultConfigs(next) { function setupDefaultConfigs(next) {
@ -491,7 +487,6 @@ function setCopyrightWidget(next) {
install.setup = function (callback) { install.setup = function (callback) {
async.series([ async.series([
checkSetupFlag, checkSetupFlag,
checkCIFlag, checkCIFlag,

@ -78,6 +78,7 @@ module.exports = function (Meta) {
acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n'; acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n';
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n'; acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n';
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";'; acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
minify(acpSource, paths, 'acpCache', callback); minify(acpSource, paths, 'acpCache', callback);
} }

@ -1,10 +1,29 @@
'use strict'; 'use strict';
var async = require('async'); var async = require('async');
var db = require('../../database');
var topics = require('../../topics'); var topics = require('../../topics');
var utils = require('../../../public/src/utils'); var utils = require('../../../public/src/utils');
module.exports = function (SocketTopics) { module.exports = function (SocketTopics) {
SocketTopics.isTagAllowed = function (socket, data, callback) {
if (!data || !data.cid || !data.tag) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
db.getSortedSetRange('cid:' + data.cid + ':tag:whitelist', 0, -1, next);
},
function (tagWhitelist, next) {
if (!tagWhitelist.length) {
return next(null, true);
}
next(null, tagWhitelist.indexOf(data.tag) !== -1);
}
], callback);
};
SocketTopics.autocompleteTags = function (socket, data, callback) { SocketTopics.autocompleteTags = function (socket, data, callback) {
topics.autocompleteTags(data, callback); topics.autocompleteTags(data, callback);
}; };

@ -31,6 +31,10 @@ module.exports = function (Topics) {
return tag && tag.length >= (meta.config.minimumTagLength || 3) && array.indexOf(tag) === index; return tag && tag.length >= (meta.config.minimumTagLength || 3) && array.indexOf(tag) === index;
}); });
filterCategoryTags(tags, tid, next);
},
function (_tags, next) {
tags = _tags;
var keys = tags.map(function (tag) { var keys = tags.map(function (tag) {
return 'tag:' + tag + ':topics'; return 'tag:' + tag + ':topics';
}); });
@ -39,15 +43,35 @@ module.exports = function (Topics) {
async.apply(db.setAdd, 'topic:' + tid + ':tags', tags), async.apply(db.setAdd, 'topic:' + tid + ':tags', tags),
async.apply(db.sortedSetsAdd, keys, timestamp, tid) async.apply(db.sortedSetsAdd, keys, timestamp, tid)
], function (err) { ], function (err) {
if (err) { next(err);
return next(err);
}
async.each(tags, updateTagCount, next);
}); });
},
function (next) {
async.each(tags, updateTagCount, next);
} }
], callback); ], callback);
}; };
function filterCategoryTags(tags, tid, callback) {
async.waterfall([
function (next) {
Topics.getTopicField(tid, 'cid', next);
},
function (cid, next) {
db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, next);
},
function (tagWhitelist, next) {
if (!tagWhitelist.length) {
return next(null, tags);
}
tags = tags.filter(function (tag) {
return tagWhitelist.indexOf(tag) !== -1;
});
next(null, tags);
}
], callback);
}
Topics.createEmptyTag = function (tag, callback) { Topics.createEmptyTag = function (tag, callback) {
if (!tag) { if (!tag) {
return callback(new Error('[[error:invalid-tag]]')); return callback(new Error('[[error:invalid-tag]]'));
@ -273,7 +297,7 @@ module.exports = function (Topics) {
if (plugins.hasListeners('filter:topics.searchTags')) { if (plugins.hasListeners('filter:topics.searchTags')) {
plugins.fireHook('filter:topics.searchTags', {data: data}, next); plugins.fireHook('filter:topics.searchTags', {data: data}, next);
} else { } else {
findMatches(data.query, next); findMatches(data.query, 0, next);
} }
}, },
function (result, next) { function (result, next) {
@ -295,7 +319,7 @@ module.exports = function (Topics) {
if (plugins.hasListeners('filter:topics.autocompleteTags')) { if (plugins.hasListeners('filter:topics.autocompleteTags')) {
plugins.fireHook('filter:topics.autocompleteTags', {data: data}, next); plugins.fireHook('filter:topics.autocompleteTags', {data: data}, next);
} else { } else {
findMatches(data.query, next); findMatches(data.query, data.cid, next);
} }
}, },
function (result, next) { function (result, next) {
@ -304,10 +328,21 @@ module.exports = function (Topics) {
], callback); ], callback);
}; };
function findMatches(query, callback) { function findMatches(query, cid, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
db.getSortedSetRevRange('tags:topic:count', 0, -1, next); if (parseInt(cid, 10)) {
db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, next);
} else {
setImmediate(next, null, []);
}
},
function (tagWhitelist, next) {
if (tagWhitelist.length) {
setImmediate(next, null, tagWhitelist);
} else {
db.getSortedSetRevRange('tags:topic:count', 0, -1, next);
}
}, },
function (tags, next) { function (tags, next) {
query = query.toLowerCase(); query = query.toLowerCase();

@ -59,6 +59,10 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset>
<label for="tag-whitelist">Tag Whitelist</label><br />
<input id="tag-whitelist" type="text" class="form-control" placeholder="Enter category tags here" data-name="tagWhitelist" value="" />
</fieldset>
</div> </div>
</div> </div>

@ -541,7 +541,80 @@ describe('Categories', function () {
}); });
}); });
}); });
});
describe('tag whitelist', function () {
var cid;
var socketTopics = require('../src/socket.io/topics');
before(function (done) {
Categories.create({
name: 'test'
}, function (err, category) {
assert.ifError(err);
cid = category.cid;
done();
});
});
it('should error if data is invalid', function (done) {
socketTopics.isTagAllowed({uid: posterUid}, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should return true if category whitelist is empty', function (done) {
socketTopics.isTagAllowed({uid: posterUid}, {tag: 'notallowed', cid: cid}, function (err, allowed) {
assert.ifError(err);
assert(allowed);
done();
});
});
it('should add tags to category whitelist', function (done) {
var data = {};
data[cid] = {
tagWhitelist: 'nodebb,jquery,javascript'
};
Categories.update(data, function (err) {
assert.ifError(err);
db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, function (err, tagWhitelist) {
assert.ifError(err);
assert.deepEqual(['nodebb', 'jquery', 'javascript'], tagWhitelist);
done();
});
});
});
it('should return false if category whitelist does not have tag', function (done) {
socketTopics.isTagAllowed({uid: posterUid}, {tag: 'notallowed', cid: cid}, function (err, allowed) {
assert.ifError(err);
assert(!allowed);
done();
});
});
it('should return true if category whitelist has tag', function (done) {
socketTopics.isTagAllowed({uid: posterUid}, {tag: 'nodebb', cid: cid}, function (err, allowed) {
assert.ifError(err);
assert(allowed);
done();
});
});
it('should post a topic with only allowed tags', function (done) {
Topics.post({
uid: posterUid,
cid: cid,
title: 'Test Topic Title',
content: 'The content of test topic',
tags: ['nodebb', 'jquery', 'notallowed']
}, function (err, data) {
assert.ifError(err);
assert.equal(data.topicData.tags.length, 2);
done();
});
});
}); });

Loading…
Cancel
Save