diff --git a/public/language/en-GB/admin/manage/privileges.json b/public/language/en-GB/admin/manage/privileges.json new file mode 100644 index 0000000000..119633a322 --- /dev/null +++ b/public/language/en-GB/admin/manage/privileges.json @@ -0,0 +1,6 @@ +{ + "global": "Global", + "global.description": "You can configure the global privileges in this section. Privileges can be granted on a per-user or a per-group basis. You can add a new user to this table by searching for them in the form below.", + "global.warning": "Note: Privilege settings take effect immediately. It is not necessary to save after adjusting these settings.", + "global.no-users": "No user-specific global privileges." +} \ No newline at end of file diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index 2b836ed0f7..5c60dc440e 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -9,6 +9,7 @@ "section-manage": "Manage", "manage/categories": "Categories", + "manage/privileges": "Privileges", "manage/tags": "Tags", "manage/users": "Users", "manage/registration": "Registration Queue", diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 8ac0ecdf15..39ff6b6ddf 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -20,7 +20,12 @@ define('admin/manage/category', [ }); $('#category-selector').on('change', function () { - ajaxify.go('admin/manage/categories/' + $(this).val() + window.location.hash); + var val = $(this).val(); + if (val === 'global') { + ajaxify.go('admin/manage/privileges'); + } else { + ajaxify.go('admin/manage/categories/' + $(this).val() + window.location.hash); + } }); function enableColorPicker(idx, inputEl) { diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js new file mode 100644 index 0000000000..e7bf2bc849 --- /dev/null +++ b/public/src/admin/manage/privileges.js @@ -0,0 +1,158 @@ +'use strict'; + + +define('admin/manage/privileges', [ + 'autocomplete', + 'translator', + 'benchpress', +], function (autocomplete, translator, Benchpress) { + var Privileges = {}; + + Privileges.init = function () { + $('#category-selector').on('change', function () { + var val = $(this).val(); + if (val !== 'global') { + ajaxify.go('admin/manage/categories/' + $(this).val() + '#privileges'); + } + }); + + + Privileges.setupPrivilegeTable(); + }; + + Privileges.setupPrivilegeTable = function () { + $('.privilege-table-container').on('change', 'input[type="checkbox"]', function () { + var checkboxEl = $(this); + var privilege = checkboxEl.parent().attr('data-privilege'); + var state = checkboxEl.prop('checked'); + var rowEl = checkboxEl.parents('tr'); + var member = rowEl.attr('data-group-name') || rowEl.attr('data-uid'); + var isPrivate = parseInt(rowEl.attr('data-private') || 0, 10); + var isGroup = rowEl.attr('data-group-name') !== undefined; + + if (member) { + Privileges.setPrivilege(member, privilege, state, checkboxEl); + } else { + app.alertError('[[error:invalid-data]]'); + } + }); + + $('.privilege-table-container').on('click', '[data-action="search.user"]', Privileges.addUserToPrivilegeTable); + $('.privilege-table-container').on('click', '[data-action="search.group"]', Privileges.addGroupToPrivilegeTable); + + Privileges.exposeAssumedPrivileges(); + }; + + Privileges.refreshPrivilegeTable = function () { + socket.emit('admin.categories.getPrivilegeSettings', function (err, privileges) { + if (err) { + return app.alertError(err.message); + } + + Benchpress.parse('admin/partials/global/privileges', { + privileges: privileges, + }, function (html) { + translator.translate(html, function (html) { + $('.privilege-table-container').html(html); + Privileges.exposeAssumedPrivileges(); + }); + }); + }); + }; + + Privileges.exposeAssumedPrivileges = function () { + /* + If registered-users has a privilege enabled, then all users and groups of that privilege + should be assumed to have that privilege as well, even if not set in the db, so reflect + this arrangement in the table + */ + var privs = []; + $('.privilege-table tr[data-group-name="registered-users"] td input[type="checkbox"]').parent().each(function (idx, el) { + if ($(el).find('input').prop('checked')) { + privs.push(el.getAttribute('data-privilege')); + } + }); + for (var x = 0, numPrivs = privs.length; x < numPrivs; x += 1) { + var inputs = $('.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="guests"]) td[data-privilege="' + privs[x] + '"] input'); + inputs.each(function (idx, el) { + if (!el.checked) { + el.indeterminate = true; + } + }); + } + }; + + Privileges.setPrivilege = function (member, privilege, state, checkboxEl) { + socket.emit('admin.categories.setPrivilege', { + cid: 0, + privilege: privilege, + set: state, + member: member, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + checkboxEl.replaceWith(''); + Privileges.refreshPrivilegeTable(); + }); + }; + + Privileges.addUserToPrivilegeTable = function () { + var modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.find-user]]', + message: '', + show: true, + }); + + modal.on('shown.bs.modal', function () { + var inputEl = modal.find('input'); + + autocomplete.user(inputEl, function (ev, ui) { + socket.emit('admin.categories.setPrivilege', { + cid: 0, + privilege: ['chat'], + set: true, + member: ui.item.user.uid, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + Privileges.refreshPrivilegeTable(); + modal.modal('hide'); + }); + }); + }); + }; + + Privileges.addGroupToPrivilegeTable = function () { + var modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.find-group]]', + message: '', + show: true, + }); + + modal.on('shown.bs.modal', function () { + var inputEl = modal.find('input'); + + autocomplete.group(inputEl, function (ev, ui) { + socket.emit('admin.categories.setPrivilege', { + cid: 0, + privilege: ['groups:chat'], + set: true, + member: ui.item.group.name, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + Privileges.refreshPrivilegeTable(); + modal.modal('hide'); + }); + }); + }); + }; + + return Privileges; +}); diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index c3b9990c26..d717f6267a 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -5,6 +5,7 @@ var async = require('async'); var messaging = require('../../messaging'); var meta = require('../../meta'); var user = require('../../user'); +var privileges = require('../../privileges'); var helpers = require('../helpers'); var chatsController = module.exports; @@ -19,6 +20,12 @@ 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) { diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 6ef000fa14..136eed1267 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -3,6 +3,7 @@ var adminController = { dashboard: require('./admin/dashboard'), categories: require('./admin/categories'), + privileges: require('./admin/privileges'), tags: require('./admin/tags'), postQueue: require('./admin/postqueue'), blacklist: require('./admin/blacklist'), diff --git a/src/controllers/admin/privileges.js b/src/controllers/admin/privileges.js new file mode 100644 index 0000000000..a1fdd4acf8 --- /dev/null +++ b/src/controllers/admin/privileges.js @@ -0,0 +1,25 @@ +'use strict'; + +var async = require('async'); + +var categories = require('../../categories'); +var privileges = require('../../privileges'); + +var privilegesController = module.exports; + +privilegesController.get = function (req, res, callback) { + async.waterfall([ + function (next) { + async.parallel({ + privileges: async.apply(privileges.global.list), + allCategories: async.apply(categories.buildForSelect, req.uid, 'read'), + }, next); + }, + function (data) { + res.render('admin/manage/privileges', { + privileges: data.privileges, + allCategories: data.allCategories, + }); + }, + ], callback); +}; diff --git a/src/middleware/header.js b/src/middleware/header.js index 3824ff6fc3..add5a458aa 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -10,6 +10,7 @@ var meta = require('../meta'); var plugins = require('../plugins'); var navigation = require('../navigation'); var translator = require('../translator'); +var privileges = require('../privileges'); var utils = require('../utils'); var controllers = { @@ -75,6 +76,9 @@ module.exports = function (middleware) { isModerator: function (next) { user.isModeratorOfAnyCategory(req.uid, next); }, + canChat: function (next) { + privileges.global.can('chat', req.uid, next); + }, user: function (next) { var userData = { uid: 0, @@ -124,6 +128,7 @@ module.exports = function (middleware) { results.user.isAdmin = results.isAdmin; results.user.isGlobalMod = results.isGlobalMod; results.user.isMod = !!results.isModerator; + results.user.uid = parseInt(results.user.uid, 10); results.user.email = String(results.user.email); results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1; @@ -138,6 +143,7 @@ module.exports = function (middleware) { templateValues.isAdmin = results.user.isAdmin; templateValues.isGlobalMod = results.user.isGlobalMod; templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod; + templateValues.canChat = results.canChat; templateValues.user = results.user; templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true }); templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS; diff --git a/src/privileges.js b/src/privileges.js index c1ac018ec7..b4da9f8e88 100644 --- a/src/privileges.js +++ b/src/privileges.js @@ -40,6 +40,7 @@ privileges.groupPrivilegeList = privileges.userPrivilegeList.map(function (privi privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList); +require('./privileges/global')(privileges); require('./privileges/categories')(privileges); require('./privileges/topics')(privileges); require('./privileges/posts')(privileges); diff --git a/src/privileges/global.js b/src/privileges/global.js new file mode 100644 index 0000000000..3047cdc55a --- /dev/null +++ b/src/privileges/global.js @@ -0,0 +1,171 @@ + +'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'); + +module.exports = function (privileges) { + privileges.global = {}; + + privileges.global.privilegeLabels = [ + { name: 'Chat' }, + ]; + + privileges.global.userPrivilegeList = [ + 'chat', + ]; + + privileges.global.groupPrivilegeList = privileges.global.userPrivilegeList.map(function (privilege) { + return 'groups:' + privilege; + }); + + privileges.global.list = function (callback) { + var privilegeLabels = privileges.global.privilegeLabels.slice(); + var userPrivilegeList = privileges.global.userPrivilegeList.slice(); + var groupPrivilegeList = privileges.global.groupPrivilegeList.slice(); + + async.waterfall([ + function (next) { + async.parallel({ + labels: function (next) { + async.parallel({ + users: async.apply(plugins.fireHook, 'filter:privileges.global.list_human', privilegeLabels), + groups: async.apply(plugins.fireHook, 'filter:privileges.global.groups.list_human', privilegeLabels), + }, next); + }, + users: function (next) { + var userPrivileges; + var memberSets; + async.waterfall([ + async.apply(plugins.fireHook, 'filter:privileges.global.list', userPrivilegeList), + function (_privs, next) { + userPrivileges = _privs; + groups.getMembersOfGroups(userPrivileges.map(function (privilege) { + return 'cid:0:privileges:' + privilege; + }), next); + }, + function (_memberSets, next) { + memberSets = _memberSets.map(function (set) { + return set.map(function (uid) { + return parseInt(uid, 10); + }); + }); + + var members = _.uniq(_.flatten(memberSets)); + + user.getUsersFields(members, ['picture', 'username'], next); + }, + function (memberData, next) { + memberData.forEach(function (member) { + member.privileges = {}; + for (var x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) { + member.privileges[userPrivileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1; + } + }); + + next(null, memberData); + }, + ], next); + }, + groups: function (next) { + var groupPrivileges; + async.waterfall([ + async.apply(plugins.fireHook, 'filter:privileges.global.groups.list', groupPrivilegeList), + function (_privs, next) { + groupPrivileges = _privs; + async.parallel({ + memberSets: function (next) { + groups.getMembersOfGroups(groupPrivileges.map(function (privilege) { + return 'cid:0:privileges:' + privilege; + }), next); + }, + groupNames: function (next) { + groups.getGroups('groups:createtime', 0, -1, next); + }, + }, next); + }, + function (results, next) { + var memberSets = results.memberSets; + var uniqueGroups = _.uniq(_.flatten(memberSets)); + + var groupNames = results.groupNames.filter(function (groupName) { + return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1; + }); + + var registeredUsersIndex = groupNames.indexOf('registered-users'); + if (registeredUsersIndex !== -1) { + groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]); + } else { + groupNames = ['registered-users'].concat(groupNames); + } + + var adminIndex = groupNames.indexOf('administrators'); + if (adminIndex !== -1) { + groupNames.splice(adminIndex, 1); + } + + var memberPrivs; + + var memberData = groupNames.map(function (member) { + memberPrivs = {}; + + for (var x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) { + memberPrivs[groupPrivileges[x]] = memberSets[x].indexOf(member) !== -1; + } + return { + name: member, + privileges: memberPrivs, + }; + }); + + next(null, memberData); + }, + function (memberData, next) { + // Grab privacy info for the groups as well + async.map(memberData, function (member, next) { + async.waterfall([ + function (next) { + groups.isPrivate(member.name, next); + }, + function (isPrivate, next) { + member.isPrivate = isPrivate; + next(null, member); + }, + ], next); + }, next); + }, + ], next); + }, + }, next); + }, + function (payload, next) { + // This is a hack because I can't do {labels.users.length} to echo the count in templates.js + payload.columnCount = payload.labels.users.length + 2; + next(null, payload); + }, + ], callback); + }; + + privileges.global.can = function (privilege, uid, callback) { + helpers.some([ + function (next) { + helpers.isUserAllowedTo(privilege, uid, [0], function (err, results) { + next(err, Array.isArray(results) && results.length ? results[0] : false); + }); + }, + function (next) { + user.isGlobalModerator(uid, next); + }, + function (next) { + user.isAdministrator(uid, next); + }, + ], callback); + }; + + +}; diff --git a/src/routes/admin.js b/src/routes/admin.js index ba4048516e..ac90b89b97 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -55,6 +55,7 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/categories/:category_id', middlewares, controllers.admin.categories.get); router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics); + router.get('/manage/privileges', middlewares, controllers.admin.privileges.get); router.get('/manage/tags', middlewares, controllers.admin.tags.get); router.get('/manage/post-queue', middlewares, controllers.admin.postQueue.get); router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get); diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 232b2041d1..7bd491c8cd 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -83,7 +83,11 @@ Categories.setPrivilege = function (socket, data, callback) { }; Categories.getPrivilegeSettings = function (socket, cid, callback) { - privileges.categories.list(cid, callback); + if (!parseInt(cid, 10)) { + privileges.global.list(callback); + } else { + privileges.categories.list(cid, callback); + } }; Categories.copyPrivilegesToChildren = function (socket, cid, callback) { diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index d58fb7fa59..f5d2e8143a 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -11,6 +11,7 @@ var Messaging = require('../messaging'); var utils = require('../utils'); var server = require('./'); var user = require('../user'); +var privileges = require('../privileges'); var SocketModules = module.exports; @@ -73,6 +74,12 @@ SocketModules.chats.newRoom = function (socket, data, callback) { async.waterfall([ function (next) { + privileges.global.can('chat', socket.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } Messaging.canMessageUser(socket.uid, data.touid, next); }, function (next) { @@ -92,6 +99,13 @@ SocketModules.chats.send = function (socket, data, callback) { async.waterfall([ function (next) { + privileges.global.can('chat', socket.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } + plugins.fireHook('filter:messaging.send', { data: data, uid: socket.uid, @@ -133,6 +147,13 @@ SocketModules.chats.loadRoom = function (socket, data, callback) { async.waterfall([ function (next) { + privileges.global.can('chat', socket.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } + Messaging.isUserInRoom(socket.uid, data.roomId, next); }, function (inRoom, next) { @@ -174,6 +195,13 @@ SocketModules.chats.addUserToRoom = function (socket, data, callback) { var uid; async.waterfall([ function (next) { + privileges.global.can('chat', socket.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } + Messaging.getUserCountInRoom(data.roomId, next); }, function (userCount, next) { diff --git a/src/upgrades/1.8.0/chat_privilege.js b/src/upgrades/1.8.0/chat_privilege.js new file mode 100644 index 0000000000..d337784d2b --- /dev/null +++ b/src/upgrades/1.8.0/chat_privilege.js @@ -0,0 +1,12 @@ +'use strict'; + + +var groups = require('../../groups'); + +module.exports = { + name: 'Give chat privilege to registered-users', + timestamp: Date.UTC(2017, 11, 18), + method: function (callback) { + groups.join('cid:0:privileges:group:chat', 'registered-users', callback); + }, +}; diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index 7c39ddee44..4addee15ba 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -12,13 +12,15 @@
- +
diff --git a/src/views/admin/manage/privileges.tpl b/src/views/admin/manage/privileges.tpl new file mode 100644 index 0000000000..a3596b39dc --- /dev/null +++ b/src/views/admin/manage/privileges.tpl @@ -0,0 +1,34 @@ +
+
+
+
+ +
+
+ +
+ +
+

+ [[admin/manage/privileges:global.description]] +

+

+ [[admin/manage/privileges:global.warning]] +

+
+
+ +
+
+
+
+ + diff --git a/src/views/admin/partials/global/privileges.tpl b/src/views/admin/partials/global/privileges.tpl new file mode 100644 index 0000000000..6a792eb7ee --- /dev/null +++ b/src/views/admin/partials/global/privileges.tpl @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + {function.spawnPrivilegeStates, privileges.users.username, ../privileges} + + + + + + + + + + + +
[[admin/manage/categories:privileges.section-user]]{privileges.labels.users.name}
+ + + +
{../icon:text}
+ +
{privileges.users.username}
+ +
+ [[admin/manage/privileges:global.no-users]] + +
+ + + + + + + + + + + + + + + + + + + {function.spawnPrivilegeStates, privileges.groups.name, ../privileges} + + + + + + +
[[admin/manage/categories:privileges.section-group]]{privileges.labels.groups.name}
+ + + + {privileges.groups.name} +
+
+ +
+
+
+ [[admin/manage/categories:privileges.inherit]] +
diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 9fd4742c3c..027071ca32 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -15,6 +15,7 @@