diff --git a/public/language/zh-CN/admin/extend/plugins.json b/public/language/zh-CN/admin/extend/plugins.json
index 933e82c4b7..f5cdf59dbc 100644
--- a/public/language/zh-CN/admin/extend/plugins.json
+++ b/public/language/zh-CN/admin/extend/plugins.json
@@ -13,7 +13,7 @@
"reorder-plugins": "重新排序插件",
"order-active": "排序生效插件",
"dev-interested": "有兴趣为NodeBB开发插件?",
- "docs-info": "有关插件创作的完整文档可以在 NodeBB 文档中找到。",
+ "docs-info": "有关插件创作的完整文档可以在 NodeBB 文档中找到。",
"order.description": "部分插件需要在其它插件启用之后才能完美运作。",
"order.explanation": "插件将按照以下顺序载入,从上至下。",
diff --git a/src/flags.js b/src/flags.js
index 489ebbf9d9..2f5264678d 100644
--- a/src/flags.js
+++ b/src/flags.js
@@ -19,6 +19,16 @@ const utils = require('../public/src/utils');
const Flags = module.exports;
+Flags._constants = {
+ states: ['open', 'wip', 'resolved', 'rejected'],
+ state_class: {
+ open: 'info',
+ wip: 'warning',
+ resolved: 'success',
+ rejected: 'danger',
+ },
+};
+
Flags.init = async function () {
// Query plugins for custom filter strategies and merge into core filter strategies
function prepareSets(sets, orSets, prefix, value) {
@@ -162,13 +172,7 @@ Flags.list = async function (filters, uid) {
'icon:text': userObj['icon:text'],
},
};
- const stateToLabel = {
- open: 'info',
- wip: 'warning',
- resolved: 'success',
- rejected: 'danger',
- };
- flagObj.labelClass = stateToLabel[flagObj.state];
+ flagObj.labelClass = Flags._constants.state_class[flagObj.state];
return Object.assign(flagObj, {
description: validator.escape(String(flagObj.description)),
@@ -344,6 +348,7 @@ Flags.getTargetCid = async function (type, id) {
};
Flags.update = async function (flagId, uid, changeset) {
+ const current = await db.getObjectFields('flag:' + flagId, ['state', 'assignee', 'type', 'targetId']);
const now = changeset.datetime || Date.now();
const notifyAssignee = async function (assigneeId) {
if (assigneeId === '' || parseInt(uid, 10) === parseInt(assigneeId, 10)) {
@@ -359,20 +364,40 @@ Flags.update = async function (flagId, uid, changeset) {
});
await notifications.push(notifObj, [assigneeId]);
};
+ const isAssignable = async function (assigneeId) {
+ let allowed = false;
+ allowed = await user.isAdminOrGlobalMod(assigneeId);
+
+ // Mods are also allowed to be assigned, if flag target is post in uid's moderated cid
+ if (!allowed && current.type === 'post') {
+ const cid = await posts.getCidByPid(current.targetId);
+ allowed = await user.isModerator(assigneeId, cid);
+ }
+
+ return allowed;
+ };
- // Retrieve existing flag data to compare for history-saving purposes
- const current = await db.getObjectFields('flag:' + flagId, ['state', 'assignee']);
+ // Retrieve existing flag data to compare for history-saving/reference purposes
const tasks = [];
for (var prop in changeset) {
if (changeset.hasOwnProperty(prop)) {
if (current[prop] === changeset[prop]) {
delete changeset[prop];
} else if (prop === 'state') {
- tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId));
- tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId));
+ if (!Flags._constants.states.includes(changeset[prop])) {
+ delete changeset[prop];
+ } else {
+ tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId));
+ tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId));
+ }
} else if (prop === 'assignee') {
- tasks.push(db.sortedSetAdd('flags:byAssignee:' + changeset[prop], now, flagId));
- tasks.push(notifyAssignee(changeset[prop]));
+ /* eslint-disable-next-line */
+ if (!await isAssignable(parseInt(changeset[prop], 10))) {
+ delete changeset[prop];
+ } else {
+ tasks.push(db.sortedSetAdd('flags:byAssignee:' + changeset[prop], now, flagId));
+ tasks.push(notifyAssignee(changeset[prop]));
+ }
}
}
}
diff --git a/src/meta/build.js b/src/meta/build.js
index 0a99bd5837..f9370d1b9f 100644
--- a/src/meta/build.js
+++ b/src/meta/build.js
@@ -1,5 +1,6 @@
'use strict';
+const os = require('os');
const async = require('async');
const winston = require('winston');
const nconf = require('nconf');
@@ -150,7 +151,14 @@ exports.build = function (targets, options, callback) {
targets = targets.split(',');
}
- var parallel = !nconf.get('series') && !options.series;
+ let series = nconf.get('series') || options.series;
+ if (series === undefined) {
+ // Detect # of CPUs and select strategy as appropriate
+ winston.verbose('[build] Querying CPU core count for build strategy');
+ const cpus = os.cpus();
+ series = cpus.length < 4;
+ winston.verbose('[build] System returned ' + cpus.length + ' cores, opting for ' + (series ? 'series' : 'parallel') + ' build strategy');
+ }
targets = targets
// get full target name
@@ -195,14 +203,14 @@ exports.build = function (targets, options, callback) {
require('./minifier').maxThreads = threads - 1;
}
- if (parallel) {
+ if (!series) {
winston.info('[build] Building in parallel mode');
} else {
winston.info('[build] Building in series mode');
}
startTime = Date.now();
- buildTargets(targets, parallel, next);
+ buildTargets(targets, !series, next);
},
function (next) {
totalTime = (Date.now() - startTime) / 1000;
diff --git a/test/flags.js b/test/flags.js
index b157ba26c0..75e0285412 100644
--- a/test/flags.js
+++ b/test/flags.js
@@ -13,40 +13,30 @@ var Groups = require('../src/groups');
var Meta = require('../src/meta');
describe('Flags', function () {
- before(function (done) {
+ let uid1;
+ let uid2;
+ let uid3;
+ let category;
+ before(async () => {
// Create some stuff to flag
- async.waterfall([
- async.apply(User.create, { username: 'testUser', password: 'abcdef', email: 'b@c.com' }),
- function (uid, next) {
- Categories.create({
- name: 'test category',
- }, function (err, category) {
- if (err) {
- return done(err);
- }
+ uid1 = await User.create({ username: 'testUser', password: 'abcdef', email: 'b@c.com' });
- Topics.post({
- cid: category.cid,
- uid: uid,
- title: 'Topic to flag',
- content: 'This is flaggable content',
- }, next);
- });
- },
- function (topicData, next) {
- User.create({
- username: 'testUser2', password: 'abcdef', email: 'c@d.com',
- }, next);
- },
- function (uid, next) {
- Groups.join('administrators', uid, next);
- },
- function (next) {
- User.create({
- username: 'unprivileged', password: 'abcdef', email: 'd@e.com',
- }, next);
- },
- ], done);
+ uid2 = await User.create({ username: 'testUser2', password: 'abcdef', email: 'c@d.com' });
+ await Groups.join('administrators', uid2);
+
+ category = await Categories.create({
+ name: 'test category',
+ });
+ await Topics.post({
+ cid: category.cid,
+ uid: uid1,
+ title: 'Topic to flag',
+ content: 'This is flaggable content',
+ });
+
+ uid3 = await User.create({
+ username: 'unprivileged', password: 'abcdef', email: 'd@e.com',
+ });
});
describe('.create()', function () {
@@ -274,9 +264,9 @@ describe('Flags', function () {
describe('.update()', function () {
it('should alter a flag\'s various attributes and persist them to the database', function (done) {
- Flags.update(1, 1, {
+ Flags.update(1, uid2, {
state: 'wip',
- assignee: 1,
+ assignee: uid2,
}, function (err) {
assert.ifError(err);
db.getObjectFields('flag:1', ['state', 'assignee'], function (err, data) {
@@ -286,7 +276,7 @@ describe('Flags', function () {
assert.strictEqual('wip', data.state);
assert.ok(!isNaN(parseInt(data.assignee, 10)));
- assert.strictEqual(1, parseInt(data.assignee, 10));
+ assert.strictEqual(uid2, parseInt(data.assignee, 10));
done();
});
});
@@ -313,6 +303,65 @@ describe('Flags', function () {
done();
});
});
+
+ it('should allow assignment if user is an admin and do nothing otherwise', async () => {
+ await Flags.update(1, uid2, {
+ assignee: uid2,
+ });
+ let assignee = await db.getObjectField('flag:1', 'assignee');
+ assert.strictEqual(uid2, parseInt(assignee, 10));
+
+ await Flags.update(1, uid2, {
+ assignee: uid3,
+ });
+ assignee = await db.getObjectField('flag:1', 'assignee');
+ assert.strictEqual(uid2, parseInt(assignee, 10));
+ });
+
+ it('should allow assignment if user is a global mod and do nothing otherwise', async () => {
+ await Groups.join('Global Moderators', uid3);
+
+ await Flags.update(1, uid3, {
+ assignee: uid3,
+ });
+ let assignee = await db.getObjectField('flag:1', 'assignee');
+ assert.strictEqual(uid3, parseInt(assignee, 10));
+
+ await Flags.update(1, uid3, {
+ assignee: uid1,
+ });
+ assignee = await db.getObjectField('flag:1', 'assignee');
+ assert.strictEqual(uid3, parseInt(assignee, 10));
+
+ await Groups.leave('Global Moderators', uid3);
+ });
+
+ it('should allow assignment if user is a mod of the category, do nothing otherwise', async () => {
+ await Groups.join('cid:' + category.cid + ':privileges:moderate', uid3);
+
+ await Flags.update(1, uid3, {
+ assignee: uid3,
+ });
+ let assignee = await db.getObjectField('flag:1', 'assignee');
+ assert.strictEqual(uid3, parseInt(assignee, 10));
+
+ await Flags.update(1, uid3, {
+ assignee: uid1,
+ });
+ assignee = await db.getObjectField('flag:1', 'assignee');
+ assert.strictEqual(uid3, parseInt(assignee, 10));
+
+ await Groups.leave('cid:' + category.cid + ':privileges:moderate', uid3);
+ });
+
+ it('should do nothing when you attempt to set a bogus state', async () => {
+ await Flags.update(1, uid2, {
+ state: 'hocus pocus',
+ });
+
+ const state = await db.getObjectField('flag:1', 'state');
+ assert.strictEqual('wip', state);
+ });
});
describe('.getTarget()', function () {