diff --git a/install/package.json b/install/package.json index c5b8cab9b4..e8fa9735fc 100644 --- a/install/package.json +++ b/install/package.json @@ -59,7 +59,7 @@ "morgan": "^1.9.0", "mousetrap": "^1.6.1", "nconf": "^0.9.1", - "nodebb-plugin-composer-default": "6.0.7", + "nodebb-plugin-composer-default": "6.0.8", "nodebb-plugin-dbsearch": "2.0.9", "nodebb-plugin-emoji": "2.0.9", "nodebb-plugin-emoji-android": "2.0.0", diff --git a/src/categories/create.js b/src/categories/create.js index d4a74084d9..9cd698b6f3 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -62,7 +62,6 @@ module.exports = function (Categories) { 'posts:edit', 'posts:delete', 'topics:delete', - 'upload:post:image', ]; async.series([ diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index d717f6267a..ff5a07d157 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -20,12 +20,6 @@ chatsController.get = function (req, res, callback) { async.waterfall([ function (next) { - privileges.global.can('chat', req.uid, next); - }, - function (canChat, next) { - if (!canChat) { - return next(new Error('[[error:no-privileges]]')); - } user.getUidByUserslug(req.params.userslug, next); }, function (_uid, next) { @@ -33,6 +27,13 @@ chatsController.get = function (req, res, callback) { if (!uid) { return callback(); } + + privileges.global.can('chat', req.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } messaging.getRecentChats(req.uid, uid, 0, 19, next); }, function (_recentChats, next) { diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 0a91cd5dcc..e7e77c4a4f 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -37,9 +37,6 @@ uploadsController.upload = function (req, res, filesIterator) { uploadsController.uploadPost = function (req, res, next) { uploadsController.upload(req, res, function (uploadedFile, next) { - if (!parseInt(req.body.cid, 10)) { - return next(new Error('[[error:category-not-selected]]')); - } var isImage = uploadedFile.type.match(/image./); if (isImage) { uploadAsImage(req, uploadedFile, next); @@ -52,7 +49,7 @@ uploadsController.uploadPost = function (req, res, next) { function uploadAsImage(req, uploadedFile, callback) { async.waterfall([ function (next) { - privileges.categories.can('upload:post:image', req.body.cid, req.uid, next); + privileges.global.can('upload:post:image', req.uid, next); }, function (canUpload, next) { if (!canUpload) { @@ -82,7 +79,7 @@ function uploadAsImage(req, uploadedFile, callback) { function uploadAsFile(req, uploadedFile, callback) { async.waterfall([ function (next) { - privileges.categories.can('upload:post:file', req.body.cid, req.uid, next); + privileges.global.can('upload:post:file', req.uid, next); }, function (canUpload, next) { if (!canUpload) { diff --git a/src/install.js b/src/install.js index e9e8eecb49..2906adc9e8 100644 --- a/src/install.js +++ b/src/install.js @@ -354,8 +354,8 @@ function createGlobalModeratorsGroup(next) { } function giveGlobalPrivileges(next) { - var groups = require('./groups'); - groups.join('cid:0:privileges:groups:chat', 'registered-users', next); + var privileges = require('./privileges'); + privileges.global.give(['chat', 'upload:post:image'], 'registered-users', next); } function createCategories(next) { diff --git a/src/middleware/header.js b/src/middleware/header.js index ae245ce78f..a0cf65d396 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -78,8 +78,8 @@ module.exports = function (middleware) { isModerator: function (next) { user.isModeratorOfAnyCategory(req.uid, next); }, - canChat: function (next) { - privileges.global.can('chat', req.uid, next); + privileges: function (next) { + privileges.global.get(req.uid, next); }, user: function (next) { var userData = { @@ -136,6 +136,7 @@ module.exports = function (middleware) { results.user.isAdmin = results.isAdmin; results.user.isGlobalMod = results.isGlobalMod; results.user.isMod = !!results.isModerator; + results.user.privileges = results.privileges; results.user.uid = parseInt(results.user.uid, 10); results.user.email = String(results.user.email); diff --git a/src/privileges.js b/src/privileges.js index b4da9f8e88..64d16d3e5c 100644 --- a/src/privileges.js +++ b/src/privileges.js @@ -12,8 +12,6 @@ privileges.privilegeLabels = [ { name: 'Edit Posts' }, { name: 'Delete Posts' }, { name: 'Delete Topics' }, - { name: 'Upload Images' }, - { name: 'Upload Files' }, { name: 'Purge' }, { name: 'Moderate' }, ]; @@ -28,8 +26,6 @@ privileges.userPrivilegeList = [ 'posts:edit', 'posts:delete', 'topics:delete', - 'upload:post:image', - 'upload:post:file', 'purge', 'moderate', ]; diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 610ff7b711..60822c4e46 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -198,19 +198,13 @@ module.exports = function (privileges) { }; privileges.categories.give = function (privileges, cid, groupName, callback) { - giveOrRescind(groups.join, privileges, cid, groupName, callback); + helpers.giveOrRescind(groups.join, privileges, cid, groupName, callback); }; privileges.categories.rescind = function (privileges, cid, groupName, callback) { - giveOrRescind(groups.leave, privileges, cid, groupName, callback); + helpers.giveOrRescind(groups.leave, privileges, cid, groupName, callback); }; - function giveOrRescind(method, privileges, cid, groupName, callback) { - async.eachSeries(privileges, function (privilege, next) { - method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next); - }, callback); - } - privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) { async.waterfall([ function (next) { diff --git a/src/privileges/global.js b/src/privileges/global.js index bf86029ac0..f1f88c4fff 100644 --- a/src/privileges/global.js +++ b/src/privileges/global.js @@ -2,8 +2,10 @@ 'use strict'; var async = require('async'); +var _ = require('lodash'); var user = require('../user'); +var groups = require('../groups'); var helpers = require('./helpers'); var plugins = require('../plugins'); @@ -12,10 +14,14 @@ module.exports = function (privileges) { privileges.global.privilegeLabels = [ { name: 'Chat' }, + { name: 'Upload Images' }, + { name: 'Upload Files' }, ]; privileges.global.userPrivilegeList = [ 'chat', + 'upload:post:image', + 'upload:post:file', ]; privileges.global.groupPrivilegeList = privileges.global.userPrivilegeList.map(function (privilege) { @@ -48,6 +54,34 @@ module.exports = function (privileges) { ], callback); }; + privileges.global.get = function (uid, callback) { + async.waterfall([ + function (next) { + async.parallel({ + privileges: function (next) { + helpers.isUserAllowedTo(privileges.global.userPrivilegeList, uid, 0, next); + }, + isAdministrator: function (next) { + user.isAdministrator(uid, next); + }, + isGlobalModerator: function (next) { + user.isGlobalModerator(uid, next); + }, + }, next); + }, + function (results, next) { + var privData = _.zipObject(privileges.global.userPrivilegeList, results.privileges); + var isAdminOrMod = results.isAdministrator || results.isGlobalModerator; + + plugins.fireHook('filter:privileges.global.get', { + chat: privData.chat || isAdminOrMod, + 'upload:post:image': privData['upload:post:image'] || isAdminOrMod, + 'upload:post:file': privData['upload:post:file'] || isAdminOrMod, + }, next); + }, + ], callback); + }; + privileges.global.can = function (privilege, uid, callback) { helpers.some([ function (next) { @@ -63,4 +97,32 @@ module.exports = function (privileges) { }, ], callback); }; + + privileges.global.give = function (privileges, groupName, callback) { + helpers.giveOrRescind(groups.join, privileges, 0, groupName, callback); + }; + + privileges.global.rescind = function (privileges, groupName, callback) { + helpers.giveOrRescind(groups.leave, privileges, 0, groupName, callback); + }; + + privileges.global.userPrivileges = function (uid, callback) { + var tasks = {}; + + privileges.global.userPrivilegeList.forEach(function (privilege) { + tasks[privilege] = async.apply(groups.isMember, uid, 'cid:0:privileges:' + privilege); + }); + + async.parallel(tasks, callback); + }; + + privileges.global.groupPrivileges = function (groupName, callback) { + var tasks = {}; + + privileges.global.groupPrivilegeList.forEach(function (privilege) { + tasks[privilege] = async.apply(groups.isMember, groupName, 'cid:0:privileges:' + privilege); + }); + + async.parallel(tasks, callback); + }; }; diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index 5a1218ae19..c3452c495e 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -221,3 +221,9 @@ helpers.getGroupPrivileges = function (cid, hookName, groupPrivilegeList, callba }, ], callback); }; + +helpers.giveOrRescind = function (method, privileges, cid, groupName, callback) { + async.eachSeries(privileges, function (privilege, next) { + method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next); + }, callback); +}; diff --git a/src/upgrades/1.8.0/global_upload_privilege.js b/src/upgrades/1.8.0/global_upload_privilege.js new file mode 100644 index 0000000000..22473a9ee0 --- /dev/null +++ b/src/upgrades/1.8.0/global_upload_privilege.js @@ -0,0 +1,45 @@ +'use strict'; + + +var async = require('async'); +var groups = require('../../groups'); +var privileges = require('../../privileges'); +var db = require('../../database'); + +module.exports = { + name: 'Give upload privilege to registered-users globally if it is given on a category', + timestamp: Date.UTC(2018, 0, 3), + method: function (callback) { + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { + if (err) { + return callback(err); + } + async.eachSeries(cids, function (cid, next) { + getGroupPrivileges(cid, function (err, groupPrivileges) { + if (err) { + return next(err); + } + + var privs = []; + if (groupPrivileges['groups:upload:post:image']) { + privs.push('upload:post:image'); + } + if (groupPrivileges['groups:upload:post:file']) { + privs.push('upload:post:file'); + } + privileges.global.give(privs, 'registered-users', next); + }); + }, callback); + }); + }, +}; + +function getGroupPrivileges(cid, callback) { + var tasks = {}; + + ['groups:upload:post:image', 'groups:upload:post:file'].forEach(function (privilege) { + tasks[privilege] = async.apply(groups.isMember, 'registered-users', 'cid:' + cid + ':privileges:' + privilege); + }); + + async.parallel(tasks, callback); +} diff --git a/src/views/admin/partials/categories/privileges.tpl b/src/views/admin/partials/categories/privileges.tpl index c5bfc3ec63..c240c05a63 100644 --- a/src/views/admin/partials/categories/privileges.tpl +++ b/src/views/admin/partials/categories/privileges.tpl @@ -5,7 +5,7 @@ [[admin/manage/categories:privileges.section-viewing]] - + [[admin/manage/categories:privileges.section-posting]] @@ -61,7 +61,7 @@ [[admin/manage/categories:privileges.section-viewing]] - + [[admin/manage/categories:privileges.section-posting]] diff --git a/test/categories.js b/test/categories.js index 4bb66b49af..c1869652f7 100644 --- a/test/categories.js +++ b/test/categories.js @@ -638,7 +638,7 @@ describe('Categories', function () { }); }); - it('should load user privileges', function (done) { + it('should load category user privileges', function (done) { privileges.categories.userPrivileges(categoryObj.cid, 1, function (err, data) { assert.ifError(err); assert.deepEqual(data, { @@ -651,8 +651,6 @@ describe('Categories', function () { 'topics:tag': false, 'topics:delete': false, 'posts:edit': false, - 'upload:post:file': false, - 'upload:post:image': false, purge: false, moderate: false, }); @@ -661,7 +659,20 @@ describe('Categories', function () { }); }); - it('should load group privileges', function (done) { + it('should load global user privileges', function (done) { + privileges.global.userPrivileges(1, function (err, data) { + assert.ifError(err); + assert.deepEqual(data, { + chat: false, + 'upload:post:image': false, + 'upload:post:file': false, + }); + + done(); + }); + }); + + it('should load category group privileges', function (done) { privileges.categories.groupPrivileges(categoryObj.cid, 'registered-users', function (err, data) { assert.ifError(err); assert.deepEqual(data, { @@ -674,8 +685,6 @@ describe('Categories', function () { 'groups:posts:delete': true, 'groups:read': true, 'groups:topics:read': true, - 'groups:upload:post:file': false, - 'groups:upload:post:image': true, 'groups:purge': false, 'groups:moderate': false, }); @@ -684,6 +693,19 @@ describe('Categories', function () { }); }); + it('should load global group privileges', function (done) { + privileges.global.groupPrivileges('registered-users', function (err, data) { + assert.ifError(err); + assert.deepEqual(data, { + 'groups:chat': true, + 'groups:upload:post:image': true, + 'groups:upload:post:file': false, + }); + + done(); + }); + }); + it('should return false if cid is falsy', function (done) { privileges.categories.isUserAllowedTo('find', null, adminUid, function (err, isAllowed) { assert.ifError(err); diff --git a/test/groups.js b/test/groups.js index 311e4ab93b..7c305e1e13 100644 --- a/test/groups.js +++ b/test/groups.js @@ -71,9 +71,9 @@ describe('Groups', function () { describe('.list()', function () { it('should list the groups present', function (done) { - Groups.getGroupsFromSet('groups:createtime', 0, 0, -1, function (err, groups) { + Groups.getGroupsFromSet('groups:visible:createtime', 0, 0, -1, function (err, groups) { assert.ifError(err); - assert.equal(groups.length, 7); + assert.equal(groups.length, 4); done(); }); }); diff --git a/test/messaging.js b/test/messaging.js index 0253fbf6b0..761500e7e4 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -414,7 +414,7 @@ describe('Messaging Library', function () { it('should fail to load room if user is not in', function (done) { socketModules.chats.loadRoom({ uid: 0 }, { roomId: roomId }, function (err) { - assert.equal(err.message, '[[error:not-allowed]]'); + assert.equal(err.message, '[[error:no-privileges]]'); done(); }); }); @@ -579,11 +579,12 @@ describe('Messaging Library', function () { }); }); - it('should 404 for guest', function (done) { + it('should 500 for guest with no privilege error', function (done) { meta.config.disableChat = 0; - request(nconf.get('url') + '/user/baz/chats', function (err, response) { + request(nconf.get('url') + '/api/user/baz/chats', { json: true }, function (err, response, body) { assert.ifError(err); - assert.equal(response.statusCode, 404); + assert.equal(response.statusCode, 500); + assert.equal(body.error, '[[error:no-privileges]]'); done(); }); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 96eada54b2..5314894543 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -154,6 +154,9 @@ function setupMockDefaults(callback) { winston.info('test_database flushed'); setupDefaultConfigs(meta, next); }, + function (next) { + giveDefaultGlobalPrivileges(next); + }, function (next) { meta.configs.init(next); }, @@ -182,6 +185,11 @@ function setupDefaultConfigs(meta, next) { meta.configs.setOnEmpty(defaults, next); } +function giveDefaultGlobalPrivileges(next) { + var privileges = require('../../src/privileges'); + privileges.global.give(['chat', 'upload:post:image'], 'registered-users', next); +} + function enableDefaultPlugins(callback) { winston.info('Enabling default plugins\n'); diff --git a/test/uploads.js b/test/uploads.js index 8d76f21baf..7533553cf8 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -62,7 +62,7 @@ describe('Upload Controllers', function () { assert.ifError(err); jar = _jar; csrf_token = _csrf_token; - privileges.categories.give(['upload:post:file'], cid, 'registered-users', done); + privileges.global.give(['upload:post:file'], 'registered-users', done); }); }); @@ -77,17 +77,8 @@ describe('Upload Controllers', function () { }); }); - it('should fail to upload an image to a post with invalid cid', function (done) { - helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.png'), { cid: '0' }, jar, csrf_token, function (err, res, body) { - assert.ifError(err); - assert.equal(res.statusCode, 500); - assert.equal(body.error, '[[error:category-not-selected]]'); - done(); - }); - }); - it('should upload an image to a post', function (done) { - helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.png'), { cid: cid }, jar, csrf_token, function (err, res, body) { + helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, function (err, res, body) { assert.ifError(err); assert.equal(res.statusCode, 200); assert(Array.isArray(body)); @@ -100,7 +91,7 @@ describe('Upload Controllers', function () { it('should resize and upload an image to a post', function (done) { var oldValue = meta.config.maximumImageWidth; meta.config.maximumImageWidth = 10; - helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.png'), { cid: cid }, jar, csrf_token, function (err, res, body) { + helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, function (err, res, body) { assert.ifError(err); assert.equal(res.statusCode, 200); assert(Array.isArray(body)); @@ -116,7 +107,7 @@ describe('Upload Controllers', function () { meta.config.allowFileUploads = 1; var oldValue = meta.config.allowedFileExtensions; meta.config.allowedFileExtensions = 'png,jpg,bmp,html'; - helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/503.html'), { cid: cid }, jar, csrf_token, function (err, res, body) { + helpers.uploadFile(nconf.get('url') + '/api/post/upload', path.join(__dirname, '../test/files/503.html'), {}, jar, csrf_token, function (err, res, body) { meta.config.allowedFileExtensions = oldValue; assert.ifError(err); assert.equal(res.statusCode, 200);