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>&lt;head&gt;</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>&lt;head&gt;</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>&lt;head&gt;</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, '&nbsp;&nbsp;&nbsp;&nbsp;' + level);
+			recursive(child, categoriesData, '&nbsp;&nbsp;&nbsp;&nbsp;' + 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,