From 4a73621dcabf81737fc6fdf635632443e25d566b Mon Sep 17 00:00:00 2001 From: Baris Usakli <barisusakli@gmail.com> Date: Mon, 18 Dec 2017 15:43:57 -0500 Subject: [PATCH 01/22] chat privilege --- .../en-GB/admin/manage/privileges.json | 6 + public/language/en-GB/admin/menu.json | 1 + public/src/admin/manage/category.js | 7 +- public/src/admin/manage/privileges.js | 158 ++++++++++++++++ src/controllers/accounts/chats.js | 7 + src/controllers/admin.js | 1 + src/controllers/admin/privileges.js | 25 +++ src/middleware/header.js | 6 + src/privileges.js | 1 + src/privileges/global.js | 171 ++++++++++++++++++ src/routes/admin.js | 1 + src/socket.io/admin/categories.js | 6 +- src/socket.io/modules.js | 28 +++ src/upgrades/1.8.0/chat_privilege.js | 12 ++ src/views/admin/manage/category.tpl | 4 +- src/views/admin/manage/privileges.tpl | 34 ++++ .../admin/partials/global/privileges.tpl | 86 +++++++++ src/views/admin/partials/menu.tpl | 2 + 18 files changed, 553 insertions(+), 3 deletions(-) create mode 100644 public/language/en-GB/admin/manage/privileges.json create mode 100644 public/src/admin/manage/privileges.js create mode 100644 src/controllers/admin/privileges.js create mode 100644 src/privileges/global.js create mode 100644 src/upgrades/1.8.0/chat_privilege.js create mode 100644 src/views/admin/manage/privileges.tpl create mode 100644 src/views/admin/partials/global/privileges.tpl 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": "<strong>Note</strong>: 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('<i class="fa fa-spin fa-spinner"></i>'); + Privileges.refreshPrivilegeTable(); + }); + }; + + Privileges.addUserToPrivilegeTable = function () { + var modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.find-user]]', + message: '<input class="form-control input-lg" placeholder="[[admin/manage/categories:alert.user-search]]" />', + 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: '<input class="form-control input-lg" placeholder="[[admin/manage/categories:alert.group-search]]" />', + 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 @@ </div> <div class="col-md-3"> <select id="category-selector" class="form-control"> + <option value="global" selected>[[admin/manage/privileges:global]]</option> + <option disabled>_____________</option> <!-- BEGIN allCategories --> <option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option> <!-- END allCategories --> </select> </div> </div> - + <br/> <div class="tab-content"> 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 @@ +<div class="row"> + <form role="form" class="category"> + <div class="row"> + <div class="col-md-3 pull-right"> + <select id="category-selector" class="form-control"> + <option value="global" selected>[[admin/manage/privileges:global]]</option> + <option disabled>_____________</option> + <!-- BEGIN allCategories --> + <option value="{allCategories.value}">{allCategories.text}</option> + <!-- END allCategories --> + </select> + </div> + </div> + + <br/> + + <div class=""> + <p> + [[admin/manage/privileges:global.description]] + </p> + <p class="text-warning"> + [[admin/manage/privileges:global.warning]] + </p> + <hr /> + <div class="privilege-table-container"> + <!-- IMPORT admin/partials/global/privileges.tpl --> + </div> + </div> + </form> +</div> + +<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored"> + <i class="material-icons">save</i> +</button> 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 @@ + <table class="table table-striped privilege-table"> + <thead> + <tr class="privilege-table-header"> + <th colspan="3"></th> + </tr><tr><!-- zebrastripe reset --></tr> + <tr> + <th colspan="2">[[admin/manage/categories:privileges.section-user]]</th> + <!-- BEGIN privileges.labels.users --> + <th class="text-center">{privileges.labels.users.name}</th> + <!-- END privileges.labels.users --> + </tr> + </thead> + <tbody> + <!-- IF privileges.users.length --> + <!-- BEGIN privileges.users --> + <tr data-uid="{privileges.users.uid}"> + <td> + <!-- IF ../picture --> + <img class="avatar avatar-sm" src="{privileges.users.picture}" title="{privileges.users.username}" /> + <!-- ELSE --> + <div class="avatar avatar-sm" style="background-color: {../icon:bgColor};">{../icon:text}</div> + <!-- ENDIF ../picture --> + </td> + <td>{privileges.users.username}</td> + {function.spawnPrivilegeStates, privileges.users.username, ../privileges} + </tr> + <!-- END privileges.users --> + <tr> + <td colspan="{privileges.columnCount}"> + <button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user"> + [[admin/manage/categories:privileges.search-user]] + </button> + </td> + </tr> + <!-- ELSE --> + <tr> + <td colspan="{privileges.columnCount}"> + [[admin/manage/privileges:global.no-users]] + <button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user"> + [[admin/manage/categories:privileges.search-user]] + </button> + </td> + </tr> + <!-- ENDIF privileges.users.length --> + </tbody> + </table> + + <table class="table table-striped privilege-table"> + <thead> + <tr class="privilege-table-header"> + <th colspan="3"></th> + </tr><tr><!-- zebrastripe reset --></tr> + <tr> + <th colspan="2">[[admin/manage/categories:privileges.section-group]]</th> + <!-- BEGIN privileges.labels.groups --> + <th class="text-center">{privileges.labels.groups.name}</th> + <!-- END privileges.labels.groups --> + </tr> + </thead> + <tbody> + <!-- BEGIN privileges.groups --> + <tr data-group-name="{privileges.groups.name}" data-private="<!-- IF privileges.groups.isPrivate -->1<!-- ELSE -->0<!-- ENDIF privileges.groups.isPrivate -->"> + <td> + <!-- IF privileges.groups.isPrivate --> + <i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i> + <!-- ENDIF privileges.groups.isPrivate --> + {privileges.groups.name} + </td> + <td></td> + {function.spawnPrivilegeStates, privileges.groups.name, ../privileges} + </tr> + <!-- END privileges.groups --> + <tr> + <td colspan="{privileges.columnCount}"> + <div class="btn-toolbar"> + <button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.group"> + [[admin/manage/categories:privileges.search-group]] + </button> + </div> + </td> + </tr> + </tbody> + </table> + <div class="help-block"> + [[admin/manage/categories:privileges.inherit]] + </div> 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 @@ <h3 class="menu-section-title">[[admin/menu:section-manage]]</h3> <ul class="menu-section-list"> <li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li> + <li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li> <li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li> <li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li> <li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li> @@ -188,6 +189,7 @@ <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-manage]]</a> <ul class="dropdown-menu" role="menu"> <li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li> + <li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li> <li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li> <li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li> <li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li> From 242dc41aca3aa6235eaf6b78b6d594838340ab87 Mon Sep 17 00:00:00 2001 From: Baris Usakli <barisusakli@gmail.com> Date: Wed, 20 Dec 2017 14:49:20 -0500 Subject: [PATCH 02/22] move privileges to same page --- .../en-GB/admin/manage/privileges.json | 2 - public/src/admin/manage/category.js | 218 +----------------- public/src/admin/manage/privileges.js | 64 +++-- public/src/modules/categorySelector.js | 39 +++- src/controllers/admin/categories.js | 3 - src/controllers/admin/privileges.js | 17 +- src/privileges/categories.js | 109 +-------- src/privileges/global.js | 109 +-------- src/privileges/helpers.js | 110 +++++++++ src/routes/admin.js | 2 +- src/views/admin/manage/category.tpl | 25 +- src/views/admin/manage/privileges.tpl | 12 +- 12 files changed, 235 insertions(+), 475 deletions(-) diff --git a/public/language/en-GB/admin/manage/privileges.json b/public/language/en-GB/admin/manage/privileges.json index 119633a322..b5b4f35885 100644 --- a/public/language/en-GB/admin/manage/privileges.json +++ b/public/language/en-GB/admin/manage/privileges.json @@ -1,6 +1,4 @@ { "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": "<strong>Note</strong>: 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/src/admin/manage/category.js b/public/src/admin/manage/category.js index 39ff6b6ddf..71470baf46 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -1,6 +1,5 @@ 'use strict'; - define('admin/manage/category', [ 'uploader', 'iconSelect', @@ -8,8 +7,7 @@ define('admin/manage/category', [ 'autocomplete', 'translator', 'categorySelector', - 'benchpress', -], function (uploader, iconSelect, colorpicker, autocomplete, translator, categorySelector, Benchpress) { +], function (uploader, iconSelect, colorpicker, autocomplete, translator, categorySelector) { var Category = {}; var modified_categories = {}; @@ -21,11 +19,7 @@ define('admin/manage/category', [ $('#category-selector').on('change', function () { var val = $(this).val(); - if (val === 'global') { - ajaxify.go('admin/manage/privileges'); - } else { - ajaxify.go('admin/manage/categories/' + $(this).val() + window.location.hash); - } + ajaxify.go('admin/manage/categories/' + $(this).val() + window.location.hash); }); function enableColorPicker(idx, inputEl) { @@ -105,7 +99,7 @@ define('admin/manage/category', [ }); $('.copy-settings').on('click', function () { - selectCategoryModal(function (cid) { + categorySelector.modal(function (cid) { socket.emit('admin.categories.copySettingsFrom', { fromCid: cid, toCid: ajaxify.data.category.cid }, function (err) { if (err) { return app.alertError(err.message); @@ -174,8 +168,6 @@ define('admin/manage/category', [ $('button[data-action="setParent"]').removeClass('hide'); }); }); - - Category.setupPrivilegeTable(); }; function modified(el) { @@ -213,102 +205,12 @@ define('admin/manage/category', [ }); } - Category.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) { - if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) { - bootbox.confirm('[[admin/manage/categories:alert.confirm-moderate]]', function (confirm) { - if (confirm) { - Category.setPrivilege(member, privilege, state, checkboxEl); - } else { - checkboxEl.prop('checked', !checkboxEl.prop('checked')); - } - }); - } else { - Category.setPrivilege(member, privilege, state, checkboxEl); - } - } else { - app.alertError('[[error:invalid-data]]'); - } - }); - - $('.privilege-table-container').on('click', '[data-action="search.user"]', Category.addUserToPrivilegeTable); - $('.privilege-table-container').on('click', '[data-action="search.group"]', Category.addGroupToPrivilegeTable); - $('.privilege-table-container').on('click', '[data-action="copyToChildren"]', Category.copyPrivilegesToChildren); - $('.privilege-table-container').on('click', '[data-action="copyPrivilegesFrom"]', Category.copyPrivilegesFromCategory); - - Category.exposeAssumedPrivileges(); - }; - - Category.refreshPrivilegeTable = function () { - socket.emit('admin.categories.getPrivilegeSettings', ajaxify.data.category.cid, function (err, privileges) { - if (err) { - return app.alertError(err.message); - } - - Benchpress.parse('admin/partials/categories/privileges', { - privileges: privileges, - }, function (html) { - translator.translate(html, function (html) { - $('.privilege-table-container').html(html); - Category.exposeAssumedPrivileges(); - }); - }); - }); - }; - - Category.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; - } - }); - } - }; - - Category.setPrivilege = function (member, privilege, state, checkboxEl) { - socket.emit('admin.categories.setPrivilege', { - cid: ajaxify.data.category.cid, - privilege: privilege, - set: state, - member: member, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - - checkboxEl.replaceWith('<i class="fa fa-spin fa-spinner"></i>'); - Category.refreshPrivilegeTable(); - }); - }; - Category.launchParentSelector = function () { var categories = ajaxify.data.allCategories.filter(function (category) { return category && !category.disabled && parseInt(category.cid, 10) !== parseInt(ajaxify.data.category.cid, 10); }); - selectCategoryModal(categories, function (parentCid) { + categorySelector.modal(categories, function (parentCid) { var payload = {}; payload[ajaxify.data.category.cid] = { @@ -332,117 +234,5 @@ define('admin/manage/category', [ }); }; - Category.addUserToPrivilegeTable = function () { - var modal = bootbox.dialog({ - title: '[[admin/manage/categories:alert.find-user]]', - message: '<input class="form-control input-lg" placeholder="[[admin/manage/categories:alert.user-search]]" />', - 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: ajaxify.data.category.cid, - privilege: ['find', 'read', 'topics:read'], - set: true, - member: ui.item.user.uid, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - - Category.refreshPrivilegeTable(); - modal.modal('hide'); - }); - }); - }); - }; - - Category.addGroupToPrivilegeTable = function () { - var modal = bootbox.dialog({ - title: '[[admin/manage/categories:alert.find-group]]', - message: '<input class="form-control input-lg" placeholder="[[admin/manage/categories:alert.group-search]]" />', - 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: ajaxify.data.category.cid, - privilege: ['groups:find', 'groups:read', 'groups:topics:read'], - set: true, - member: ui.item.group.name, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - - Category.refreshPrivilegeTable(); - modal.modal('hide'); - }); - }); - }); - }; - - Category.copyPrivilegesToChildren = function () { - socket.emit('admin.categories.copyPrivilegesToChildren', ajaxify.data.category.cid, function (err) { - if (err) { - return app.alertError(err.message); - } - app.alertSuccess('Privileges copied!'); - }); - }; - - Category.copyPrivilegesFromCategory = function () { - selectCategoryModal(function (cid) { - socket.emit('admin.categories.copyPrivilegesFrom', { toCid: ajaxify.data.category.cid, fromCid: cid }, function (err) { - if (err) { - return app.alertError(err.message); - } - ajaxify.refresh(); - }); - }); - }; - - function selectCategoryModal(categories, callback) { - if (typeof categories === 'function') { - callback = categories; - categories = ajaxify.data.allCategories; - } - Benchpress.parse('admin/partials/categories/select-category', { - categories: categories, - }, function (html) { - translator.translate(html, function (html) { - var modal = bootbox.dialog({ - title: '[[modules:composer.select_category]]', - message: html, - buttons: { - save: { - label: '[[global:select]]', - className: 'btn-primary', - callback: submit, - }, - }, - }); - categorySelector.init(modal.find('[component="category-selector"]')); - function submit(ev) { - ev.preventDefault(); - var selectedCategory = categorySelector.getSelectedCategory(); - if (selectedCategory) { - callback(selectedCategory.cid); - modal.modal('hide'); - } - return false; - } - - modal.find('form').on('submit', submit); - }); - }); - } - return Category; }); diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index e7bf2bc849..98587fc87f 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -1,19 +1,21 @@ 'use strict'; - define('admin/manage/privileges', [ 'autocomplete', 'translator', 'benchpress', -], function (autocomplete, translator, Benchpress) { + 'categorySelector' +], function (autocomplete, translator, Benchpress, categorySelector) { var Privileges = {}; + var cid; + Privileges.init = function () { + cid = ajaxify.data.cid || 0; + $('#category-selector').on('change', function () { var val = $(this).val(); - if (val !== 'global') { - ajaxify.go('admin/manage/categories/' + $(this).val() + '#privileges'); - } + ajaxify.go('admin/manage/privileges/' + (val === 'global' ? '' : $(this).val())); }); @@ -31,7 +33,17 @@ define('admin/manage/privileges', [ var isGroup = rowEl.attr('data-group-name') !== undefined; if (member) { - Privileges.setPrivilege(member, privilege, state, checkboxEl); + if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) { + bootbox.confirm('[[admin/manage/categories:alert.confirm-moderate]]', function (confirm) { + if (confirm) { + Privileges.setPrivilege(member, privilege, state, checkboxEl); + } else { + checkboxEl.prop('checked', !checkboxEl.prop('checked')); + } + }); + } else { + Privileges.setPrivilege(member, privilege, state, checkboxEl); + } } else { app.alertError('[[error:invalid-data]]'); } @@ -39,17 +51,19 @@ define('admin/manage/privileges', [ $('.privilege-table-container').on('click', '[data-action="search.user"]', Privileges.addUserToPrivilegeTable); $('.privilege-table-container').on('click', '[data-action="search.group"]', Privileges.addGroupToPrivilegeTable); + $('.privilege-table-container').on('click', '[data-action="copyToChildren"]', Privileges.copyPrivilegesToChildren); + $('.privilege-table-container').on('click', '[data-action="copyPrivilegesFrom"]', Privileges.copyPrivilegesFromCategory); Privileges.exposeAssumedPrivileges(); }; Privileges.refreshPrivilegeTable = function () { - socket.emit('admin.categories.getPrivilegeSettings', function (err, privileges) { + socket.emit('admin.categories.getPrivilegeSettings', cid, function (err, privileges) { if (err) { return app.alertError(err.message); } - - Benchpress.parse('admin/partials/global/privileges', { + var tpl = cid ? 'admin/partials/categories/privileges' : 'admin/manage/privileges'; + Benchpress.parse(tpl, { privileges: privileges, }, function (html) { translator.translate(html, function (html) { @@ -84,7 +98,7 @@ define('admin/manage/privileges', [ Privileges.setPrivilege = function (member, privilege, state, checkboxEl) { socket.emit('admin.categories.setPrivilege', { - cid: 0, + cid: cid, privilege: privilege, set: state, member: member, @@ -109,9 +123,10 @@ define('admin/manage/privileges', [ var inputEl = modal.find('input'); autocomplete.user(inputEl, function (ev, ui) { + var defaultPrivileges = cid ? ['find', 'read', 'topics:read'] : ['chat']; socket.emit('admin.categories.setPrivilege', { - cid: 0, - privilege: ['chat'], + cid: cid, + privilege: defaultPrivileges, set: true, member: ui.item.user.uid, }, function (err) { @@ -137,9 +152,10 @@ define('admin/manage/privileges', [ var inputEl = modal.find('input'); autocomplete.group(inputEl, function (ev, ui) { + var defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat']; socket.emit('admin.categories.setPrivilege', { - cid: 0, - privilege: ['groups:chat'], + cid: cid, + privilege: defaultPrivileges, set: true, member: ui.item.group.name, }, function (err) { @@ -154,5 +170,25 @@ define('admin/manage/privileges', [ }); }; + Privileges.copyPrivilegesToChildren = function () { + socket.emit('admin.categories.copyPrivilegesToChildren', cid, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('Privileges copied!'); + }); + }; + + Privileges.copyPrivilegesFromCategory = function () { + categorySelector.modal(function (fromCid) { + socket.emit('admin.categories.copyPrivilegesFrom', { toCid: cid, fromCid: fromCid }, function (err) { + if (err) { + return app.alertError(err.message); + } + ajaxify.refresh(); + }); + }); + }; + return Privileges; }); diff --git a/public/src/modules/categorySelector.js b/public/src/modules/categorySelector.js index 882206a42d..b9b8c9e3a4 100644 --- a/public/src/modules/categorySelector.js +++ b/public/src/modules/categorySelector.js @@ -1,7 +1,6 @@ 'use strict'; - -define('categorySelector', function () { +define('categorySelector', ['benchpress', 'translator'], function (Benchpress, translator) { var categorySelector = {}; var selectedCategory; var el; @@ -29,6 +28,42 @@ define('categorySelector', function () { el.find('[component="category-selector-selected"]').html(categoryEl.find('[component="category-markup"]').html()); }; + categorySelector.modal = function (categories, callback) { + if (typeof categories === 'function') { + callback = categories; + categories = ajaxify.data.allCategories; + } + Benchpress.parse('admin/partials/categories/select-category', { + categories: categories, + }, function (html) { + translator.translate(html, function (html) { + var modal = bootbox.dialog({ + title: '[[modules:composer.select_category]]', + message: html, + buttons: { + save: { + label: '[[global:select]]', + className: 'btn-primary', + callback: submit, + }, + }, + }); + categorySelector.init(modal.find('[component="category-selector"]')); + function submit(ev) { + ev.preventDefault(); + var selectedCategory = categorySelector.getSelectedCategory(); + if (selectedCategory) { + callback(selectedCategory.cid); + modal.modal('hide'); + } + return false; + } + + modal.find('form').on('submit', submit); + }); + }); + }; + return categorySelector; }); diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js index 0b78912644..b3564afcef 100644 --- a/src/controllers/admin/categories.js +++ b/src/controllers/admin/categories.js @@ -15,7 +15,6 @@ categoriesController.get = function (req, res, callback) { function (next) { async.parallel({ category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid), - privileges: async.apply(privileges.categories.list, req.params.category_id), allCategories: async.apply(categories.buildForSelect, req.uid, 'read'), }, next); }, @@ -36,7 +35,6 @@ categoriesController.get = function (req, res, callback) { req: req, res: res, category: category, - privileges: data.privileges, allCategories: data.allCategories, }, next); }, @@ -44,7 +42,6 @@ categoriesController.get = function (req, res, callback) { data.category.name = translator.escape(String(data.category.name)); res.render('admin/manage/category', { category: data.category, - privileges: data.privileges, allCategories: data.allCategories, }); }, diff --git a/src/controllers/admin/privileges.js b/src/controllers/admin/privileges.js index a1fdd4acf8..1fad4cfd06 100644 --- a/src/controllers/admin/privileges.js +++ b/src/controllers/admin/privileges.js @@ -8,17 +8,32 @@ var privileges = require('../../privileges'); var privilegesController = module.exports; privilegesController.get = function (req, res, callback) { + var cid = req.params.cid ? req.params.cid : 0; async.waterfall([ function (next) { async.parallel({ - privileges: async.apply(privileges.global.list), + privileges: function (next) { + if (!cid) { + privileges.global.list(next); + } else { + privileges.categories.list(cid, next); + } + }, allCategories: async.apply(categories.buildForSelect, req.uid, 'read'), }, next); }, function (data) { + + data.allCategories.forEach(function (category) { + if (category) { + category.selected = parseInt(category.cid, 10) === parseInt(cid, 10); + } + }); + res.render('admin/manage/privileges', { privileges: data.privileges, allCategories: data.allCategories, + cid: cid, }); }, ], callback); diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 69aec85135..610ff7b711 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -15,121 +15,20 @@ 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 = privileges.privilegeLabels.slice(); - var userPrivilegeList = privileges.userPrivilegeList.slice(); - var groupPrivilegeList = privileges.groupPrivilegeList.slice(); 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.slice()), + groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privileges.privilegeLabels.slice()), }, next); }, users: function (next) { - var userPrivileges; - var memberSets; - async.waterfall([ - async.apply(plugins.fireHook, 'filter:privileges.list', userPrivilegeList), - function (_privs, next) { - userPrivileges = _privs; - groups.getMembersOfGroups(userPrivileges.map(function (privilege) { - return 'cid:' + cid + ':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); + helpers.getUserPrivileges(cid, 'filter:privileges.list', privileges.userPrivilegeList, next); }, groups: function (next) { - var groupPrivileges; - async.waterfall([ - async.apply(plugins.fireHook, 'filter:privileges.groups.list', groupPrivilegeList), - function (_privs, next) { - groupPrivileges = _privs; - async.parallel({ - memberSets: function (next) { - groups.getMembersOfGroups(groupPrivileges.map(function (privilege) { - return 'cid:' + cid + ':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; - }); - - groupNames = groups.ephemeralGroups.concat(groupNames); - 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); + helpers.getGroupPrivileges(cid, 'filter:privileges.groups.list', privileges.groupPrivilegeList, next); }, }, next); }, diff --git a/src/privileges/global.js b/src/privileges/global.js index 3047cdc55a..0ca4ac4347 100644 --- a/src/privileges/global.js +++ b/src/privileges/global.js @@ -25,121 +25,20 @@ module.exports = function (privileges) { }); 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), + users: async.apply(plugins.fireHook, 'filter:privileges.global.list_human', privileges.global.privilegeLabels.slice()), + groups: async.apply(plugins.fireHook, 'filter:privileges.global.groups.list_human', privileges.global.privilegeLabels.slice()), }, 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); + helpers.getUserPrivileges(0, 'filter:privileges.global.list', privileges.global.userPrivilegeList, 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); + helpers.getGroupPrivileges(0, 'filter:privileges.global.groups.list', privileges.global.groupPrivilegeList, next); }, }, next); }, diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index 0f56e4f9c8..8f2f21f0f7 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -2,7 +2,11 @@ 'use strict'; var async = require('async'); +var _ = require('lodash'); + var groups = require('../groups'); +var user = require('../user'); +var plugins = require('../plugins'); var helpers = module.exports; @@ -111,3 +115,109 @@ function isGuestAllowedToPrivileges(privileges, cid, callback) { groups.isMemberOfGroups('guests', groupKeys, callback); } + +helpers.getUserPrivileges = function (cid, hookName, userPrivilegeList, callback) { + var userPrivileges; + var memberSets; + async.waterfall([ + async.apply(plugins.fireHook, hookName, userPrivilegeList.slice()), + function (_privs, next) { + userPrivileges = _privs; + groups.getMembersOfGroups(userPrivileges.map(function (privilege) { + return 'cid:' + cid + ':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); + }, + ], callback); +}; + +helpers.getGroupPrivileges = function (cid, hookName, groupPrivilegeList, callback) { + var groupPrivileges; + async.waterfall([ + async.apply(plugins.fireHook, hookName, groupPrivilegeList.slice()), + function (_privs, next) { + groupPrivileges = _privs; + async.parallel({ + memberSets: function (next) { + groups.getMembersOfGroups(groupPrivileges.map(function (privilege) { + return 'cid:' + cid + ':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; + }); + + groupNames = groups.ephemeralGroups.concat(groupNames); + 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); + }, + ], callback); +}; \ No newline at end of file diff --git a/src/routes/admin.js b/src/routes/admin.js index ac90b89b97..ae90f4716b 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -55,7 +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/privileges/:cid?', 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/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index 4addee15ba..cf8ab89312 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -2,18 +2,8 @@ <form role="form" class="category" data-cid="{category.cid}"> <div class="row"> - <div class="col-md-9"> - <ul class="nav nav-pills"> - <li class="active"><a href="#category-settings" data-toggle="tab"> - [[admin/manage/categories:settings]] - </a></li> - <li><a href="#privileges" data-toggle="tab">[[admin/manage/categories:privileges]]</a></li> - </ul> - </div> - <div class="col-md-3"> + <div class="col-md-3 pull-right"> <select id="category-selector" class="form-control"> - <option value="global" selected>[[admin/manage/privileges:global]]</option> - <option disabled>_____________</option> <!-- BEGIN allCategories --> <option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option> <!-- END allCategories --> @@ -176,19 +166,6 @@ </div> </div> </div> - - <div class="tab-pane fade col-xs-12" id="privileges"> - <p> - [[admin/manage/categories:privileges.description]] - </p> - <p class="text-warning"> - [[admin/manage/categories:privileges.warning]] - </p> - <hr /> - <div class="privilege-table-container"> - <!-- IMPORT admin/partials/categories/privileges.tpl --> - </div> - </div> </div> </form> </div> diff --git a/src/views/admin/manage/privileges.tpl b/src/views/admin/manage/privileges.tpl index a3596b39dc..8568767d0d 100644 --- a/src/views/admin/manage/privileges.tpl +++ b/src/views/admin/manage/privileges.tpl @@ -3,10 +3,10 @@ <div class="row"> <div class="col-md-3 pull-right"> <select id="category-selector" class="form-control"> - <option value="global" selected>[[admin/manage/privileges:global]]</option> + <option value="global" <!-- IF !cid --> selected <!-- ENDIF !cid -->>[[admin/manage/privileges:global]]</option> <option disabled>_____________</option> <!-- BEGIN allCategories --> - <option value="{allCategories.value}">{allCategories.text}</option> + <option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option> <!-- END allCategories --> </select> </div> @@ -16,14 +16,18 @@ <div class=""> <p> - [[admin/manage/privileges:global.description]] + [[admin/manage/categories:privileges.description]] </p> <p class="text-warning"> - [[admin/manage/privileges:global.warning]] + [[admin/manage/categories:privileges.warning]] </p> <hr /> <div class="privilege-table-container"> + <!-- IF cid --> + <!-- IMPORT admin/partials/categories/privileges.tpl --> + <!-- ELSE --> <!-- IMPORT admin/partials/global/privileges.tpl --> + <!-- ENDIF cid --> </div> </div> </form> From 025709499c60150991ccdd3060d7f2f1ff6d4559 Mon Sep 17 00:00:00 2001 From: Baris Usakli <barisusakli@gmail.com> Date: Wed, 20 Dec 2017 15:19:22 -0500 Subject: [PATCH 03/22] more fixes --- public/src/admin/manage/category.js | 1 - public/src/admin/manage/privileges.js | 4 ++-- src/controllers/admin/categories.js | 1 - src/controllers/admin/privileges.js | 3 +-- src/install.js | 6 ++++++ src/privileges/global.js | 4 ---- src/privileges/helpers.js | 2 +- src/upgrades/1.8.0/chat_privilege.js | 2 +- 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 71470baf46..fb4ae062b8 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -18,7 +18,6 @@ define('admin/manage/category', [ }); $('#category-selector').on('change', function () { - var val = $(this).val(); ajaxify.go('admin/manage/categories/' + $(this).val() + window.location.hash); }); diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index 98587fc87f..74aea3a195 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -4,7 +4,7 @@ define('admin/manage/privileges', [ 'autocomplete', 'translator', 'benchpress', - 'categorySelector' + 'categorySelector', ], function (autocomplete, translator, Benchpress, categorySelector) { var Privileges = {}; @@ -62,7 +62,7 @@ define('admin/manage/privileges', [ if (err) { return app.alertError(err.message); } - var tpl = cid ? 'admin/partials/categories/privileges' : 'admin/manage/privileges'; + var tpl = cid ? 'admin/partials/categories/privileges' : 'admin/partials/global/privileges'; Benchpress.parse(tpl, { privileges: privileges, }, function (html) { diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js index b3564afcef..e03b51745c 100644 --- a/src/controllers/admin/categories.js +++ b/src/controllers/admin/categories.js @@ -3,7 +3,6 @@ var async = require('async'); var categories = require('../../categories'); -var privileges = require('../../privileges'); var analytics = require('../../analytics'); var plugins = require('../../plugins'); var translator = require('../../translator'); diff --git a/src/controllers/admin/privileges.js b/src/controllers/admin/privileges.js index 1fad4cfd06..92dbe27ef9 100644 --- a/src/controllers/admin/privileges.js +++ b/src/controllers/admin/privileges.js @@ -13,7 +13,7 @@ privilegesController.get = function (req, res, callback) { function (next) { async.parallel({ privileges: function (next) { - if (!cid) { + if (!cid) { privileges.global.list(next); } else { privileges.categories.list(cid, next); @@ -23,7 +23,6 @@ privilegesController.get = function (req, res, callback) { }, next); }, function (data) { - data.allCategories.forEach(function (category) { if (category) { category.selected = parseInt(category.cid, 10) === parseInt(cid, 10); diff --git a/src/install.js b/src/install.js index b55b1ed08e..e9e8eecb49 100644 --- a/src/install.js +++ b/src/install.js @@ -353,6 +353,11 @@ function createGlobalModeratorsGroup(next) { ], next); } +function giveGlobalPrivileges(next) { + var groups = require('./groups'); + groups.join('cid:0:privileges:groups:chat', 'registered-users', next); +} + function createCategories(next) { var Categories = require('./categories'); @@ -498,6 +503,7 @@ install.setup = function (callback) { createCategories, createAdministrator, createGlobalModeratorsGroup, + giveGlobalPrivileges, createMenuItems, createWelcomePost, enableDefaultPlugins, diff --git a/src/privileges/global.js b/src/privileges/global.js index 0ca4ac4347..bf86029ac0 100644 --- a/src/privileges/global.js +++ b/src/privileges/global.js @@ -2,10 +2,8 @@ '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'); @@ -65,6 +63,4 @@ module.exports = function (privileges) { }, ], callback); }; - - }; diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index 8f2f21f0f7..5a1218ae19 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -220,4 +220,4 @@ helpers.getGroupPrivileges = function (cid, hookName, groupPrivilegeList, callba }, next); }, ], callback); -}; \ No newline at end of file +}; diff --git a/src/upgrades/1.8.0/chat_privilege.js b/src/upgrades/1.8.0/chat_privilege.js index d337784d2b..c4bd2ff8d1 100644 --- a/src/upgrades/1.8.0/chat_privilege.js +++ b/src/upgrades/1.8.0/chat_privilege.js @@ -7,6 +7,6 @@ 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); + groups.join('cid:0:privileges:groups:chat', 'registered-users', callback); }, }; From fb97ff226539c8a08005af09f82e2cee70cf81e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Thu, 21 Dec 2017 14:24:29 -0500 Subject: [PATCH 04/22] canChat should be false if chat is globally disabled --- src/middleware/header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/header.js b/src/middleware/header.js index add5a458aa..ddfe5af7bd 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -143,7 +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.canChat = results.canChat && parseInt(meta.config.disableChat, 10) !== 1; templateValues.user = results.user; templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true }); templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS; From e563e8ac82ad44ee113da20a0eaa0e83a6cc22f8 Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" <deploy@nodebb.org> Date: Fri, 29 Dec 2017 09:24:50 +0000 Subject: [PATCH 05/22] Latest translations and fallbacks --- .../cs/admin/appearance/customise.json | 8 ++--- .../language/cs/admin/manage/post-queue.json | 2 +- public/language/cs/admin/menu.json | 4 +-- .../cs/admin/settings/notifications.json | 2 +- .../cs/admin/settings/pagination.json | 4 +-- public/language/cs/admin/settings/post.json | 4 +-- public/language/cs/admin/settings/user.json | 4 +-- public/language/cs/email.json | 2 +- public/language/cs/error.json | 10 +++--- public/language/cs/flags.json | 8 ++--- public/language/cs/notifications.json | 32 +++++++++---------- public/language/cs/pages.json | 2 +- public/language/cs/topic.json | 6 ++-- public/language/cs/unread.json | 4 +-- public/language/cs/user.json | 20 ++++++------ public/language/nl/email.json | 4 +-- public/language/nl/error.json | 6 ++-- public/language/nl/notifications.json | 30 ++++++++--------- public/language/nl/topic.json | 6 ++-- public/language/nl/user.json | 10 +++--- 20 files changed, 84 insertions(+), 84 deletions(-) diff --git a/public/language/cs/admin/appearance/customise.json b/public/language/cs/admin/appearance/customise.json index c22a869f80..0cd4e0f8da 100644 --- a/public/language/cs/admin/appearance/customise.json +++ b/public/language/cs/admin/appearance/customise.json @@ -3,12 +3,12 @@ "custom-css.description": "Zadejte vlastní deklarace CSS, které budou použity na všechny ostatních styly.", "custom-css.enable": "Povolit uživatelské CSS", - "custom-js": "Custom Javascript", - "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", - "custom-js.enable": "Enable Custom Javascript", + "custom-js": "Uživatelský Javascript", + "custom-js.description": "Zadejte zde váš javascriptový kód. Bude spuštěn, jakmile se stránka plně načte.", + "custom-js.enable": "Povolit uživatelský Javascript", "custom-header": "Uživatelská hlavička", - "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <code><head></code> section of your forum's markup. Script tags are allowed, but are discouraged, as the <a href=\"#custom-header\" data-toggle=\"tab\">Custom Javascript</a> tab is available.", + "custom-header.description": "Zde zadejte uživatelské HTML (mimo Meta Tags, atd.), které bude připojeno k části značek <code><head></code> vašeho fóra.. Značky pro „script” jsou povoleny, ale nedoporučujeme je, neboť je dostupný <a href=\"#custom-header\" data-toggle=\"tab\">Uživatelský Javascript</a> .", "custom-header.enable": "Povolit uživatelskou hlavičku", "custom-css.livereload": "Povolit aktuální znovu načtení", diff --git a/public/language/cs/admin/manage/post-queue.json b/public/language/cs/admin/manage/post-queue.json index 57fe1f8ab7..e46490b1b7 100644 --- a/public/language/cs/admin/manage/post-queue.json +++ b/public/language/cs/admin/manage/post-queue.json @@ -7,5 +7,5 @@ "content": "Obsah", "posted": "Přidáno", "reply-to": "Odpovědět na \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Kvůli úpravám a před odesláním příspěvku můžete klikat na obsah." } \ No newline at end of file diff --git a/public/language/cs/admin/menu.json b/public/language/cs/admin/menu.json index 8b9812e514..7de94b7a5c 100644 --- a/public/language/cs/admin/menu.json +++ b/public/language/cs/admin/menu.json @@ -39,7 +39,7 @@ "section-appearance": "Vzhled", "appearance/themes": "Motivy", "appearance/skins": "Vzhledy", - "appearance/customise": "Custom Content (HTML/JS/CSS)", + "appearance/customise": "Uživatelský obsah (HTML/JS/CSS)", "section-extend": "Rozšířit", "extend/plugins": "Rozšíření", @@ -65,7 +65,7 @@ "logout": "Odhlásit", "view-forum": "Zobrazit fórum", - "search.placeholder": "Search for settings", + "search.placeholder": "Hledat nastavení", "search.no-results": "Žádné výsledky…", "search.search-forum": "Prohledat fórum pro <strong></strong>", "search.keep-typing": "Pište dále pro zobrazení výsledků…", diff --git a/public/language/cs/admin/settings/notifications.json b/public/language/cs/admin/settings/notifications.json index fd95917606..39bc83bdcb 100644 --- a/public/language/cs/admin/settings/notifications.json +++ b/public/language/cs/admin/settings/notifications.json @@ -2,5 +2,5 @@ "notifications": "Oznámení", "welcome-notification": "Uvítání", "welcome-notification-link": "Odkaz na uvítání", - "welcome-notification-uid": "Welcome Notification User (UID)" + "welcome-notification-uid": "Uvítání uživatele (UID)" } \ No newline at end of file diff --git a/public/language/cs/admin/settings/pagination.json b/public/language/cs/admin/settings/pagination.json index dc1faf68da..34052b2a9c 100644 --- a/public/language/cs/admin/settings/pagination.json +++ b/public/language/cs/admin/settings/pagination.json @@ -3,9 +3,9 @@ "enable": "Stránkovat témata a příspěvky namísto nekonečného posouvání", "topics": "Stránkování témat", "posts-per-page": "Příspěvků na stránku", - "max-posts-per-page": "Maximum posts per page", + "max-posts-per-page": "Maximální množství příspěvků na stránku", "categories": "Stránkování kategorii", "topics-per-page": "Témat na stránku", - "max-topics-per-page": "Maximum topics per page", + "max-topics-per-page": "Maximální množství témat na stránku", "initial-num-load": "Počáteční počet témat pro načtení u nepřečtených, posledních a polulárních" } \ No newline at end of file diff --git a/public/language/cs/admin/settings/post.json b/public/language/cs/admin/settings/post.json index 67d297b33b..de8832b501 100644 --- a/public/language/cs/admin/settings/post.json +++ b/public/language/cs/admin/settings/post.json @@ -3,8 +3,8 @@ "sorting.post-default": "Výchozí třídění příspěvků", "sorting.oldest-to-newest": "Od nejstarších po nejnovější", "sorting.newest-to-oldest": "Od nejnovějších po nejstarší", - "sorting.most-votes": "Dle hlasování", - "sorting.most-posts": "Most Posts", + "sorting.most-votes": "Dle počtu hlasů", + "sorting.most-posts": "Dle počtu příspěvků", "sorting.topic-default": "Výchozí třídění tématu", "restrictions": "Omezení příspěvků", "restrictions.post-queue": "Povolit frontu pro příspěvky", diff --git a/public/language/cs/admin/settings/user.json b/public/language/cs/admin/settings/user.json index 2ad41f23cf..e683492860 100644 --- a/public/language/cs/admin/settings/user.json +++ b/public/language/cs/admin/settings/user.json @@ -19,8 +19,8 @@ "themes": "Motivy", "disable-user-skins": "Zabránit uživateli ve výběru vlastního vzhledu", "account-protection": "Ochrana účtu", - "admin-relogin-duration": "Admin relogin duration (minutes)", - "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "admin-relogin-duration": "Doba pro opětovné přihlášení správce (minuty)", + "admin-relogin-duration-help": "Po nastavení počtu přístupu do správcovské části, bude vyžadováno opětovné přihlášení. Pro zakázání, nastavte na 0.", "login-attempts": "Počet pokusů o přihlášení za hodinu", "login-attempts-help": "Překročí-li pokusy o přihlášení uživatele/ů tuto hranici, účet bude uzamknut na určený čas", "lockout-duration": "Délka blokování účtu (v minutách)", diff --git a/public/language/cs/email.json b/public/language/cs/email.json index 1d1efa10e9..b330291160 100644 --- a/public/language/cs/email.json +++ b/public/language/cs/email.json @@ -30,7 +30,7 @@ "notif.chat.unsub.info": "Toto upozornění na chat vám bylo odesláno na základě vašeho nastavení odběru.", "notif.post.cta": "Klikněte zde pro přečtené celého tématu", "notif.post.unsub.info": "Toto upozornění na příspěvek vám bylo odesláno na základě vašeho nastavení odběru.", - "notif.cta": "Click here to go to forum", + "notif.cta": "Pro přejití na fórum, klikněte zde", "test.text1": "Tento testovací e-mail slouží k ověření, že je e-mailer správně nastaven pro práci s NodeBB.", "unsub.cta": "Chcete-li změnit tyto nastavení, klikněte zde.", "banned.subject": "Byl jste zablokován od %1", diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 43ea641999..18576ee9bc 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -11,7 +11,7 @@ "invalid-uid": "Neplatné ID uživatele", "invalid-username": "Neplatné uživatelské jméno", "invalid-email": "Neplatný e-mail", - "invalid-title": "Invalid title", + "invalid-title": "Neplatný název", "invalid-user-data": "Neplatná uživatelská data", "invalid-password": "Neplatné heslo", "invalid-login-credentials": "Neplatné přihlašovací údaje", @@ -81,7 +81,7 @@ "cant-ban-other-admins": "Nemůžete zablokovat jiné správce.", "cant-remove-last-admin": "Jste jediným správcem. Před vlastním odebráním oprávnění správce nejdříve přidejte jiného uživatele jako správce", "cant-delete-admin": "Před odstraněním účtu mu nejprve odeberte oprávnění správce.", - "invalid-image": "Invalid image", + "invalid-image": "Neplatný obrázek", "invalid-image-type": "Neplatný typ obrázku. Povolené typy jsou: %1", "invalid-image-extension": "Neplatná přípona obrázku", "invalid-file-type": "Neplatný typ souboru. Povolené typy jsou: %1", @@ -119,13 +119,13 @@ "not-enough-reputation-to-downvote": "Nemáte dostatečnou reputaci pro vyjádření nesouhlasu u tohoto příspěvku", "not-enough-reputation-to-flag": "Pro označení tohoto příspěvku nemáte dostatečnou reputaci", "already-flagged": "Tento příspěvek jste již označil", - "self-vote": "You cannot vote on your own post", + "self-vote": "U svého vlastního příspěvku nemůžete hlasovat", "reload-failed": "Vyskytla se chyba v NodeBB při znovu načtení: \"%1\". NodeBB bude pokračovat v běhu na straně klienta, nicméně byste měl/a přenastavit zpět to, co jste udělal/a před opětovným načtením.", "registration-error": "Chyba při registraci", "parse-error": "Při analýze odpovědi serveru nastala chyba", "wrong-login-type-email": "Pro přihlášení použijte vaši e-mailovou adresu", "wrong-login-type-username": "Pro přihlášení použijte vaše uživatelské jméno", - "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-registration-disabled": "Registrace byla zakázána pro účty - %1. Nejprve si zaregistrujte e-mailovou adresu", "invite-maximum-met": "Již jste pozval/a maximálně možný počet lidí (%1 z %2).", "no-session-found": "Nebyla nalezena relace s přihlášením.", "not-in-room": "Uživatel není přítomen v místnosti", @@ -135,5 +135,5 @@ "invalid-home-page-route": "Neplatná cesta k domovské stránkce", "invalid-session": "Nesoulad v relacích", "invalid-session-text": "Zdá se, že vše relace s přihlášením již není aktivní nebo již neodpovídá s relací na serveru. Obnovte prosím tuto stránku.", - "no-topics-selected": "No topics selected!" + "no-topics-selected": "Žádná vybraná témata." } \ No newline at end of file diff --git a/public/language/cs/flags.json b/public/language/cs/flags.json index e221f6965b..eb37572f8a 100644 --- a/public/language/cs/flags.json +++ b/public/language/cs/flags.json @@ -54,11 +54,11 @@ "modal-body": "Zadejte váš důvod k označení %1 %2 pro kontrolu. Nebo použijte tlačítko je-li dostupné.", "modal-reason-spam": "Spam", "modal-reason-offensive": "Urážlivé", - "modal-reason-other": "Other (specify below)", + "modal-reason-other": "Jiné (popište níže)", "modal-reason-custom": "Důvod ohlášení tohoto obsahu…", "modal-submit": "Předat hlášení", "modal-submit-success": "Obsah byl označen pro moderaci.", - "modal-submit-confirm": "Confirm Submission", - "modal-submit-confirm-text": "You have a custom reason specified already. Are you sure you wish to submit via quick-report?", - "modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined." + "modal-submit-confirm": "Potvrdit hlášení", + "modal-submit-confirm-text": "Již jste zadal/a nějaký důvod. Jste si jist/a, že chcete nahlásit pomocí rychlé zprávy?", + "modal-submit-confirm-text-help": "Zaslání rychlé zprávy přepíše jiné zadané důvody." } \ No newline at end of file diff --git a/public/language/cs/notifications.json b/public/language/cs/notifications.json index 8182ae3f47..284b394c6f 100644 --- a/public/language/cs/notifications.json +++ b/public/language/cs/notifications.json @@ -8,8 +8,8 @@ "outgoing_link_message": "Opouštíte %1", "continue_to": "Pokračovat na %1", "return_to": "Vrátit se na %1", - "new_notification": "Nové upozornění", - "new_notification_from": "You have a new Notification from %1", + "new_notification": "Nové oznámení", + "new_notification_from": "Máte nové upozornění od %1", "you_have_unread_notifications": "Máte nepřečtená upozornění.", "all": "Vše", "topics": "Témata", @@ -47,18 +47,18 @@ "email-confirmed-message": "Děkujeme za ověření vaší e-mailové adresy. Váš účet je nyní aktivní.", "email-confirm-error-message": "Nastal problém s ověřením vaší e-mailové adresy. Kód je pravděpodobně neplatný nebo jeho platnost vypršela.", "email-confirm-sent": "Ověřovací e-mail odeslán.", - "none": "None", - "notification_only": "Notification Only", - "email_only": "Email Only", - "notification_and_email": "Notification & Email", - "notificationType_upvote": "When someone upvotes your post", - "notificationType_new-topic": "When someone you follow posts a topic", - "notificationType_new-reply": "When a new reply is posted in a topic you are watching", - "notificationType_follow": "When someone starts following you", - "notificationType_new-chat": "When you receive a chat message", - "notificationType_group-invite": "When you receive a group invite", - "notificationType_new-register": "When someone gets added to registration queue", - "notificationType_post-queue": "When a new post is queued", - "notificationType_new-post-flag": "When a post is flagged", - "notificationType_new-user-flag": "When a user is flagged" + "none": "Nic", + "notification_only": "Jen oznámení", + "email_only": "Jen e-mail", + "notification_and_email": "Oznámení a e-mail", + "notificationType_upvote": "Vyjádří-li někdo souhlas s vaším příspěvkem", + "notificationType_new-topic": "Začne-li někdo sledovat příspěvky a téma", + "notificationType_new-reply": "Bude-li přidán nový příspěvek v tématu, které sledujete", + "notificationType_follow": "Začne-li vás někdo sledovat", + "notificationType_new-chat": "Obdržíte-li novou konverzační zprávu", + "notificationType_group-invite": "Obdržíte-li pozvání ke skupině", + "notificationType_new-register": "Bude-li někdo přidán do registrační fronty", + "notificationType_post-queue": "Bude-li přidán nový příspěvek do fronty", + "notificationType_new-post-flag": "Bude-li příspěvek označen", + "notificationType_new-user-flag": "Bude-li uživatel označen" } \ No newline at end of file diff --git a/public/language/cs/pages.json b/public/language/cs/pages.json index 8a0270a9db..befc7ceba3 100644 --- a/public/language/cs/pages.json +++ b/public/language/cs/pages.json @@ -44,7 +44,7 @@ "account/bookmarks": "%1's zazáložkované příspěvky", "account/settings": "Uživatelské nastavení", "account/watched": "Témata sledovaná uživatelem %1", - "account/ignored": "Topics ignored by %1", + "account/ignored": "Témata ignorovaná uživatelem %1", "account/upvoted": "Souhlasí s příspěvkem %1", "account/downvoted": "Nesouhlasí s příspěvkem %1", "account/best": "Nejlepší příspěvky od %1", diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json index 7cedca5e24..80e7561e26 100644 --- a/public/language/cs/topic.json +++ b/public/language/cs/topic.json @@ -68,8 +68,8 @@ "thread_tools.restore_confirm": "Jste si jist/a, že chcete toto téma obnovit?", "thread_tools.purge": "Vyčistit téma", "thread_tools.purge_confirm": "Jste si jist/a, že chcete vyčistit toto téma?", - "thread_tools.merge_topics": "Merge Topics", - "thread_tools.merge": "Merge", + "thread_tools.merge_topics": "Sloučit témata", + "thread_tools.merge": "Sloučit", "topic_move_success": "Toto téma bylo úspěšně přesunuto do %1", "post_delete_confirm": "Jste si jist/a, že chcete odstranit tento příspěvek?", "post_restore_confirm": "Jste si jist/a, že chcete obnovit tento příspěvek?", @@ -91,7 +91,7 @@ "fork_pid_count": "Vybráno %1 příspěvek/ů", "fork_success": "Téma úspěšně rozděleno. Pro přejití na rozdělené téma, zde klikněte.", "delete_posts_instruction": "Klikněte na příspěvek, který chcete odstranit/vyčistit", - "merge_topics_instruction": "Click the topics you want to merge", + "merge_topics_instruction": "Pro sloučení témat, klikněte na ně", "composer.title_placeholder": "Zadejte název tématu…", "composer.handle_placeholder": "Jméno", "composer.discard": "Zrušit", diff --git a/public/language/cs/unread.json b/public/language/cs/unread.json index 60381d5efd..35035c5cb0 100644 --- a/public/language/cs/unread.json +++ b/public/language/cs/unread.json @@ -10,6 +10,6 @@ "all-topics": "Všechna témata", "new-topics": "Nová témata", "watched-topics": "Sledovaná témata", - "unreplied-topics": "Unreplied Topics", - "multiple-categories-selected": "Multiple Selected" + "unreplied-topics": "Neodpovězené témata", + "multiple-categories-selected": "Vícenásobný výběr" } \ No newline at end of file diff --git a/public/language/cs/user.json b/public/language/cs/user.json index 0042eb8ec6..8fce80c61c 100644 --- a/public/language/cs/user.json +++ b/public/language/cs/user.json @@ -25,7 +25,7 @@ "reputation": "Reputace", "bookmarks": "Záložky", "watched": "Sledován", - "ignored": "Ignored", + "ignored": "Ignorován", "followers": "Sledují ho", "following": "Sleduje", "aboutme": "O mně", @@ -85,7 +85,7 @@ "has_no_posts": "Tento uživatel ještě nic nenapsal.", "has_no_topics": "Tento uživatel ještě nezaložil žádné téma.", "has_no_watched_topics": "Tento uživatel zatím nesleduje žádná témata.", - "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_ignored_topics": "Tento uživatel ještě neignoruje žádné témata.", "has_no_upvoted_posts": "Tento uživatel zatím nevyjádřil souhlas u žádného příspěvku.", "has_no_downvoted_posts": "Tento uživatel zatím nevyjádřil nesouhlas u žádného příspěvku.", "has_no_voted_posts": "Tento uživatel nemá žádné hlasovací příspěvky", @@ -101,11 +101,11 @@ "outgoing-message-sound": "Zvuk odchozí zprávy", "notification-sound": "Zvuk oznámení", "no-sound": "Bez zvuku", - "upvote-notif-freq": "Upvote Notification Frequency", - "upvote-notif-freq.all": "All Upvotes", - "upvote-notif-freq.everyTen": "Every Ten Upvotes", - "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", - "upvote-notif-freq.disabled": "Disabled", + "upvote-notif-freq": "Frekvence upozornění na souhlasy", + "upvote-notif-freq.all": "Všechny souhlasy", + "upvote-notif-freq.everyTen": "Každý desátý souhlas", + "upvote-notif-freq.logarithmic": "Dle 10, 100, 1000...", + "upvote-notif-freq.disabled": "Zakázáno", "browsing": "Nastavení prohlížení", "open_links_in_new_tab": "Otevřít odchozí odkaz v nové záložce", "enable_topic_searching": "Povolit vyhledávání v tématu", @@ -126,9 +126,9 @@ "sso.title": "Služby jednotného přihlášení", "sso.associated": "Přiřazeno k", "sso.not-associated": "Zde klikněte pro přiřazení k", - "sso.dissociate": "Dissociate", - "sso.dissociate-confirm-title": "Confirm Dissociation", - "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "sso.dissociate": "Odloučit", + "sso.dissociate-confirm-title": "Potvrdit odloučení", + "sso.dissociate-confirm": "Jste si jist/a, že chcete odloučit váš účet z %1?", "info.latest-flags": "Poslední označené", "info.no-flags": "Nebyly nalezeny žádné označené příspěvky", "info.ban-history": "Poslední historie blokovaných", diff --git a/public/language/nl/email.json b/public/language/nl/email.json index 48f35a2c6f..bd474f945e 100644 --- a/public/language/nl/email.json +++ b/public/language/nl/email.json @@ -18,7 +18,7 @@ "reset.notify.text2": "Neem onmiddellijk contact met een beheerder op wanneer je hiervoor geen toestemming hebt gegeven.", "digest.notifications": "Er zijn ongelezen notificaties van %1:", "digest.latest_topics": "De meest recente onderwerpen van %1", - "digest.cta": "Klik hier om deze website te bezoeken %1 ", + "digest.cta": "Klik hier om %1 te bezoeken ", "digest.unsub.info": "Deze samenvatting hebben we naar je verzonden omdat je dat hebt ingesteld.", "digest.no_topics": "In de afgelopen %1 zijn er geen actieve onderwerpen geweest.", "digest.day": "dag", @@ -30,7 +30,7 @@ "notif.chat.unsub.info": "Deze notificatie is verzonden vanwege de gebruikersinstellingen voor abonnementen.", "notif.post.cta": "Klik hier om het volledige bericht te lezen", "notif.post.unsub.info": "Deze notificatie is door ons verzonden vanwege gebruikersinstellingen voor abonnementen en berichten.", - "notif.cta": "Click here to go to forum", + "notif.cta": "Klik hier om naar het forum te gaan", "test.text1": "Dit is een testbericht om te verifiëren dat NodeBB de e-mailberichtservice correct heeft opgezet.", "unsub.cta": "Klik hier om deze instellingen te wijzigen", "banned.subject": "U bent verbannen van %1", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 460254c097..48931aa461 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -119,13 +119,13 @@ "not-enough-reputation-to-downvote": "Je hebt onvoldoende reputatie om een negatieve stem uit te mogen brengen", "not-enough-reputation-to-flag": "Je hebt onvoldoende reputatie om dit bericht aan de beheerders te mogen melden", "already-flagged": "Je hebt deze post al gerapporteerd", - "self-vote": "You cannot vote on your own post", + "self-vote": "Het is niet mogelijk om op je eigen bericht te stemmen", "reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegengekomen. NodeBB blijft operationeel. Echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.", "registration-error": "Fout tijdens registratie", "parse-error": "Tijdens het verwerken van het antwoord van de server is er iets misgegaan.", "wrong-login-type-email": "Gebruik je e-mailadres om in te loggen", "wrong-login-type-username": "Gebruik je gebruikersnaam om in te loggen", - "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-registration-disabled": "Registratie is uitgeschakeld voor %1 accounts, registreer eerst met een e-mailadres", "invite-maximum-met": "Je heb het maximum aantal mensen uitgenodigd (%1 van de %2).", "no-session-found": "Geen login sessie gevonden!", "not-in-room": "Gebruiker niet in de chat", @@ -135,5 +135,5 @@ "invalid-home-page-route": "Onbekende homepage route", "invalid-session": "Verkeerde sessie combinatie", "invalid-session-text": "Het lijkt erop dat je login sessie niet meer actief is of niet langer synchroon is met de server. Ververs de pagina.", - "no-topics-selected": "No topics selected!" + "no-topics-selected": "Geen onderwerpen geselecteerd!" } \ No newline at end of file diff --git a/public/language/nl/notifications.json b/public/language/nl/notifications.json index c19dbaef83..78ec3289b5 100644 --- a/public/language/nl/notifications.json +++ b/public/language/nl/notifications.json @@ -9,7 +9,7 @@ "continue_to": "Door naar %1", "return_to": "Terug naar %1", "new_notification": "Nieuwe notificatie", - "new_notification_from": "You have a new Notification from %1", + "new_notification_from": "Je hebt een nieuwe notificatie van %1", "you_have_unread_notifications": "Je hebt nieuwe notificaties.", "all": "Alles", "topics": "Onderwerpen", @@ -47,18 +47,18 @@ "email-confirmed-message": "Bedankt voor het bevestigen van je e-mailadres. Je account is nu volledig geactiveerd.", "email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.", "email-confirm-sent": "Bevestigingsmail verstuurd.", - "none": "None", - "notification_only": "Notification Only", - "email_only": "Email Only", - "notification_and_email": "Notification & Email", - "notificationType_upvote": "When someone upvotes your post", - "notificationType_new-topic": "When someone you follow posts a topic", - "notificationType_new-reply": "When a new reply is posted in a topic you are watching", - "notificationType_follow": "When someone starts following you", - "notificationType_new-chat": "When you receive a chat message", - "notificationType_group-invite": "When you receive a group invite", - "notificationType_new-register": "When someone gets added to registration queue", - "notificationType_post-queue": "When a new post is queued", - "notificationType_new-post-flag": "When a post is flagged", - "notificationType_new-user-flag": "When a user is flagged" + "none": "Geen", + "notification_only": "Alleen notificatie", + "email_only": "Alleen e-mail", + "notification_and_email": "Notificatie & e-mail", + "notificationType_upvote": "Als iemand positief stemt voor je bericht", + "notificationType_new-topic": "Wanneer iemand die jij volgt een onderwerp post", + "notificationType_new-reply": "Als een nieuwe reactie komt op een onderwerp dat je volgt", + "notificationType_follow": "Als iemand begint met jou te volgen", + "notificationType_new-chat": "Als je een chat-bericht ontvangt", + "notificationType_group-invite": "Als je een uitnodiging voor een groep ontvangt", + "notificationType_new-register": "Als iemand wordt toegevoegd aan een registratiewachtrij", + "notificationType_post-queue": "Als een bericht aan de wachtrij wordt toegevoegd", + "notificationType_new-post-flag": "Als een bericht wordt gevlagd", + "notificationType_new-user-flag": "Als een gebruiker wordt gevlagd" } \ No newline at end of file diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index b1d33fd9da..29b4eb9e75 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -68,8 +68,8 @@ "thread_tools.restore_confirm": "Zeker weten dit onderwerp te herstellen?", "thread_tools.purge": "Wis onderwerp ", "thread_tools.purge_confirm": "Weet je zeker dat je dit onderwerp wil verwijderen?", - "thread_tools.merge_topics": "Merge Topics", - "thread_tools.merge": "Merge", + "thread_tools.merge_topics": "Onderwerpen samenvoegen", + "thread_tools.merge": "Samenvoegen", "topic_move_success": "Verplaatsen van onderwerp naar %1 succesvol", "post_delete_confirm": "Is het absoluut de bedoeling dit bericht te verwijderen?", "post_restore_confirm": "Is het de bedoeling dit bericht te herstellen?", @@ -91,7 +91,7 @@ "fork_pid_count": "%1 bericht(en) geselecteerd", "fork_success": "Onderwerp is succesvol afgesplitst. Klik hier om het nieuwe onderwerp te zien.", "delete_posts_instruction": "Klik op de berichten die verwijderd moeten worden", - "merge_topics_instruction": "Click the topics you want to merge", + "merge_topics_instruction": "Klik op de onderwerpen die samengevoegd moeten worden", "composer.title_placeholder": "Voer hier de titel van het onderwerp in...", "composer.handle_placeholder": "Naam", "composer.discard": "Annuleren", diff --git a/public/language/nl/user.json b/public/language/nl/user.json index 6351ae8827..9c2af5d6f5 100644 --- a/public/language/nl/user.json +++ b/public/language/nl/user.json @@ -101,11 +101,11 @@ "outgoing-message-sound": "Uitgaand bericht geluid", "notification-sound": "Notificatie geluid", "no-sound": "Geen geluid", - "upvote-notif-freq": "Upvote Notification Frequency", - "upvote-notif-freq.all": "All Upvotes", - "upvote-notif-freq.everyTen": "Every Ten Upvotes", - "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", - "upvote-notif-freq.disabled": "Disabled", + "upvote-notif-freq": "Notificatie frequentie voor Upvotes", + "upvote-notif-freq.all": "Alle Upvotes", + "upvote-notif-freq.everyTen": "Elke tien Upvotes", + "upvote-notif-freq.logarithmic": "Bij 10, 100, 1000...", + "upvote-notif-freq.disabled": "Uitgeschakeld", "browsing": "Instellingen voor bladeren", "open_links_in_new_tab": "Open uitgaande links naar een externe site in een nieuw tabblad", "enable_topic_searching": "Inschakelen mogelijkheid op onderwerp te kunnen zoeken", From 2661a31227c54c6a2716f2d41d4ff644a3652a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Fri, 29 Dec 2017 13:27:07 -0500 Subject: [PATCH 06/22] closes #6202 --- src/notifications.js | 2 +- src/topics/follow.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/notifications.js b/src/notifications.js index 6a7940d50d..4bf9782e61 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -220,7 +220,7 @@ function pushToUids(uids, notification, callback) { async.eachLimit(uids, 3, function (uid, next) { emailer.send('notification', uid, { path: notification.path, - subject: '[[notifications:new_notification_from, ' + meta.config.title + ']]', + subject: notification.subject || '[[notifications:new_notification_from, ' + meta.config.title + ']]', intro: utils.stripHTMLTags(notification.bodyShort), body: utils.stripHTMLTags(notification.bodyLong || ''), showUnsubscribe: true, diff --git a/src/topics/follow.js b/src/topics/follow.js index cf8754bcc5..aad9c0f079 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -9,6 +9,7 @@ var notifications = require('../notifications'); var privileges = require('../privileges'); var plugins = require('../plugins'); var utils = require('../utils'); +var meta = require('../meta'); module.exports = function (Topics) { Topics.toggleFollow = function (tid, uid, callback) { @@ -219,6 +220,7 @@ module.exports = function (Topics) { notifications.create({ type: 'new-reply', + subject: '[' + (meta.config.title || 'NodeBB') + '] ' + title, bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', bodyLong: postData.content, pid: postData.pid, From 1fd0b760b15945a418c338e3a0d2a36f5b02bedb Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" <deploy@nodebb.org> Date: Sat, 30 Dec 2017 09:25:45 +0000 Subject: [PATCH 07/22] Latest translations and fallbacks --- public/language/fr/admin/appearance/customise.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/language/fr/admin/appearance/customise.json b/public/language/fr/admin/appearance/customise.json index ada04f6546..187f799efb 100644 --- a/public/language/fr/admin/appearance/customise.json +++ b/public/language/fr/admin/appearance/customise.json @@ -3,9 +3,9 @@ "custom-css.description": "Entrez vos propres déclarations de CSS ici, elles seront appliquées après tous les autres styles.", "custom-css.enable": "Activer les CSS personnalisés", - "custom-js": "Custom Javascript", - "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", - "custom-js.enable": "Enable Custom Javascript", + "custom-js": "Javascript personnalisé", + "custom-js.description": "Entrez votre Javascript ici. Celui-ci sera exécute après le chargement complet de la page.", + "custom-js.enable": "Activer le Javascript personnalisé", "custom-header": "En-tête personnalisé", "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <code><head></code> section of your forum's markup. Script tags are allowed, but are discouraged, as the <a href=\"#custom-header\" data-toggle=\"tab\">Custom Javascript</a> tab is available.", From cbaa9772336fc2355d2767727949fff1104edfb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Sat, 30 Dec 2017 11:41:31 -0500 Subject: [PATCH 08/22] fix type on mongodb if key has expireAt --- src/database/mongo/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 278ae6c413..509f19006b 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -108,6 +108,7 @@ module.exports = function (db, module) { if (!data) { return callback(null, null); } + delete data.expireAt; var keys = Object.keys(data); if (keys.length === 4 && data.hasOwnProperty('_key') && data.hasOwnProperty('score') && data.hasOwnProperty('value')) { return callback(null, 'zset'); From 64cbb331b7a093f1423a995692b211d42a1c5ea1 Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" <deploy@nodebb.org> Date: Sun, 31 Dec 2017 09:24:49 +0000 Subject: [PATCH 09/22] Latest translations and fallbacks --- public/language/it/email.json | 10 +++++----- public/language/it/error.json | 18 +++++++++--------- public/language/it/flags.json | 26 +++++++++++++------------- public/language/it/global.json | 4 ++-- public/language/it/groups.json | 4 ++-- public/language/it/notifications.json | 8 ++++---- public/language/it/pages.json | 4 ++-- public/language/it/register.json | 2 +- public/language/it/search.json | 2 +- public/language/it/success.json | 2 +- public/language/it/topic.json | 14 +++++++------- public/language/it/unread.json | 4 ++-- public/language/it/user.json | 22 +++++++++++----------- 13 files changed, 60 insertions(+), 60 deletions(-) diff --git a/public/language/it/email.json b/public/language/it/email.json index bcada51df4..f6fdaef532 100644 --- a/public/language/it/email.json +++ b/public/language/it/email.json @@ -13,7 +13,7 @@ "reset.text1": "Abbiamo ricevuto una richiesta di reset della tua password, probabilmente perché l'hai dimenticata. Se non è così si prega di ignorare questa email.", "reset.text2": "Per confermare il reset della password per favore clicca il seguente link:", "reset.cta": "Clicca qui per resettare la tua password", - "reset.notify.subject": "Possword modificata con successo.", + "reset.notify.subject": "Password modificata con successo.", "reset.notify.text1": "Ti informiamo che in data %1, la password è stata cambiata con successo.", "reset.notify.text2": "Se non hai autorizzato questo, per favore notifica immediatamente un amministratore.", "digest.notifications": "Hai una notifica non letta da %1:", @@ -33,9 +33,9 @@ "notif.cta": "Vai alla discussione", "test.text1": "Questa è una email di test per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.", "unsub.cta": "Clicca qui per modificare queste impostazioni", - "banned.subject": "You have been banned from %1", - "banned.text1": "The user %1 has been banned from %2.", - "banned.text2": "This ban will last until %1.", - "banned.text3": "This is the reason why you have been banned:", + "banned.subject": "Sei stato bannato da %1", + "banned.text1": "%1 è stato bannato da %2", + "banned.text2": "Questo ban durerà fino a %1.", + "banned.text3": "Il motivo del ban è:", "closing": "Grazie!" } \ No newline at end of file diff --git a/public/language/it/error.json b/public/language/it/error.json index 9fa55ff42e..0ad1f963ca 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -11,10 +11,10 @@ "invalid-uid": "ID Utente non valido", "invalid-username": "Nome utente non valido", "invalid-email": "Email non valida", - "invalid-title": "Invalid title", + "invalid-title": "Titolo non Valido", "invalid-user-data": "Dati Utente non validi", "invalid-password": "Password non valida", - "invalid-login-credentials": "Invalid login credentials", + "invalid-login-credentials": "Credenziali non Valide", "invalid-username-or-password": "Si prega di specificare sia un nome utente che una password", "invalid-search-term": "Termine di ricerca non valido", "csrf-invalid": "Non siamo riusciti a farti connettere, probabilmente perché la sessione è scaduta. Per favore riprova.", @@ -66,7 +66,7 @@ "content-too-long": "Inserisci un post più breve. I post non possono essere più lunghi di %1 caratteri.", "title-too-short": "Inserisci un titolo più lungo. I titoli devono contenere almeno %1 caratteri.", "title-too-long": "Inserisci un titolo più corto. I titoli non possono essere più lunghi di %1 caratteri.", - "category-not-selected": "Category not selected.", + "category-not-selected": "Categoria non selezionata.", "too-many-posts": "È possibile inserire un Post ogni %1 secondi - si prega di attendere prima di postare di nuovo", "too-many-posts-newbie": "Come nuovo utente puoi postare solamente una volta ogni %1 secondi finché non hai raggiunto un livello di reputazione %2 - per favore attendi prima di scrivere ancora", "tag-too-short": "Inserisci un tag più lungo. I tag devono contenere almeno %1 caratteri.", @@ -76,12 +76,12 @@ "still-uploading": "Per favore attendere il completamento degli uploads.", "file-too-big": "La dimensione massima consentita è di %1 kB - si prega di caricare un file più piccolo", "guest-upload-disabled": "Il caricamento da ospite è stato disattivato", - "already-bookmarked": "You have already bookmarked this post", - "already-unbookmarked": "You have already unbookmarked this post", + "already-bookmarked": "Hai già aggiunto questa discussione ai preferiti.", + "already-unbookmarked": "Hai già rimosso questa discussione dai preferiti", "cant-ban-other-admins": "Non puoi bannare altri amministratori!", "cant-remove-last-admin": "Sei l'unico Amministratore. Aggiungi un altro amministratore prima di rimuovere il tuo ruolo", "cant-delete-admin": "Togli i privilegi amministrativi da questo account prima di provare ad eliminarlo.", - "invalid-image": "Invalid image", + "invalid-image": "Immagine non Valida", "invalid-image-type": "Tipo dell'immagine non valido. I tipi permessi sono: %1", "invalid-image-extension": "Estensione immagine non valida", "invalid-file-type": "Tipo di file non valido. I formati consentiti sono: %1", @@ -109,7 +109,7 @@ "chat-disabled": "Il sistema di chat è stato disabilitato", "too-many-messages": "Hai inviato troppi messaggi, aspetta un attimo.", "invalid-chat-message": "Messaggio chat non valido", - "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "chat-message-too-long": "I messaggi in chat non possono superare i %1 caratteri.", "cant-edit-chat-message": "Non ti è permesso di modificare questo messaggio", "cant-remove-last-user": "Non puoi rimuovere l'ultimo utente", "cant-delete-chat-message": "Non ti è permesso di eliminare questo messaggio", @@ -119,7 +119,7 @@ "not-enough-reputation-to-downvote": "Non hai i privilegi per votare negativamente questo post", "not-enough-reputation-to-flag": "Tu non hai abbastanza reputazione per segnalare questo Post", "already-flagged": "Hai già messo marcato questo post", - "self-vote": "You cannot vote on your own post", + "self-vote": "Non puoi votare la tua stessa discussione", "reload-failed": "NodeBB ha incontrato un problema durante il ricaricamento: \"%1\". NodeBB continuerà a servire gli assets esistenti lato client, così puoi annullare quello che hai fatto prima di ricaricare.", "registration-error": "Errore nella registrazione", "parse-error": "Qualcosa è andato storto durante l'analisi della risposta proveniente dal server", @@ -135,5 +135,5 @@ "invalid-home-page-route": "Percorso della pagina iniziale non valido", "invalid-session": "Discrepanza della sessione", "invalid-session-text": "Sembra che la tua sessione non sia più attiva, oppure non corrisponde con il server. Per favore ricarica la pagina.", - "no-topics-selected": "No topics selected!" + "no-topics-selected": "Nessuna discussione selezionata!" } \ No newline at end of file diff --git a/public/language/it/flags.json b/public/language/it/flags.json index d05a5b25a8..14aaf7af02 100644 --- a/public/language/it/flags.json +++ b/public/language/it/flags.json @@ -27,12 +27,12 @@ "quick-links": "Quick Links", "flagged-user": "Flagged User", - "view-profile": "View Profile", + "view-profile": "Vedi Profilo", "start-new-chat": "Start New Chat", "go-to-target": "View Flag Target", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Vedi Profilo", + "user-edit": "Modifica Profilo", "notes": "Flag Notes", "add-note": "Add Note", @@ -44,21 +44,21 @@ "state-all": "All states", "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", + "state-wip": "Lavori in Corso", + "state-resolved": "Risolto", "state-rejected": "Rejected", - "no-assignee": "Not Assigned", + "no-assignee": "Non Assegnato", "note-added": "Note Added", - "modal-title": "Report Inappropriate Content", + "modal-title": "Segnala Contenuto Inappropriato", "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-other": "Other (specify below)", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation.", - "modal-submit-confirm": "Confirm Submission", + "modal-reason-offensive": "Offensivo", + "modal-reason-other": "Altro (Specificare di seguito)", + "modal-reason-custom": "Motivazione della segnalazione...", + "modal-submit": "Invia la Segnalazione", + "modal-submit-success": "Il contenuto è stato segnalato.", + "modal-submit-confirm": "Conferma la Segnalazione", "modal-submit-confirm-text": "You have a custom reason specified already. Are you sure you wish to submit via quick-report?", "modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined." } \ No newline at end of file diff --git a/public/language/it/global.json b/public/language/it/global.json index 7325219082..4279338d8d 100644 --- a/public/language/it/global.json +++ b/public/language/it/global.json @@ -104,6 +104,6 @@ "cookies.accept": "Ho capito!", "cookies.learn_more": "Scopri di più", "edited": "Modificato", - "disabled": "Disabled", - "select": "Select" + "disabled": "Disabilitato", + "select": "Seleziona" } \ No newline at end of file diff --git a/public/language/it/groups.json b/public/language/it/groups.json index 835ada672f..53f27c12c4 100644 --- a/public/language/it/groups.json +++ b/public/language/it/groups.json @@ -27,7 +27,7 @@ "details.disableJoinRequests": "Disabilita le richieste d'adesione", "details.grant": "Concedi / Rimuovi la Proprietà", "details.kick": "Espelli", - "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.kick_confirm": "Sei sicuro di voler rimuovere questo membro dal gruppo?", "details.owner_options": "Amministratore del Grupo", "details.group_name": "Nome Gruppo", "details.member_count": "Totale Membri", @@ -53,6 +53,6 @@ "new-group.group_name": "Nome Gruppo:", "upload-group-cover": "Carica copertina del gruppo", "bulk-invite-instructions": "Inserisci una lista di nomi utente da invitare in questo gruppo separati da virgole", - "bulk-invite": "Bulk Invite", + "bulk-invite": "Invita in Massa", "remove_group_cover_confirm": "Sei sicuro di voler rimuovere l'immagine copertina?" } \ No newline at end of file diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json index 5aa5225d1b..338a970c00 100644 --- a/public/language/it/notifications.json +++ b/public/language/it/notifications.json @@ -47,10 +47,10 @@ "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.", "email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.", "email-confirm-sent": "Email di conferma inviata.", - "none": "None", - "notification_only": "Notification Only", - "email_only": "Email Only", - "notification_and_email": "Notification & Email", + "none": "Nessuna Notifica", + "notification_only": "Solo Notifiche", + "email_only": "Solo Email", + "notification_and_email": "Email e Notifica", "notificationType_upvote": "When someone upvotes your post", "notificationType_new-topic": "When someone you follow posts a topic", "notificationType_new-reply": "When a new reply is posted in a topic you are watching", diff --git a/public/language/it/pages.json b/public/language/it/pages.json index 0b1be5dc1e..0be5a17a98 100644 --- a/public/language/it/pages.json +++ b/public/language/it/pages.json @@ -7,7 +7,7 @@ "popular-alltime": "Discussioni più popolari di sempre", "recent": "Discussioni Recenti", "moderator-tools": "Moderator Tools", - "flagged-content": "Flagged Content", + "flagged-content": "Contenuti Segnalati", "ip-blacklist": "Lista nera degli IP", "post-queue": "Post Queue", "users/online": "Utenti Online", @@ -44,7 +44,7 @@ "account/bookmarks": "%1 Post tra i favoriti", "account/settings": "Impostazioni Utente", "account/watched": "Discussioni osservate da %1", - "account/ignored": "Topics ignored by %1", + "account/ignored": "Discussioni ignorate da %1", "account/upvoted": "Post apprezzati da %1", "account/downvoted": "Post votati negativamente da %1", "account/best": "I migliori post di %1", diff --git a/public/language/it/register.json b/public/language/it/register.json index e3afd5aed2..49de769d43 100644 --- a/public/language/it/register.json +++ b/public/language/it/register.json @@ -1,5 +1,5 @@ { - "register": "Registrazione", + "register": "Registrati", "cancel_registration": "Cancella Registrazione", "help.email": "Come opzione predefinita, il tuo indirizzo email non verrà reso pubblico.", "help.username_restrictions": "Un nome utente unico, di almeno %1 caratteri e al massimo di %2. Gli altri utenti ti possono menzionare usando @<span id='yourUsername'>username</span>.", diff --git a/public/language/it/search.json b/public/language/it/search.json index 980e2c28ba..dc33a58737 100644 --- a/public/language/it/search.json +++ b/public/language/it/search.json @@ -12,7 +12,7 @@ "reply-count": "Numero Risposte", "at-least": "Almeno", "at-most": "Al massimo", - "relevance": "Relevance", + "relevance": "Rilevanza", "post-time": "Ora invio", "newer-than": "Più nuovi di", "older-than": "Più vecchi di", diff --git a/public/language/it/success.json b/public/language/it/success.json index cf8dd4cad5..35f6d5b2a9 100644 --- a/public/language/it/success.json +++ b/public/language/it/success.json @@ -1,7 +1,7 @@ { "success": "Riuscito", "topic-post": "Hai postato correttamente.", - "post-queued": "Your post is queued for approval.", + "post-queued": "La tua discussione è in attesa di approvazione.", "authentication-successful": "Autenticazione Riuscita", "settings-saved": "Impostazioni salvate!" } \ No newline at end of file diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 747ba4876b..5b15970770 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -13,9 +13,9 @@ "notify_me": "Ricevi notifiche di nuove risposte in questa discussione", "quote": "Cita", "reply": "Rispondi", - "replies_to_this_post": "%1 Replies", - "one_reply_to_this_post": "1 Reply", - "last_reply_time": "Last reply", + "replies_to_this_post": "%1 Risposte", + "one_reply_to_this_post": "1 Risposta", + "last_reply_time": "Ultima Risposta", "reply-as-topic": "Topic risposta", "guest-login-reply": "Effettua il Log in per rispondere", "edit": "Modifica", @@ -59,7 +59,7 @@ "thread_tools.unlock": "Sblocca Discussione", "thread_tools.move": "Sposta Discussione", "thread_tools.move_all": "Sposta Tutto", - "thread_tools.select_category": "Select Category", + "thread_tools.select_category": "Seleziona Categoria", "thread_tools.fork": "Dividi Discussione", "thread_tools.delete": "Elimina Discussione", "thread_tools.delete-posts": "Cancella post", @@ -68,8 +68,8 @@ "thread_tools.restore_confirm": "Sei sicuro di voler ripristinare questa discussione?", "thread_tools.purge": "Svuota Discussione", "thread_tools.purge_confirm": "Sei sicuro di voler svuotare questa discussione?", - "thread_tools.merge_topics": "Merge Topics", - "thread_tools.merge": "Merge", + "thread_tools.merge_topics": "Unisci le Discussioni", + "thread_tools.merge": "Unisci", "topic_move_success": "Questa discussione è stata correttamente spostata in %1", "post_delete_confirm": "Sei sicuro di voler cancellare questo post?", "post_restore_confirm": "Sei sicuro di voler ripristinare questo post?", @@ -91,7 +91,7 @@ "fork_pid_count": "%1 post selezionati", "fork_success": "Topic Diviso con successo ! Clicca qui per andare al Topic Diviso.", "delete_posts_instruction": "Clicca sui post che vuoi cancellare/eliminare", - "merge_topics_instruction": "Click the topics you want to merge", + "merge_topics_instruction": "Clicca sulle discussioni che vuoi unire", "composer.title_placeholder": "Inserisci qui il titolo della discussione...", "composer.handle_placeholder": "Nome", "composer.discard": "Annulla", diff --git a/public/language/it/unread.json b/public/language/it/unread.json index c1244d0fe3..2f2afebd23 100644 --- a/public/language/it/unread.json +++ b/public/language/it/unread.json @@ -10,6 +10,6 @@ "all-topics": "Tutte le Discussioni", "new-topics": "Nuova Discussione", "watched-topics": "Discussioni seguite", - "unreplied-topics": "Nessuna Risposta", - "multiple-categories-selected": "Multiple Selected" + "unreplied-topics": "Discussioni Senza Risposta", + "multiple-categories-selected": "Più Categorie" } \ No newline at end of file diff --git a/public/language/it/user.json b/public/language/it/user.json index a24f325d24..d3b66bfa4a 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -19,13 +19,13 @@ "location": "Località", "age": "Età", "joined": "Iscrizione", - "lastonline": "Ultima volta in linea", + "lastonline": "Ultimo Accesso", "profile": "Profilo", "profile_views": "Visite al profilo", "reputation": "Reputazione", - "bookmarks": "Favoriti", + "bookmarks": "Preferiti", "watched": "Osservati", - "ignored": "Ignored", + "ignored": "Ignorati", "followers": "Da chi è seguito", "following": "Chi segue", "aboutme": "Su di me", @@ -61,7 +61,7 @@ "username_taken_workaround": "Il nome utente che hai richiesto era già stato utilizzato, quindi lo abbiamo modificato leggermente. Ora il tuo è <strong>%1</strong>", "password_same_as_username": "La tua password è uguale al tuo username, per piacere scegli un'altra password", "password_same_as_email": "La tua password sembra coincidere con la tua email, per favore fornisci un'altra password.", - "weak_password": "Weak password.", + "weak_password": "Password debole.", "upload_picture": "Carica foto", "upload_a_picture": "Carica una foto", "remove_uploaded_picture": "Elimina foto caricata", @@ -85,7 +85,7 @@ "has_no_posts": "Questo utente non ha ancora scritto niente.", "has_no_topics": "Questo utente non ha ancora avviato discussioni.", "has_no_watched_topics": "Questo utente non sta osservando discussioni.", - "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_ignored_topics": "Questo utente non sta ignorando discussioni.", "has_no_upvoted_posts": "Questo utente non ha ancora apprezzato nessun post.", "has_no_downvoted_posts": "Questo utente non ha ancora votato negativamente alcun post", "has_no_voted_posts": "Questo utente non ha post votati", @@ -94,18 +94,18 @@ "paginate_description": "Non utilizzare lo scroll infinito per discussioni e messaggi", "topics_per_page": "Discussioni per Pagina", "posts_per_page": "Post per Pagina", - "max_items_per_page": "Maximum %1", + "max_items_per_page": "Massimo %1", "notification_sounds": "Riproduci un suono quando si riceve una notifica", "notifications_and_sounds": "Notifiche e Suoni", "incoming-message-sound": "Suono messaggio in entrata", "outgoing-message-sound": "Suono messaggio in uscita", "notification-sound": "Suono di notifica", "no-sound": "Nessun suono", - "upvote-notif-freq": "Upvote Notification Frequency", - "upvote-notif-freq.all": "All Upvotes", - "upvote-notif-freq.everyTen": "Every Ten Upvotes", - "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", - "upvote-notif-freq.disabled": "Disabled", + "upvote-notif-freq": "Frequenza Notifiche dei Mi Piace ", + "upvote-notif-freq.all": "Tutti i Mi Piace", + "upvote-notif-freq.everyTen": "Ogni Dieci Mi Piace", + "upvote-notif-freq.logarithmic": "Ogni 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabilitate", "browsing": "Impostazioni di Navigazione", "open_links_in_new_tab": "Apri i link web in una nuova pagina", "enable_topic_searching": "Abilita la ricerca negli argomenti", From a524f9b55ffe6efe027a4c3a610f4053b0d3654e Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" <deploy@nodebb.org> Date: Mon, 1 Jan 2018 09:25:13 +0000 Subject: [PATCH 10/22] Latest translations and fallbacks --- public/language/it/modules.json | 2 +- public/language/it/notifications.json | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/language/it/modules.json b/public/language/it/modules.json index 5fcef5d582..d379688576 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -20,7 +20,7 @@ "chat.three_months": "3 Mesi", "chat.delete_message_confirm": "Sei sicuro di voler eliminare questo messaggio?", "chat.add-users-to-room": "Aggiungi utenti alla stanza", - "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.confirm-chat-with-dnd-user": "Questo utente ha impostato il suo stato su Non Disturbare. Sei sicuro di voler iniziare una conversazione?", "composer.compose": "Componi", "composer.show_preview": "Visualizza Anteprima", "composer.hide_preview": "Nascondi Anteprima", diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json index 338a970c00..7a1b0c4b36 100644 --- a/public/language/it/notifications.json +++ b/public/language/it/notifications.json @@ -29,9 +29,9 @@ "user_flagged_post_in": "<strong>%1</strong> ha segnalato un post in <strong>%2</strong>", "user_flagged_post_in_dual": "<strong>%1</strong> e <strong>%2</strong> hanno segnalato un post in <strong>%3</strong>", "user_flagged_post_in_multiple": "<strong>%1</strong> ed altri %2 hanno segnalato un post in <strong>%3</strong>", - "user_flagged_user": "<strong>%1</strong> flagged a user profile (%2)", - "user_flagged_user_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a user profile (%3)", - "user_flagged_user_multiple": "<strong>%1</strong> and %2 others flagged a user profile (%3)", + "user_flagged_user": "<strong>%1</strong> ha segnalato un utente (%2)", + "user_flagged_user_dual": "<strong>%1</strong> e <strong>%2</strong> hanno segnalato un utente (%3)", + "user_flagged_user_multiple": "<strong>%1</strong> e altri %2 hanno segnalato un utente (%3)", "user_posted_to": "<strong>%1</strong> ha postato una risposta a: <strong>%2</strong>", "user_posted_to_dual": "<strong>%1</strong> e <strong>%2</strong> hanno postato una risposta su: <strong>%3</strong>", "user_posted_to_multiple": "<strong>%1</strong> ed altri %2 hanno postato una risposta su: <strong>%3</strong>", @@ -42,7 +42,7 @@ "new_register": "<strong>%1</strong> ha inviato una richiesta di registrazione.", "new_register_multiple": "Ci sono <strong>%1</strong> richieste di registrazione che attendono di essere esaminate.", "flag_assigned_to_you": "<strong>Flag %1</strong> has been assigned to you", - "post_awaiting_review": "Post awaiting review", + "post_awaiting_review": "Post in attesa di revisione", "email-confirmed": "Email Confermata", "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.", "email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.", @@ -51,14 +51,14 @@ "notification_only": "Solo Notifiche", "email_only": "Solo Email", "notification_and_email": "Email e Notifica", - "notificationType_upvote": "When someone upvotes your post", + "notificationType_upvote": "Quando il tuo post riceve un Mi Piace", "notificationType_new-topic": "When someone you follow posts a topic", "notificationType_new-reply": "When a new reply is posted in a topic you are watching", "notificationType_follow": "When someone starts following you", "notificationType_new-chat": "When you receive a chat message", "notificationType_group-invite": "When you receive a group invite", "notificationType_new-register": "When someone gets added to registration queue", - "notificationType_post-queue": "When a new post is queued", - "notificationType_new-post-flag": "When a post is flagged", - "notificationType_new-user-flag": "When a user is flagged" + "notificationType_post-queue": "Quando un nuovo post è in attesa di revisione", + "notificationType_new-post-flag": "Quando un post viene segnalato", + "notificationType_new-user-flag": "Quando un utente viene segnalato" } \ No newline at end of file From 9b5e0f9e95ca4d44c15ea388bea4abc715bac446 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Tue, 2 Jan 2018 14:45:21 -0500 Subject: [PATCH 11/22] updated upgrade logic to not break ACP restart flow --- src/cli/package-install.js | 2 +- src/cli/upgrade-plugins.js | 2 +- src/cli/upgrade.js | 6 ++---- src/meta/build.js | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/cli/package-install.js b/src/cli/package-install.js index 5f6f9917a5..e923094b05 100644 --- a/src/cli/package-install.js +++ b/src/cli/package-install.js @@ -30,7 +30,7 @@ function updatePackageFile() { exports.updatePackageFile = updatePackageFile; function installAll() { - process.stdout.write('\n'); + process.stdout.write(' started\n'.green); var prod = global.env !== 'development'; var command = 'npm install'; diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js index 3be00cb5d1..a61a711bf7 100644 --- a/src/cli/upgrade-plugins.js +++ b/src/cli/upgrade-plugins.js @@ -212,7 +212,7 @@ function upgradePlugins(callback) { }); } else { console.log('Package upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade -p'.green + '".'.reset); - callback(null, true); + callback(); } }); }); diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js index e5ab2b6c0c..2462f1f168 100644 --- a/src/cli/upgrade.js +++ b/src/cli/upgrade.js @@ -16,6 +16,7 @@ var steps = { handler: function (next) { packageInstall.updatePackageFile(); packageInstall.preserveExtraneousPlugins(); + process.stdout.write(' OK\n'.green); next(); }, }, @@ -54,11 +55,8 @@ function runSteps(tasks) { tasks = tasks.map(function (key, i) { return function (next) { process.stdout.write('\n' + ((i + 1) + '. ').bold + steps[key].message.yellow); - return steps[key].handler(function (err, inhibitOk) { + return steps[key].handler(function (err) { if (err) { return next(err); } - if (!inhibitOk) { - process.stdout.write(' OK'.green + '\n'.reset); - } next(); }); }; diff --git a/src/meta/build.js b/src/meta/build.js index 2beb5f8af9..552c9aa55c 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -212,7 +212,7 @@ function build(targets, callback) { } winston.info('[build] Asset compilation successful. Completed in ' + totalTime + 'sec.'); - callback(null, true); + callback(); }); } From bf1e2cfe463b95ecf3476023708cbcd4003c5d54 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Wed, 3 Jan 2018 12:50:19 -0500 Subject: [PATCH 12/22] bump persona --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index c51992816c..c5b8cab9b4 100644 --- a/install/package.json +++ b/install/package.json @@ -69,7 +69,7 @@ "nodebb-plugin-spam-be-gone": "0.5.1", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "5.0.0", - "nodebb-theme-persona": "7.2.8", + "nodebb-theme-persona": "7.2.9", "nodebb-theme-slick": "1.1.2", "nodebb-theme-vanilla": "8.1.4", "nodebb-widget-essentials": "4.0.1", From ff6c6a54c1f0c18a602b0e734cdbb8b2dd2e133a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Wed, 3 Jan 2018 13:27:30 -0500 Subject: [PATCH 13/22] make upload permissions global give upload image permission to registered users on install add global privileges to app.user.privileges for client side use --- install/package.json | 2 +- src/categories/create.js | 1 - src/controllers/accounts/chats.js | 13 ++-- src/controllers/uploads.js | 7 +-- src/install.js | 4 +- src/middleware/header.js | 5 +- src/privileges.js | 4 -- src/privileges/categories.js | 10 +-- src/privileges/global.js | 62 +++++++++++++++++++ src/privileges/helpers.js | 6 ++ src/upgrades/1.8.0/global_upload_privilege.js | 45 ++++++++++++++ .../admin/partials/categories/privileges.tpl | 4 +- test/categories.js | 34 ++++++++-- test/groups.js | 4 +- test/messaging.js | 9 +-- test/mocks/databasemock.js | 8 +++ test/uploads.js | 17 ++--- 17 files changed, 179 insertions(+), 56 deletions(-) create mode 100644 src/upgrades/1.8.0/global_upload_privilege.js 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 @@ <th class="arrowed" colspan="3"> [[admin/manage/categories:privileges.section-viewing]] </th> - <th class="arrowed" colspan="8"> + <th class="arrowed" colspan="6"> [[admin/manage/categories:privileges.section-posting]] </th> <th class="arrowed" colspan="2"> @@ -61,7 +61,7 @@ <th class="arrowed" colspan="3"> [[admin/manage/categories:privileges.section-viewing]] </th> - <th class="arrowed" colspan="8"> + <th class="arrowed" colspan="6"> [[admin/manage/categories:privileges.section-posting]] </th> <th class="arrowed" colspan="2"> 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); From df182bc7e4cd9fae7d44e1bd5138d7275ed0a49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Wed, 3 Jan 2018 13:43:36 -0500 Subject: [PATCH 14/22] closes #6198 --- public/src/client/account/edit/password.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/account/edit/password.js b/public/src/client/account/edit/password.js index 44723014ce..1585a85577 100644 --- a/public/src/client/account/edit/password.js +++ b/public/src/client/account/edit/password.js @@ -63,7 +63,7 @@ define('forum/account/edit/password', ['forum/account/header', 'translator', 'zx onPasswordConfirmChanged(); var btn = $(this); - if ((passwordvalid && passwordsmatch) || app.user.isAdmin) { + if (passwordvalid && passwordsmatch) { btn.addClass('disabled').find('i').removeClass('hide'); socket.emit('user.changePassword', { currentPassword: currentPassword.val(), From d2ffdbda7ddee4e8313dc49c4027420b6b8e44ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Wed, 3 Jan 2018 13:59:58 -0500 Subject: [PATCH 15/22] closes #6203 --- src/messaging/notifications.js | 1 + src/topics/follow.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 3116c31a2b..f0220f2929 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -76,6 +76,7 @@ module.exports = function (Messaging) { notifications.create({ type: 'new-chat', + subject: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', bodyLong: messageObj.content, nid: 'chat_' + fromuid + '_' + roomId, diff --git a/src/topics/follow.js b/src/topics/follow.js index aad9c0f079..05b98cc95d 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -220,7 +220,7 @@ module.exports = function (Topics) { notifications.create({ type: 'new-reply', - subject: '[' + (meta.config.title || 'NodeBB') + '] ' + title, + subject: title, bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', bodyLong: postData.content, pid: postData.pid, From baa868cf2b6ba0bda3ceb6fa4f52e9fde26b49ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Wed, 3 Jan 2018 14:11:51 -0500 Subject: [PATCH 16/22] fix lint --- src/topics/follow.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/topics/follow.js b/src/topics/follow.js index 05b98cc95d..a590ad2392 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -9,7 +9,6 @@ var notifications = require('../notifications'); var privileges = require('../privileges'); var plugins = require('../plugins'); var utils = require('../utils'); -var meta = require('../meta'); module.exports = function (Topics) { Topics.toggleFollow = function (tid, uid, callback) { From 87d40e9e78496b33f8eee1e5d29b95f9250b9751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Wed, 3 Jan 2018 14:24:07 -0500 Subject: [PATCH 17/22] fix increment to use data --- src/database/mongo/main.js | 5 +++-- test/database/keys.js | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 509f19006b..7e220f5e5c 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -83,8 +83,9 @@ module.exports = function (db, module) { if (!key) { return callback(); } - db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { value: 1 } }, { new: true, upsert: true }, function (err, result) { - callback(err, result && result.value ? result.value.value : null); + db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { data: 1 } }, { new: true, upsert: true }, function (err, result) { + console.log(result) + callback(err, result && result.value ? result.value.data : null); }); }; diff --git a/test/database/keys.js b/test/database/keys.js index 03d607d808..cdd8cc9ab4 100644 --- a/test/database/keys.js +++ b/test/database/keys.js @@ -149,6 +149,21 @@ describe('Key methods', function () { done(); }); }); + + it('should set then increment a key', function (done) { + db.set('myIncrement', 1, function (err) { + assert.ifError(err); + db.increment('myIncrement', function (err, value) { + assert.ifError(err); + assert.equal(value, 2); + db.get('myIncrement', function (err, value) { + assert.ifError(err); + assert.equal(value, 2); + done(); + }); + }); + }); + }); }); describe('rename', function () { From 5146f43b330f110b7f27d9111289d396eed1a674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Wed, 3 Jan 2018 14:38:09 -0500 Subject: [PATCH 18/22] remove console.log --- src/database/mongo/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 7e220f5e5c..b8ceaa4f6f 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -84,7 +84,6 @@ module.exports = function (db, module) { return callback(); } db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { data: 1 } }, { new: true, upsert: true }, function (err, result) { - console.log(result) callback(err, result && result.value ? result.value.data : null); }); }; From 6552ebbd8befa697aa88c3cb8a1825dc033c1be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Wed, 3 Jan 2018 15:06:41 -0500 Subject: [PATCH 19/22] fix redis test for custom home page --- src/controllers/home.js | 81 +++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/src/controllers/home.js b/src/controllers/home.js index 6c67e7aaa2..35a6cfe6a0 100644 --- a/src/controllers/home.js +++ b/src/controllers/home.js @@ -1,61 +1,56 @@ 'use strict'; +var async = require('async'); var plugins = require('../plugins'); var meta = require('../meta'); var user = require('../user'); -var pubsub = require('../pubsub'); -var adminHomePageRoute; -var getRoute; - -function configUpdated() { - adminHomePageRoute = (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories'; - getRoute = parseInt(meta.config.allowUserHomePage, 10) ? getRouteAllowUserHomePage : getRouteDisableUserHomePage; -} - -function getRouteDisableUserHomePage(uid, next) { - next(null, adminHomePageRoute); +function adminHomePageRoute() { + return (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories'; } -function getRouteAllowUserHomePage(uid, next) { - user.getSettings(uid, function (err, settings) { - if (err) { - return next(err); - } - - var route = adminHomePageRoute; - - if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') { - route = settings.homePageRoute || route; - } - - next(null, route); - }); +function getUserHomeRoute(uid, callback) { + async.waterfall([ + function (next) { + user.getSettings(uid, next); + }, + function (settings, next) { + var route = adminHomePageRoute(); + + if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') { + route = settings.homePageRoute || route; + } + + next(null, route); + }, + ], callback); } -pubsub.on('config:update', configUpdated); -configUpdated(); - function rewrite(req, res, next) { if (req.path !== '/' && req.path !== '/api/' && req.path !== '/api') { return next(); } - getRoute(req.uid, function (err, route) { - if (err) { - return next(err); - } - - var hook = 'action:homepage.get:' + route; - - if (!plugins.hasListeners(hook)) { - req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + route; - } else { - res.locals.homePageRoute = route; - } - - next(); - }); + async.waterfall([ + function (next) { + if (parseInt(meta.config.allowUserHomePage, 10)) { + getUserHomeRoute(req.uid, next); + } else { + next(null, adminHomePageRoute()); + } + }, + function (route, next) { + var hook = 'action:homepage.get:' + route; + + if (!plugins.hasListeners(hook)) { + req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + route; + } else { + res.locals.homePageRoute = route; + } + + next(); + }, + ], next); } exports.rewrite = rewrite; From d64b814acb166ac54026bf3b38539c23f21c7a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Thu, 4 Jan 2018 10:48:08 -0500 Subject: [PATCH 20/22] handle https://packages.nodebb.org failures --- src/plugins.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins.js b/src/plugins.js index 653edee5fe..f11ed63494 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -213,8 +213,8 @@ Plugins.list = function (matching, callback) { require('request')(url, { json: true, }, function (err, res, body) { - if (err) { - winston.error('Error parsing plugins', err); + if (err || (res && res.statusCode !== 200)) { + winston.error('Error loading ' + url, err || body); return Plugins.normalise([], callback); } @@ -225,7 +225,7 @@ Plugins.list = function (matching, callback) { Plugins.normalise = function (apiReturn, callback) { var pluginMap = {}; var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies; - apiReturn = apiReturn || []; + apiReturn = Array.isArray(apiReturn) ? apiReturn : []; for (var i = 0; i < apiReturn.length; i += 1) { apiReturn[i].id = apiReturn[i].name; apiReturn[i].installed = false; From 8446a733e5a7855eb1b337516072ef5f24fb7da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Fri, 5 Jan 2018 14:44:18 -0500 Subject: [PATCH 21/22] closes #5569 --- .../en-GB/admin/manage/admins-mods.json | 10 ++ public/language/en-GB/admin/manage/users.json | 12 +- public/language/en-GB/admin/menu.json | 1 + public/less/admin/admin.less | 1 + public/less/admin/manage/admins-mods.less | 27 ++++ public/src/admin/manage/admins-mods.js | 142 ++++++++++++++++++ public/src/admin/manage/users.js | 30 ---- public/src/modules/autocomplete.js | 5 + src/categories.js | 7 +- src/categories/data.js | 6 +- src/controllers/admin.js | 1 + src/controllers/admin/admins-mods.js | 50 ++++++ src/routes/admin.js | 2 + src/views/admin/manage/admins-mods.tpl | 64 ++++++++ src/views/admin/manage/privileges.tpl | 7 - src/views/admin/manage/users.tpl | 3 - src/views/admin/partials/menu.tpl | 2 + test/controllers-admin.js | 8 + 18 files changed, 331 insertions(+), 47 deletions(-) create mode 100644 public/language/en-GB/admin/manage/admins-mods.json create mode 100644 public/less/admin/manage/admins-mods.less create mode 100644 public/src/admin/manage/admins-mods.js create mode 100644 src/controllers/admin/admins-mods.js create mode 100644 src/views/admin/manage/admins-mods.tpl diff --git a/public/language/en-GB/admin/manage/admins-mods.json b/public/language/en-GB/admin/manage/admins-mods.json new file mode 100644 index 0000000000..e0f39ed5d4 --- /dev/null +++ b/public/language/en-GB/admin/manage/admins-mods.json @@ -0,0 +1,10 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "no-global-moderators": "No Global Moderators", + "moderators-of-category": "%1 Moderators", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/users.json b/public/language/en-GB/admin/manage/users.json index 5b68fcdc91..9dcc1a0f32 100644 --- a/public/language/en-GB/admin/manage/users.json +++ b/public/language/en-GB/admin/manage/users.json @@ -71,9 +71,15 @@ "alerts.lockout-reset-success": "Lockout(s) reset!", "alerts.flag-reset-success": "Flags(s) reset!", "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", - "alerts.make-admin-success": "User(s) are now administrators.", - "alerts.confirm-remove-admin": "Do you really want to remove admins?", - "alerts.remove-admin-success": "User(s) are no longer administrators.", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global noderator", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", "alerts.validate-email-success": "Emails validated", "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index 5c60dc440e..8f44bcd304 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -12,6 +12,7 @@ "manage/privileges": "Privileges", "manage/tags": "Tags", "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", "manage/registration": "Registration Queue", "manage/post-queue": "Post Queue", "manage/groups": "Groups", diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index 124be23b5c..d7cd7e6ae4 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -14,6 +14,7 @@ @import "./manage/groups"; @import "./manage/registration"; @import "./manage/users"; +@import "./manage/admins-mods"; @import "./appearance/customise"; @import "./appearance/themes"; @import "./extend/plugins"; diff --git a/public/less/admin/manage/admins-mods.less b/public/less/admin/manage/admins-mods.less new file mode 100644 index 0000000000..0bf7ac46cd --- /dev/null +++ b/public/less/admin/manage/admins-mods.less @@ -0,0 +1,27 @@ +.admins-mods { + .user-card { + margin-right: 10px; + padding: 2px; + } + + .remove-user-icon { + margin-right: 5px; + margin-left: 5px; + } + + .category-depth-1 { + margin-left: 30px; + } + .category-depth-2 { + margin-left: 60px; + } + .category-depth-3 { + margin-left: 90px; + } + .category-depth-4 { + margin-left: 120px; + } + .category-depth-5 { + margin-left: 150px; + } +} \ No newline at end of file diff --git a/public/src/admin/manage/admins-mods.js b/public/src/admin/manage/admins-mods.js new file mode 100644 index 0000000000..9f642f1456 --- /dev/null +++ b/public/src/admin/manage/admins-mods.js @@ -0,0 +1,142 @@ +'use strict'; + +define('admin/manage/admins-mods', ['translator', 'benchpress', 'autocomplete'], function (translator, Benchpress, autocomplete) { + var AdminsMods = {}; + + AdminsMods.init = function () { + autocomplete.user($('#admin-search'), function (ev, ui) { + socket.emit('admin.user.makeAdmins', [ui.item.user.uid], function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.make-admin-success]]'); + $('#admin-search').val(''); + + if ($('.administrator-area [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'admins.members', { admins: { members: [ui.item.user] } }, function (html) { + $('.administrator-area').prepend(html); + }); + }); + }); + + $('.administrator-area').on('click', '.remove-user-icon', function () { + var userCard = $(this).parents('[data-uid]'); + var uid = userCard.attr('data-uid'); + if (parseInt(uid, 10) === parseInt(app.user.uid, 10)) { + return app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]'); + } + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-admin]]', function (confirm) { + if (confirm) { + socket.emit('admin.user.removeAdmins', [uid], function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.remove-admin-success]]'); + userCard.remove(); + }); + } + }); + }); + + autocomplete.user($('#global-mod-search'), function (ev, ui) { + socket.emit('admin.groups.join', { + groupName: 'Global Moderators', + uid: ui.item.user.uid, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.make-global-mod-success]]'); + $('#global-mod-search').val(''); + + if ($('.global-moderator-area [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'globalMods.members', { globalMods: { members: [ui.item.user] } }, function (html) { + $('.global-moderator-area').prepend(html); + $('#no-global-mods-warning').addClass('hidden'); + }); + }); + }); + + $('.global-moderator-area').on('click', '.remove-user-icon', function () { + var userCard = $(this).parents('[data-uid]'); + var uid = userCard.attr('data-uid'); + + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-global-mod]]', function (confirm) { + if (confirm) { + socket.emit('admin.groups.leave', { uid: uid, groupName: 'Global Moderators' }, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.remove-global-mod-success]]'); + userCard.remove(); + if (!$('.global-moderator-area').children().length) { + $('#no-global-mods-warning').removeClass('hidden'); + } + }); + } + }); + }); + + + autocomplete.user($('.moderator-search'), function (ev, ui) { + var input = $(ev.target); + var cid = $(ev.target).attr('data-cid'); + socket.emit('admin.categories.setPrivilege', { + cid: cid, + privilege: ['moderate'], + set: true, + member: ui.item.user.uid, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.make-moderator-success]]'); + input.val(''); + + if ($('.moderator-area[data-cid="' + cid + '"] [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'globalMods', { globalMods: [ui.item.user] }, function (html) { + $('.moderator-area[data-cid="' + cid + '"]').prepend(html); + $('.no-moderator-warning[data-cid="' + cid + '"]').addClass('hidden'); + }); + }); + }); + + $('.moderator-area').on('click', '.remove-user-icon', function () { + var moderatorArea = $(this).parents('[data-cid]'); + var cid = moderatorArea.attr('data-cid'); + var userCard = $(this).parents('[data-uid]'); + var uid = userCard.attr('data-uid'); + + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-moderator]]', function (confirm) { + if (confirm) { + socket.emit('admin.categories.setPrivilege', { + cid: cid, + privilege: ['moderate'], + set: false, + member: uid, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.remove-moderator-success]]'); + userCard.remove(); + if (!moderatorArea.children().length) { + $('.no-moderator-warning[data-cid="' + cid + '"]').removeClass('hidden'); + } + }); + } + }); + }); + }; + + return AdminsMods; +}); diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index b6c7b7aa03..96da2ef057 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -127,36 +127,6 @@ define('admin/manage/users', ['translator', 'benchpress'], function (translator, socket.emit('admin.user.resetLockouts', uids, done('[[admin/manage/users:alerts.lockout-reset-success]]')); }); - $('.admin-user').on('click', function () { - var uids = getSelectedUids(); - if (!uids.length) { - return; - } - - if (uids.indexOf(app.user.uid.toString()) !== -1) { - app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]'); - } else { - socket.emit('admin.user.makeAdmins', uids, done('[[admin/manage/users:alerts.make-admin-success]]', '.administrator', true)); - } - }); - - $('.remove-admin-user').on('click', function () { - var uids = getSelectedUids(); - if (!uids.length) { - return; - } - - if (uids.indexOf(app.user.uid.toString()) !== -1) { - app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]'); - } else { - bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-admin]]', function (confirm) { - if (confirm) { - socket.emit('admin.user.removeAdmins', uids, done('[[admin/manage/users:alerts.remove-admin-success]]', '.administrator', false)); - } - }); - } - }); - $('.validate-email').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js index 68cabdb45b..6c32cb36f2 100644 --- a/public/src/modules/autocomplete.js +++ b/public/src/modules/autocomplete.js @@ -29,6 +29,11 @@ define('autocomplete', function () { uid: user.uid, name: user.username, slug: user.userslug, + username: user.username, + userslug: user.userslug, + picture: user.picture, + 'icon:text': user['icon:text'], + 'icon:bgColor': user['icon:bgColor'], }, }; }); diff --git a/src/categories.js b/src/categories.js index 6013091050..452762ae38 100644 --- a/src/categories.js +++ b/src/categories.js @@ -338,7 +338,7 @@ Categories.buildForSelect = function (uid, privilege, callback) { }; Categories.buildForSelectCategories = function (categories, callback) { - function recursive(category, categoriesData, level) { + function recursive(category, categoriesData, level, depth) { if (category.link) { return; } @@ -347,10 +347,11 @@ Categories.buildForSelectCategories = function (categories, callback) { category.value = category.cid; category.level = level; category.text = level + bullet + category.name; + category.depth = depth; categoriesData.push(category); category.children.forEach(function (child) { - recursive(child, categoriesData, ' ' + level); + recursive(child, categoriesData, ' ' + level, depth + 1); }); } @@ -361,7 +362,7 @@ Categories.buildForSelectCategories = function (categories, callback) { }); categories.forEach(function (category) { - recursive(category, categoriesData, ''); + recursive(category, categoriesData, '', 0); }); callback(null, categoriesData); }; diff --git a/src/categories/data.js b/src/categories/data.js index 73c9300902..a1a9d5c587 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -31,7 +31,11 @@ module.exports = function (Categories) { category.name = validator.escape(String(category.name || '')); category.disabled = category.hasOwnProperty('disabled') ? parseInt(category.disabled, 10) === 1 : undefined; category.isSection = category.hasOwnProperty('isSection') ? parseInt(category.isSection, 10) === 1 : undefined; - category.icon = category.icon || 'hidden'; + + if (category.hasOwnProperty('icon')) { + category.icon = category.icon || 'hidden'; + } + if (category.hasOwnProperty('post_count')) { category.post_count = category.post_count || 0; category.totalPostCount = category.post_count; diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 136eed1267..dc3b6862ae 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -4,6 +4,7 @@ var adminController = { dashboard: require('./admin/dashboard'), categories: require('./admin/categories'), privileges: require('./admin/privileges'), + adminsMods: require('./admin/admins-mods'), tags: require('./admin/tags'), postQueue: require('./admin/postqueue'), blacklist: require('./admin/blacklist'), diff --git a/src/controllers/admin/admins-mods.js b/src/controllers/admin/admins-mods.js new file mode 100644 index 0000000000..97d5828c16 --- /dev/null +++ b/src/controllers/admin/admins-mods.js @@ -0,0 +1,50 @@ +'use strict'; + +var async = require('async'); + +var groups = require('../../groups'); +var categories = require('../../categories'); + +var AdminsMods = module.exports; + +AdminsMods.get = function (req, res, next) { + async.waterfall([ + function (next) { + async.parallel({ + admins: function (next) { + groups.get('administrators', { uid: req.uid }, next); + }, + globalMods: function (next) { + groups.get('Global Moderators', { uid: req.uid }, next); + }, + categories: function (next) { + getModeratorsOfCategories(req.uid, next); + }, + }, next); + }, + function (results) { + res.render('admin/manage/admins-mods', results); + }, + ], next); +}; + +function getModeratorsOfCategories(uid, callback) { + async.waterfall([ + function (next) { + categories.buildForSelect(uid, 'find', next); + }, + function (categoryData, next) { + async.map(categoryData, function (category, next) { + async.waterfall([ + function (next) { + categories.getModerators(category.cid, next); + }, + function (moderators, next) { + category.moderators = moderators; + next(null, category); + }, + ], next); + }, next); + }, + ], callback); +} diff --git a/src/routes/admin.js b/src/routes/admin.js index ae90f4716b..db0ce7798c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -72,6 +72,8 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/users/banned', middlewares, controllers.admin.users.banned); router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue); + router.get('/manage/admins-mods', middlewares, controllers.admin.adminsMods.get); + router.get('/manage/groups', middlewares, controllers.admin.groups.list); router.get('/manage/groups/:name', middlewares, controllers.admin.groups.get); diff --git a/src/views/admin/manage/admins-mods.tpl b/src/views/admin/manage/admins-mods.tpl new file mode 100644 index 0000000000..76d3982302 --- /dev/null +++ b/src/views/admin/manage/admins-mods.tpl @@ -0,0 +1,64 @@ +<div class="admins-mods"> + <h4><!-- IF admins.icon --><i class="fa {admins.icon}"></i> <!-- ENDIF admins.icon -->[[admin/manage/admins-mods:administrators]]</h4> + <div class="administrator-area"> + <!-- BEGIN admins.members --> + <div class="user-card pull-left" data-uid="{admins.members.uid}"> + <!-- IF admins.members.picture --> + <img class="avatar avatar-sm" src="{admins.members.picture}" /> + <!-- ELSE --> + <div class="avatar avatar-sm" style="background-color: {admins.members.icon:bgColor};">{admins.members.icon:text}</div> + <!-- ENDIF admins.members.picture --> + <a href="{config.relative_path}/user/{admins.members.userslug}">{admins.members.username}</a> + <i class="remove-user-icon fa fa-times" role="button"></i> + </div> + <!-- END admins.members --> + </div> + <input id="admin-search" class="form-control" placeholder="[[admin/manage/admins-mods:add-administrator]]" /> + + <br/> + + <h4><!-- IF globalMods.icon --><i class="fa {globalMods.icon}"></i> <!-- ENDIF globalMods.icon -->[[admin/manage/admins-mods:global-moderators]]</h4> + <div class="global-moderator-area"> + <!-- BEGIN globalMods.members --> + <div class="user-card pull-left" data-uid="{globalMods.members.uid}"> + <!-- IF globalMods.members.picture --> + <img class="avatar avatar-sm" src="{globalMods.members.picture}" /> + <!-- ELSE --> + <div class="avatar avatar-sm" style="background-color: {globalMods.members.icon:bgColor};">{globalMods.members.icon:text}</div> + <!-- ENDIF globalMods.members.picture --> + <a href="{config.relative_path}/user/{globalMods.members.userslug}">{globalMods.members.username}</a> + <i class="remove-user-icon fa fa-times" role="button"></i> + </div> + <!-- END globalMods.members --> + </div> + + <div id="no-global-mods-warning" class="<!-- IF globalMods.members.length -->hidden<!-- ENDIF globalMods.members.length -->">[[admin/manage/admins-mods:no-global-moderators]]</div> + + <input id="global-mod-search" class="form-control" placeholder="[[admin/manage/admins-mods:add-global-moderator]]" /> + + <br/> + + <!-- BEGIN categories --> + <div class="categories category-wrapper category-depth-{categories.depth}"> + <h4><!-- IF categories.icon --><i class="fa {categories.icon}"></i> <!-- ENDIF categories.icon -->[[admin/manage/admins-mods:moderators-of-category, {categories.name}]]</h4> + <div class="moderator-area" data-cid="{categories.cid}"> + <!-- BEGIN categories.moderators --> + <div class="user-card pull-left" data-uid="{categories.moderators.uid}"> + <!-- IF categories.moderators.picture --> + <img class="avatar avatar-sm" src="{categories.moderators.picture}" /> + <!-- ELSE --> + <div class="avatar avatar-sm" style="background-color: {categories.moderators.icon:bgColor};">{categories.moderators.icon:text}</div> + <!-- ENDIF categories.moderators.picture --> + <a href="{config.relative_path}/user/{categories.moderators.userslug}">{categories.moderators.username}</a> + <i class="remove-user-icon fa fa-times" role="button"></i> + </div> + <!-- END categories.moderators --> + </div> + + <div data-cid="{categories.cid}" class="no-moderator-warning <!-- IF categories.moderators.length -->hidden<!-- ENDIF categories.moderators.length -->">[[admin/manage/admins-mods:no-moderators]]</div> + + <input data-cid="{categories.cid}" class="form-control moderator-search" placeholder="[[admin/manage/admins-mods:add-moderator]]" /> + </div> + <br/> + <!-- END categories --> +</div> diff --git a/src/views/admin/manage/privileges.tpl b/src/views/admin/manage/privileges.tpl index 8568767d0d..42d8be735e 100644 --- a/src/views/admin/manage/privileges.tpl +++ b/src/views/admin/manage/privileges.tpl @@ -18,9 +18,6 @@ <p> [[admin/manage/categories:privileges.description]] </p> - <p class="text-warning"> - [[admin/manage/categories:privileges.warning]] - </p> <hr /> <div class="privilege-table-container"> <!-- IF cid --> @@ -32,7 +29,3 @@ </div> </form> </div> - -<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored"> - <i class="material-icons">save</i> -</button> diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index 3eca4f998d..c52cccbe63 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -8,9 +8,6 @@ <div class="btn-group pull-right"> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button> <ul class="dropdown-menu"> - <li><a href="#" class="admin-user"><i class="fa fa-fw fa-shield"></i> [[admin/manage/users:make-admin]]</a></li> - <li><a href="#" class="remove-admin-user"><i class="fa fa-fw fa-ban"></i> [[admin/manage/users:remove-admin]]</a></li> - <li class="divider"></li> <li><a href="#" class="validate-email"><i class="fa fa-fw fa-check"></i> [[admin/manage/users:validate-email]]</a></li> <li><a href="#" class="send-validation-email"><i class="fa fa-fw fa-mail-forward"></i> [[admin/manage/users:send-validation-email]]</a></li> <li><a href="#" class="password-reset-email"><i class="fa fa-fw fa-key"></i> [[admin/manage/users:password-reset-email]]</a></li> diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 027071ca32..87d9bfeecb 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -17,6 +17,7 @@ <li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li> <li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li> <li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li> + <li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li> <li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li> <li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li> <li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li> @@ -191,6 +192,7 @@ <li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li> <li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li> <li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li> + <li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li> <li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li> <li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li> <li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li> diff --git a/test/controllers-admin.js b/test/controllers-admin.js index 7639a0df4f..de746fcc74 100644 --- a/test/controllers-admin.js +++ b/test/controllers-admin.js @@ -255,6 +255,14 @@ describe('Admin Controllers', function () { }); }); + it('should load /admin/manage/admins-mods', function (done) { + request(nconf.get('url') + '/api/admin/manage/admins-mods', { jar: jar, json: true }, function (err, res, body) { + assert.ifError(err); + assert(body); + done(); + }); + }); + it('should return 403 if no referer', function (done) { request(nconf.get('url') + '/api/admin/users/csv', { jar: jar }, function (err, res, body) { assert.ifError(err); From 9922720dd359402b3a4a2fb88a2dc98ff06c86c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Sat, 6 Jan 2018 11:58:48 -0500 Subject: [PATCH 22/22] closes #6209 --- public/src/client/chats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index f82623fd5d..2da1735a36 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -324,7 +324,7 @@ define('forum/chats', [ ajaxify.data = payload; Chats.setActive(); Chats.addEventListeners(); - + messages.scrollToBottom($('.expanded-chat ul')); if (history.pushState) { history.pushState({ url: 'user/' + payload.userslug + '/chats/' + payload.roomId,