From 541f51e947abfac5f3aa7b8c1138907df42f7803 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 3 Jan 2017 13:38:06 -0500 Subject: [PATCH] restrict flags interface so that moderators only see flags from cids they can actually moderate --- src/controllers/mods.js | 39 +++++++++++++++++++++++++++++-- src/flags.js | 52 ++++++++++++++++++++++++++++++++++------- test/flags.js | 35 +++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 10 deletions(-) diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 242d68d708..8056b8c5b5 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -27,6 +27,7 @@ modsController.flags.list = function (req, res, next) { } // Parse query string params for filters + var hasFilter = false; var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick']; var filters = valid.reduce(function (memo, cur) { if (req.query.hasOwnProperty(cur)) { @@ -35,6 +36,24 @@ modsController.flags.list = function (req, res, next) { return memo; }, {}); + hasFilter = !!Object.keys(filters).length; + + if (res.locals.cids) { + if (!filters.cid) { + // If mod and no cid filter, add filter for their modded categories + filters.cid = res.locals.cids; + } else { + // Remove cids they do not moderate + if (Array.isArray(filters.cid)) { + filters.cid = filters.cid.filter(function (cid) { + return res.locals.cids.indexOf(String(cid)) !== -1; + }); + } else if (res.locals.cids.indexOf(String(filters.cid)) === -1) { + filters.cid = res.locals.cids; + hasFilter = false; + } + } + } async.parallel({ flags: async.apply(flags.list, filters, req.uid), @@ -45,9 +64,25 @@ modsController.flags.list = function (req, res, next) { return next(err); } + // If res.locals.cids is populated, then slim down the categories list + if (res.locals.cids) { + data.categories = data.categories.filter(function (category) { + return res.locals.cids.indexOf(String(category.cid)) !== -1; + }); + } + // Minimal returned set for templates.js data.categories = data.categories.reduce(function (memo, cur) { - memo[cur.cid] = cur.name; + if (!res.locals.cids) { + memo[cur.cid] = cur.name; + return memo; + } + + // If mod, remove categories they can't moderate + if (res.locals.cids.indexOf(String(cur.cid)) !== -1) { + memo[cur.cid] = cur.name; + } + return memo; }, {}); @@ -55,7 +90,7 @@ modsController.flags.list = function (req, res, next) { flags: data.flags, analytics: data.analytics, categories: data.categories, - hasFilter: !!Object.keys(filters).length, + hasFilter: hasFilter, filters: filters, title: '[[pages:flags]]' }); diff --git a/src/flags.js b/src/flags.js index ecc5a84e3e..168eb5a11e 100644 --- a/src/flags.js +++ b/src/flags.js @@ -53,31 +53,45 @@ Flags.list = function (filters, uid, callback) { } var sets = []; + var orSets = []; + var prepareSets = function (setPrefix, value) { + if (!Array.isArray(value)) { + sets.push(setPrefix + value); + } else if (value.length) { + value.forEach(function (x) { + orSets.push(setPrefix + x); + }); + } else { + // Empty array, do nothing + return; + } + }; + if (Object.keys(filters).length > 0) { for (var type in filters) { switch (type) { case 'type': - sets.push('flags:byType:' + filters[type]); + prepareSets('flags:byType:', filters[type]); break; case 'state': - sets.push('flags:byState:' + filters[type]); + prepareSets('flags:byState:', filters[type]); break; case 'reporterId': - sets.push('flags:byReporter:' + filters[type]); + prepareSets('flags:byReporter:', filters[type]); break; case 'assignee': - sets.push('flags:byAssignee:' + filters[type]); + prepareSets('flags:byAssignee:', filters[type]); break; case 'targetUid': - sets.push('flags:byTargetUid:' + filters[type]); + prepareSets('flags:byTargetUid:', filters[type]); break; case 'cid': - sets.push('flags:byCid:' + filters[type]); + prepareSets('flags:byCid:', filters[type]); break; case 'quick': @@ -90,14 +104,36 @@ Flags.list = function (filters, uid, callback) { } } } - sets = sets.length ? sets : ['flags:datetime']; // No filter default + sets = (sets.length || orSets.length) ? sets : ['flags:datetime']; // No filter default async.waterfall([ function (next) { if (sets.length === 1) { db.getSortedSetRevRange(sets[0], 0, -1, next); + } else if (sets.length > 1) { + db.getSortedSetRevIntersect({ sets: sets, start: 0, stop: -1, aggregate: 'MAX' }, next); + } else { + next(null, []); + } + }, + function (flagIds, next) { + // Find flags according to "or" rules, if any + if (orSets.length) { + db.getSortedSetRevUnion({ sets: orSets, start: 0, stop: -1, aggregate: 'MAX' }, function (err, _flagIds) { + if (err) { + return next(err); + } + + if (sets.length) { + // If flag ids are already present, return a subset of flags that are in both sets + next(null, _.intersection(flagIds, _flagIds)); + } else { + // Otherwise, return all flags returned via orSets + next(null, _.union(flagIds, _flagIds)); + } + }); } else { - db.getSortedSetRevIntersect({sets: sets, start: 0, stop: -1, aggregate: 'MAX'}, next); + setImmediate(next, null, flagIds); } }, function (flagIds, next) { diff --git a/test/flags.js b/test/flags.js index dfa6f1a773..9df1a11a2b 100644 --- a/test/flags.js +++ b/test/flags.js @@ -203,6 +203,41 @@ describe('Flags', function () { done(); }); }); + + it('should return a flag when filtered by both cid 1 and 2', function (done) { + Flags.list({ + cid: [1, 2] + }, 1, function (err, flags) { + assert.ifError(err); + assert.ok(Array.isArray(flags)); + assert.strictEqual(1, flags.length); + done(); + }); + }); + + it('should return one flag if filtered by both cid 1 and 2 and open state', function (done) { + Flags.list({ + cid: [1, 2], + state: 'open' + }, 1, function (err, flags) { + assert.ifError(err); + assert.ok(Array.isArray(flags)); + assert.strictEqual(1, flags.length); + done(); + }); + }); + + it('should return no flag if filtered by both cid 1 and 2 and non-open state', function (done) { + Flags.list({ + cid: [1, 2], + state: 'resolved' + }, 1, function (err, flags) { + assert.ifError(err); + assert.ok(Array.isArray(flags)); + assert.strictEqual(0, flags.length); + done(); + }); + }); }); });