diff --git a/package.json b/package.json index 180f23bbd9..6f03e3c465 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "morgan": "^1.3.2", "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "4.4.13", + "nodebb-plugin-composer-default": "4.4.14", "nodebb-plugin-dbsearch": "2.0.4", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.2.1", diff --git a/src/categories/create.js b/src/categories/create.js index 326545d76c..e489bfa815 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -52,7 +52,18 @@ module.exports = function (Categories) { function (data, next) { category = data.category; - var defaultPrivileges = ['find', 'read', 'topics:read', 'topics:create', 'topics:reply', 'posts:edit', 'posts:delete', 'topics:delete', 'upload:post:image']; + var defaultPrivileges = [ + 'find', + 'read', + 'topics:read', + 'topics:create', + 'topics:reply', + 'topics:tag', + 'posts:edit', + 'posts:delete', + 'topics:delete', + 'upload:post:image', + ]; async.series([ async.apply(db.setObject, 'category:' + category.cid, category), diff --git a/src/posts/edit.js b/src/posts/edit.js index 65ca05fc2f..0a9867feeb 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -48,9 +48,7 @@ module.exports = function (Posts) { }, function (result, next) { postData = result.post; - Posts.setPostFields(data.pid, postData, next); - }, - function (next) { + async.parallel({ editor: function (next) { user.getUserFields(data.uid, ['username', 'userslug'], next); @@ -62,7 +60,9 @@ module.exports = function (Posts) { }, function (_results, next) { results = _results; - + Posts.setPostFields(data.pid, postData, next); + }, + function (next) { postData.cid = results.topic.cid; postData.topic = results.topic; plugins.fireHook('action:post.edit', { post: _.clone(postData), uid: data.uid }); @@ -123,6 +123,17 @@ module.exports = function (Posts) { data.tags = data.tags || []; + if (!data.tags.length) { + return next(null, true); + } + + privileges.categories.can('topics:tag', topicData.cid, data.uid, next); + }, + function (canTag, next) { + if (!canTag) { + return next(new Error('[[error:no-privileges]]')); + } + plugins.fireHook('filter:topic.edit', { req: data.req, topic: topicData, data: data }, next); }, function (results, next) { diff --git a/src/privileges.js b/src/privileges.js index 63bae570e1..c1ac018ec7 100644 --- a/src/privileges.js +++ b/src/privileges.js @@ -2,12 +2,29 @@ var privileges = module.exports; +privileges.privilegeLabels = [ + { name: 'Find Category' }, + { name: 'Access Category' }, + { name: 'Access Topics' }, + { name: 'Create Topics' }, + { name: 'Reply to Topics' }, + { name: 'Tag Topics' }, + { name: 'Edit Posts' }, + { name: 'Delete Posts' }, + { name: 'Delete Topics' }, + { name: 'Upload Images' }, + { name: 'Upload Files' }, + { name: 'Purge' }, + { name: 'Moderate' }, +]; + privileges.userPrivilegeList = [ 'find', 'read', 'topics:read', 'topics:create', 'topics:reply', + 'topics:tag', 'posts:edit', 'posts:delete', 'topics:delete', diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 4ec3eacaf3..604c2c5ac7 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -16,28 +16,13 @@ module.exports = function (privileges) { privileges.categories.list = function (cid, callback) { // Method used in admin/category controller to show all users/groups with privs in that given cid - var privilegeLabels = [ - { name: 'Find Category' }, - { name: 'Access Category' }, - { name: 'Access Topics' }, - { name: 'Create Topics' }, - { name: 'Reply to Topics' }, - { name: 'Edit Posts' }, - { name: 'Delete Posts' }, - { name: 'Delete Topics' }, - { name: 'Upload Images' }, - { name: 'Upload Files' }, - { name: 'Purge' }, - { name: 'Moderate' }, - ]; - async.waterfall([ function (next) { async.parallel({ labels: function (next) { async.parallel({ - users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privilegeLabels), - groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privilegeLabels), + users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privileges.privilegeLabels), + groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privileges.privilegeLabels), }, next); }, users: function (next) { @@ -155,7 +140,7 @@ module.exports = function (privileges) { }; privileges.categories.get = function (cid, uid, callback) { - var privs = ['topics:create', 'topics:read', 'read']; + var privs = ['topics:create', 'topics:read', 'topics:tag', 'read']; async.waterfall([ function (next) { async.parallel({ @@ -177,6 +162,7 @@ module.exports = function (privileges) { plugins.fireHook('filter:privileges.categories.get', { 'topics:create': privData['topics:create'] || isAdminOrMod, 'topics:read': privData['topics:read'] || isAdminOrMod, + 'topics:tag': privData['topics:tag'] || isAdminOrMod, read: privData.read || isAdminOrMod, cid: cid, uid: uid, diff --git a/src/privileges/topics.js b/src/privileges/topics.js index 88273800fe..ac602cc25a 100644 --- a/src/privileges/topics.js +++ b/src/privileges/topics.js @@ -16,7 +16,7 @@ module.exports = function (privileges) { privileges.topics.get = function (tid, uid, callback) { var topic; - var privs = ['topics:reply', 'topics:read', 'topics:delete', 'posts:edit', 'posts:delete', 'read']; + var privs = ['topics:reply', 'topics:read', 'topics:tag', 'topics:delete', 'posts:edit', 'posts:delete', 'read']; async.waterfall([ async.apply(topics.getTopicFields, tid, ['cid', 'uid', 'locked', 'deleted']), function (_topic, next) { @@ -41,6 +41,7 @@ module.exports = function (privileges) { plugins.fireHook('filter:privileges.topics.get', { 'topics:reply': (privData['topics:reply'] && !locked && !deleted) || isAdminOrMod, 'topics:read': privData['topics:read'] || isAdminOrMod, + 'topics:tag': privData['topics:tag'] || isAdminOrMod, 'topics:delete': (isOwner && privData['topics:delete']) || isAdminOrMod, 'posts:edit': (privData['posts:edit'] && !locked) || isAdminOrMod, 'posts:delete': (privData['posts:delete'] && !locked) || isAdminOrMod, diff --git a/src/topics/create.js b/src/topics/create.js index 071093e967..e758f5f513 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -107,18 +107,30 @@ module.exports = function (Topics) { check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); }, function (next) { - categories.exists(data.cid, next); + async.parallel({ + categoryExists: function (next) { + categories.exists(data.cid, next); + }, + canCreate: function (next) { + privileges.categories.can('topics:create', data.cid, data.uid, next); + }, + canTag: function (next) { + if (!data.tags.length) { + return next(null, true); + } + privileges.categories.can('topics:tag', data.cid, data.uid, next); + }, + }, next); }, - function (categoryExists, next) { - if (!categoryExists) { + function (results, next) { + if (!results.categoryExists) { return next(new Error('[[error:no-category]]')); } - privileges.categories.can('topics:create', data.cid, data.uid, next); - }, - function (canCreate, next) { - if (!canCreate) { + + if (!results.canCreate || !results.canTag) { return next(new Error('[[error:no-privileges]]')); } + guestHandleValid(data, next); }, function (next) { diff --git a/src/upgrades/1.5.2/tags_privilege.js b/src/upgrades/1.5.2/tags_privilege.js new file mode 100644 index 0000000000..d5d703407c --- /dev/null +++ b/src/upgrades/1.5.2/tags_privilege.js @@ -0,0 +1,22 @@ +'use strict'; + +var async = require('async'); + +var batch = require('../../batch'); + +module.exports = { + name: 'Give tag privilege to registered-users on all categories', + timestamp: Date.UTC(2017, 5, 16), + method: function (callback) { + var progress = this.progress; + var privileges = require('../../privileges'); + batch.processSortedSet('categories:cid', function (cids, next) { + async.eachSeries(cids, function (cid, next) { + progress.incr(); + privileges.categories.give(['topics:tag'], cid, 'registered-users', next); + }, next); + }, { + progress: progress, + }, callback); + }, +}; diff --git a/test/categories.js b/test/categories.js index 18e125f3c7..53a867e858 100644 --- a/test/categories.js +++ b/test/categories.js @@ -645,6 +645,7 @@ describe('Categories', function () { 'topics:reply': false, 'topics:read': false, 'topics:create': false, + 'topics:tag': false, 'topics:delete': false, 'posts:edit': false, 'upload:post:file': false, @@ -666,6 +667,7 @@ describe('Categories', function () { 'groups:topics:delete': false, 'groups:topics:create': true, 'groups:topics:reply': true, + 'groups:topics:tag': true, 'groups:posts:delete': true, 'groups:read': true, 'groups:topics:read': true, diff --git a/test/topics.js b/test/topics.js index da3d148e70..3aed603c95 100644 --- a/test/topics.js +++ b/test/topics.js @@ -7,7 +7,9 @@ var nconf = require('nconf'); var db = require('./mocks/databasemock'); var topics = require('../src/topics'); +var posts = require('../src/posts'); var categories = require('../src/categories'); +var privileges = require('../src/privileges'); var meta = require('../src/meta'); var User = require('../src/user'); var groups = require('../src/groups'); @@ -825,7 +827,7 @@ describe('Topic\'s', function () { }); it('should 404 if tid is not a number', function (done) { - request(nconf.get('url') + '/api/topic/teaser/nan', { json: true }, function (err, response, body) { + request(nconf.get('url') + '/api/topic/teaser/nan', { json: true }, function (err, response) { assert.ifError(err); assert.equal(response.statusCode, 404); done(); @@ -858,7 +860,7 @@ describe('Topic\'s', function () { it('should 404 if tid is not a number', function (done) { - request(nconf.get('url') + '/api/topic/pagination/nan', { json: true }, function (err, response, body) { + request(nconf.get('url') + '/api/topic/pagination/nan', { json: true }, function (err, response) { assert.ifError(err); assert.equal(response.statusCode, 404); done(); @@ -866,7 +868,7 @@ describe('Topic\'s', function () { }); it('should 404 if tid does not exist', function (done) { - request(nconf.get('url') + '/api/topic/pagination/1231231', { json: true }, function (err, response, body) { + request(nconf.get('url') + '/api/topic/pagination/1231231', { json: true }, function (err, response) { assert.ifError(err); assert.equal(response.statusCode, 404); done(); @@ -1643,4 +1645,61 @@ describe('Topic\'s', function () { }); }); }); + + describe('tag privilege', function () { + var uid; + var cid; + before(function (done) { + async.waterfall([ + function (next) { + User.create({ username: 'tag_poster' }, next); + }, + function (_uid, next) { + uid = _uid; + categories.create({ name: 'tag category' }, next); + }, + function (categoryObj, next) { + cid = categoryObj.cid; + next(); + }, + ], done); + }); + + it('should fail to post if user does not have tag privilege', function (done) { + privileges.categories.rescind(['topics:tag'], cid, 'registered-users', function (err) { + assert.ifError(err); + topics.post({ uid: uid, cid: cid, tags: ['tag1'], title: 'topic with tags', content: 'some content here' }, function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + }); + + it('should fail to edit if user does not have tag privilege', function (done) { + topics.post({ uid: uid, cid: cid, title: 'topic with tags', content: 'some content here' }, function (err, result) { + assert.ifError(err); + var pid = result.postData.pid; + posts.edit({ pid: pid, uid: uid, content: 'edited content', tags: ['tag2'] }, function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + }); + + it('should be able to edit topic and add tags if allowed', function (done) { + privileges.categories.give(['topics:tag'], cid, 'registered-users', function (err) { + assert.ifError(err); + topics.post({ uid: uid, cid: cid, tags: ['tag1'], title: 'topic with tags', content: 'some content here' }, function (err, result) { + assert.ifError(err); + posts.edit({ pid: result.postData.pid, uid: uid, content: 'edited content', tags: ['tag1', 'tag2'] }, function (err, result) { + assert.ifError(err); + assert.deepEqual(result.topic.tags.map(function (tag) { + return tag.value; + }), ['tag1', 'tag2']); + done(); + }); + }); + }); + }); + }); });