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,
timestamp: function () {
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'),
json: (!!nconf.get('json-logging')),
@ -180,21 +180,26 @@ function start() {
});
async.waterfall([
async.apply(db.init),
async.apply(db.checkCompatibility),
function (next) {
require('./src/meta').configs.init(next);
},
async.apply(db.init),
function (next) {
if (nconf.get('dep-check') === undefined || nconf.get('dep-check') !== false) {
require('./src/meta').dependencies.check(next);
} else {
winston.warn('[init] Dependency checking skipped!');
setImmediate(next);
}
},
function (next) {
require('./src/upgrade').check(next);
var meta = require('./src/meta');
async.parallel([
async.apply(db.checkCompatibility),
async.apply(meta.configs.init),
function (next) {
if (nconf.get('dep-check') === undefined || nconf.get('dep-check') !== false) {
meta.dependencies.check(next);
} else {
winston.warn('[init] Dependency checking skipped!');
setImmediate(next);
}
},
function (next) {
require('./src/upgrade').check(next);
}
], function (err) {
next(err);
});
},
function (next) {
var webserver = require('./src/webserver');

@ -52,7 +52,7 @@
"morgan": "^1.3.2",
"mousetrap": "^1.5.3",
"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-emoji-extended": "1.1.1",
"nodebb-plugin-emoji-one": "1.1.5",

@ -8,44 +8,9 @@ define('admin/manage/category', [
'autocomplete'
], function (uploader, iconSelect, colorpicker, autocomplete) {
var Category = {};
var modified_categories = {};
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 () {
var $this = $(this);
@ -85,7 +50,28 @@ define('admin/manage/category', [
$('[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) {
e.preventDefault();
@ -171,8 +157,37 @@ define('admin/manage/category', [
});
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 () {
$('.privilege-table-container').on('change', 'input[type="checkbox"]', function () {
var checkboxEl = $(this),

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

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

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

@ -2,7 +2,9 @@
'use strict';
var async = require('async');
var db = require('../database');
var meta = require('../meta');
var utils = require('../../public/src/utils');
var translator = require('../../public/src/modules/translator');
var plugins = require('../plugins');
@ -66,6 +68,8 @@ module.exports = function (Categories) {
function updateCategoryField(cid, key, value, callback) {
if (key === 'parentCid') {
return updateParent(cid, value, callback);
} else if (key === 'tagWhitelist') {
return updateTagWhitelist(cid, value, callback);
}
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) {
async.waterfall([
function (next) {

@ -111,13 +111,11 @@
if (err) {
return callback(err);
}
createSessionStore();
createIndices();
createSessionStore();
});
} else {
winston.warn('You have no mongo password setup!');
createSessionStore();
createIndices();
}
function createSessionStore() {
@ -137,37 +135,44 @@
db: db
});
}
}
callback();
}
});
};
function createIndices() {
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);
}
winston.info('[database] Checking database indices done!');
callback(err);
});
}
module.createIndices = function (callback) {
function createIndex(collection, index, options, callback) {
module.client.collection(collection).createIndex(index, options, callback);
}
function createIndex(collection, index, options, callback) {
db.collection(collection).createIndex(index, options, callback);
if (!module.client) {
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) {
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) {
err.stacktrace = false;
var mongoPkg = require.main.require('./node_modules/mongodb/package.json');
if (semver.lt(mongoPkg.version, '2.0.0')) {
return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.'));
}
callback(err);
callback();
};
module.info = function (db, callback) {

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

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

@ -78,6 +78,7 @@ module.exports = function (Meta) {
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/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);
}

@ -1,10 +1,29 @@
'use strict';
var async = require('async');
var db = require('../../database');
var topics = require('../../topics');
var utils = require('../../../public/src/utils');
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) {
topics.autocompleteTags(data, callback);
};

@ -31,6 +31,10 @@ module.exports = function (Topics) {
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) {
return 'tag:' + tag + ':topics';
});
@ -39,15 +43,35 @@ module.exports = function (Topics) {
async.apply(db.setAdd, 'topic:' + tid + ':tags', tags),
async.apply(db.sortedSetsAdd, keys, timestamp, tid)
], function (err) {
if (err) {
return next(err);
}
async.each(tags, updateTagCount, next);
next(err);
});
},
function (next) {
async.each(tags, updateTagCount, next);
}
], 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) {
if (!tag) {
return callback(new Error('[[error:invalid-tag]]'));
@ -273,7 +297,7 @@ module.exports = function (Topics) {
if (plugins.hasListeners('filter:topics.searchTags')) {
plugins.fireHook('filter:topics.searchTags', {data: data}, next);
} else {
findMatches(data.query, next);
findMatches(data.query, 0, next);
}
},
function (result, next) {
@ -295,7 +319,7 @@ module.exports = function (Topics) {
if (plugins.hasListeners('filter:topics.autocompleteTags')) {
plugins.fireHook('filter:topics.autocompleteTags', {data: data}, next);
} else {
findMatches(data.query, next);
findMatches(data.query, data.cid, next);
}
},
function (result, next) {
@ -304,10 +328,21 @@ module.exports = function (Topics) {
], callback);
};
function findMatches(query, callback) {
function findMatches(query, cid, callback) {
async.waterfall([
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) {
query = query.toLowerCase();

@ -59,6 +59,10 @@
</div>
</div>
</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>

@ -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