From d16667a5fb8f5bb9458c8e94dfb60cdb50a27011 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: Mon, 5 Feb 2018 10:04:19 -0500
Subject: [PATCH 01/42] closes #6304

---
 public/src/client/account/settings.js | 2 +-
 src/middleware/header.js              | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js
index bffefc725f..2acd578dd9 100644
--- a/public/src/client/account/settings.js
+++ b/public/src/client/account/settings.js
@@ -55,7 +55,7 @@ define('forum/account/settings', ['forum/account/header', 'components', 'sounds'
 			if (skinName === 'default') {
 				skinName = config.defaultBootswatchSkin;
 			}
-			var cssSource = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + skinName + '/bootstrap.min.css';
+			var cssSource = '//maxcdn.bootstrapcdn.com/bootswatch/3.3.7/' + skinName + '/bootstrap.min.css';
 			if (css.length) {
 				css.attr('href', cssSource);
 			} else {
diff --git a/src/middleware/header.js b/src/middleware/header.js
index a0cf65d396..0d2b916965 100644
--- a/src/middleware/header.js
+++ b/src/middleware/header.js
@@ -283,7 +283,7 @@ module.exports = function (middleware) {
 			}
 
 			if (skinToUse) {
-				obj.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + skinToUse + '/bootstrap.min.css';
+				obj.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/3.3.7/' + skinToUse + '/bootstrap.min.css';
 			}
 		}
 	}

From 6cbd70d5107bb20f43a62ae4dcfa53313a8bdeaf 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: Mon, 5 Feb 2018 10:35:23 -0500
Subject: [PATCH 02/42] closes #6307

---
 public/src/modules/notifications.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js
index d9f29cebcd..92574f3eb1 100644
--- a/public/src/modules/notifications.js
+++ b/public/src/modules/notifications.js
@@ -77,7 +77,7 @@ define('notifications', ['sounds', 'translator', 'components', 'navigator', 'ben
 				payload.message = notifData.bodyShort;
 				payload.type = 'info';
 				payload.clickfn = function () {
-					if (notifData.path.startsWith('http') && notifData.path.startsWith('https')) {
+					if (notifData.path.startsWith('http') || notifData.path.startsWith('https')) {
 						window.location.href = notifData.path;
 					} else {
 						window.location.href = window.location.protocol + '//' + window.location.host + config.relative_path + notifData.path;

From 8c5aa740cab65a269856ab7b7f1224504e5cb53b Mon Sep 17 00:00:00 2001
From: "Misty (Bot)" <deploy@nodebb.org>
Date: Tue, 6 Feb 2018 09:25:04 +0000
Subject: [PATCH 03/42] Latest translations and fallbacks

---
 .../language/fa-IR/admin/advanced/errors.json  | 18 +++++++++---------
 .../language/fa-IR/admin/advanced/events.json  |  8 ++++----
 public/language/fa-IR/admin/advanced/logs.json | 10 +++++-----
 .../fa-IR/admin/appearance/customise.json      | 14 +++++++-------
 4 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/public/language/fa-IR/admin/advanced/errors.json b/public/language/fa-IR/admin/advanced/errors.json
index 546f0f1508..de75745bf6 100644
--- a/public/language/fa-IR/admin/advanced/errors.json
+++ b/public/language/fa-IR/admin/advanced/errors.json
@@ -1,14 +1,14 @@
 {
 	"figure-x": "Figure %1",
 	"error-events-per-day": "<code>%1</code> events per day",
-	"error.404": "404 Not Found",
-	"error.503": "503 Service Unavailable",
-	"manage-error-log": "Manage Error Log",
+	"error.404": "ارور 404 یافت نشد",
+	"error.503": "ارور 503 سرویس دردسترس نیست",
+	"manage-error-log": "مدیریت Error Log",
 	"export-error-log": "Export Error Log (CSV)",
-	"clear-error-log": "Clear Error Log",
-	"route": "Route",
-	"count": "Count",
-	"no-routes-not-found": "Hooray! No 404 errors!",
-	"clear404-confirm": "Are you sure you wish to clear the 404 error logs?",
-	"clear404-success": "\"404 Not Found\" errors cleared"
+	"clear-error-log": "پاک کردن Error Log",
+	"route": "مسیر",
+	"count": "شمارش",
+	"no-routes-not-found": "ایول! بدون ارور 404 !",
+	"clear404-confirm": "آیا از پاک کردن ارور های 404 اطمینان دارید؟",
+	"clear404-success": "ارور های 404 پاک شدند"
 }
\ No newline at end of file
diff --git a/public/language/fa-IR/admin/advanced/events.json b/public/language/fa-IR/admin/advanced/events.json
index 766eb5e951..37672285ce 100644
--- a/public/language/fa-IR/admin/advanced/events.json
+++ b/public/language/fa-IR/admin/advanced/events.json
@@ -1,6 +1,6 @@
 {
-	"events": "Events",
-	"no-events": "There are no events",
-	"control-panel": "Events Control Panel",
-	"delete-events": "Delete Events"
+	"events": "رویداد ها",
+	"no-events": "رویدادی موجود نیست",
+	"control-panel": "کنترل پنل رویداد ها",
+	"delete-events": "پاک کردن رویداد ها"
 }
\ No newline at end of file
diff --git a/public/language/fa-IR/admin/advanced/logs.json b/public/language/fa-IR/admin/advanced/logs.json
index b9de400e1c..37846be559 100644
--- a/public/language/fa-IR/admin/advanced/logs.json
+++ b/public/language/fa-IR/admin/advanced/logs.json
@@ -1,7 +1,7 @@
 {
-	"logs": "Logs",
-	"control-panel": "Logs Control Panel",
-	"reload": "Reload Logs",
-	"clear": "Clear Logs",
-	"clear-success": "Logs Cleared!"
+	"logs": "گزارشات",
+	"control-panel": "کنترل پنل گزارشات",
+	"reload": "بارگزاری مجدد گزارش ها",
+	"clear": "حذف گزارشات",
+	"clear-success": "گزارش ها پاک شدند"
 }
\ No newline at end of file
diff --git a/public/language/fa-IR/admin/appearance/customise.json b/public/language/fa-IR/admin/appearance/customise.json
index 56c11a2805..39aab64979 100644
--- a/public/language/fa-IR/admin/appearance/customise.json
+++ b/public/language/fa-IR/admin/appearance/customise.json
@@ -1,13 +1,13 @@
 {
-	"custom-css": "Custom CSS/LESS",
-	"custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.",
-	"custom-css.enable": "Enable Custom CSS/LESS",
+	"custom-css": "سفارشی کردن CSS/LESS",
+	"custom-css.description": "کد های CSS/LESS خود را در این قسمت وارد کنید . بعد از همه ی استایل های دیگر اعمال میشود",
+	"custom-css.enable": "به کار گرفتن CSS/LESS سفارشی",
 
-	"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": "جائا اسکریپت سفارشی",
+	"custom-js.description": "کد های جاوا اسکریپت خود را در این قسمت وارد کنید بعد از لود شدن تمام صفحه اجرا خواهند شد",
+	"custom-js.enable": "به کارگیری جاوا اسکریپت سفارشی ",
 
-	"custom-header": "Custom Header",
+	"custom-header": "هدر سفارشی",
 	"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.enable": "Enable Custom Header",
 

From 3551a3413861467be1e1acfa147aac3e332ea3f2 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: Tue, 6 Feb 2018 12:54:56 -0500
Subject: [PATCH 04/42] up spam be gone

---
 install/package.json |  2 +-
 src/topics/create.js | 11 +++++------
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/install/package.json b/install/package.json
index 36d6add398..22585bf839 100644
--- a/install/package.json
+++ b/install/package.json
@@ -66,7 +66,7 @@
         "nodebb-plugin-markdown": "8.3.0",
         "nodebb-plugin-mentions": "2.2.3",
         "nodebb-plugin-soundpack-default": "1.0.0",
-        "nodebb-plugin-spam-be-gone": "0.5.1",
+        "nodebb-plugin-spam-be-gone": "0.5.2",
         "nodebb-rewards-essentials": "0.0.11",
         "nodebb-theme-lavender": "5.0.1",
         "nodebb-theme-persona": "7.2.20",
diff --git a/src/topics/create.js b/src/topics/create.js
index bc1091bf52..d8108beb70 100644
--- a/src/topics/create.js
+++ b/src/topics/create.js
@@ -209,18 +209,17 @@ module.exports = function (Topics) {
 		var uid = data.uid;
 		var content = data.content;
 		var postData;
-		var cid;
 
 		async.waterfall([
 			function (next) {
 				Topics.getTopicField(tid, 'cid', next);
 			},
-			function (_cid, next) {
-				cid = _cid;
+			function (cid, next) {
+				data.cid = cid;
 				async.parallel({
 					topicData: async.apply(Topics.getTopicData, tid),
 					canReply: async.apply(privileges.topics.can, 'topics:reply', tid, uid),
-					isAdminOrMod: async.apply(privileges.categories.isAdminOrMod, cid, uid),
+					isAdminOrMod: async.apply(privileges.categories.isAdminOrMod, data.cid, uid),
 				}, next);
 			},
 			function (results, next) {
@@ -243,7 +242,7 @@ module.exports = function (Topics) {
 				guestHandleValid(data, next);
 			},
 			function (next) {
-				user.isReadyToPost(uid, cid, next);
+				user.isReadyToPost(uid, data.cid, next);
 			},
 			function (next) {
 				plugins.fireHook('filter:topic.reply', data, next);
@@ -284,7 +283,7 @@ module.exports = function (Topics) {
 				}
 
 				Topics.notifyFollowers(postData, uid);
-				analytics.increment(['posts', 'posts:byCid:' + cid]);
+				analytics.increment(['posts', 'posts:byCid:' + data.cid]);
 				plugins.fireHook('action:topic.reply', { post: _.clone(postData) });
 
 				next(null, postData);

From 6c5e99171e7603bc410fd1010e98bbc8e607f25c 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: Tue, 6 Feb 2018 14:34:49 -0500
Subject: [PATCH 05/42] closes #6309

---
 src/upgrade.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/upgrade.js b/src/upgrade.js
index f30a5f43d4..89541ec9f2 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -33,7 +33,13 @@ Upgrade.getAll = function (callback) {
 				versionA = path.dirname(a).split('/').pop();
 				versionB = path.dirname(b).split('/').pop();
 
-				return semver.compare(versionA, versionB);
+				var semverCompare = semver.compare(versionA, versionB);
+				if (semverCompare) {
+					return semverCompare;
+				}
+				var timestampA = require(a).timestamp;
+				var timestampB = require(b).timestamp;
+				return timestampA - timestampB;
 			}));
 		},
 		async.apply(Upgrade.appendPluginScripts),

From 58f5bb35fc5c14e146329570b5a4eb3d6a4017c5 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: Tue, 6 Feb 2018 16:49:54 -0500
Subject: [PATCH 06/42] show error

---
 src/cli/upgrade.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js
index b9cd46e4a2..befd627daf 100644
--- a/src/cli/upgrade.js
+++ b/src/cli/upgrade.js
@@ -65,7 +65,7 @@ function runSteps(tasks) {
 
 	async.series(tasks, function (err) {
 		if (err) {
-			console.error('Error occurred during upgrade');
+			console.error('Error occurred during upgrade: ' + err.stack);
 			throw err;
 		}
 

From 5f663b580c3b0db56967e166f7e8ce236180b1b1 Mon Sep 17 00:00:00 2001
From: "Misty (Bot)" <deploy@nodebb.org>
Date: Wed, 7 Feb 2018 09:25:05 +0000
Subject: [PATCH 07/42] Latest translations and fallbacks

---
 public/language/fa-IR/notifications.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/language/fa-IR/notifications.json b/public/language/fa-IR/notifications.json
index b80e40669c..c0b76b8c80 100644
--- a/public/language/fa-IR/notifications.json
+++ b/public/language/fa-IR/notifications.json
@@ -52,7 +52,7 @@
     "email_only": "فقط ایمیل",
     "notification_and_email": "اعلان و ایمیل",
     "notificationType_upvote": "هنگامی که شخصی به پست شما رای مثبت می دهد",
-    "notificationType_new-topic": "هنگامی که شخصی که شما فالو می کنید موضوعی ایجاد نماید",
+    "notificationType_new-topic": "هنگامی که شخصی که شما دنبال می کنید موضوعی ایجاد نماید",
     "notificationType_new-reply": "هنگامی که پاسخ جدید در تاپیکی که شما پیگیری می کنید فرستاده می شود",
     "notificationType_follow": "هنگامی که کسی شما را دنبال می کند",
     "notificationType_new-chat": "هنگامی که شما پیام چتی دریافت می کنید",

From ef4de68f5bc4c8966ac6e4c978517cd3bafb4115 Mon Sep 17 00:00:00 2001
From: Baris Usakli <barisusakli@gmail.com>
Date: Wed, 7 Feb 2018 12:30:03 -0500
Subject: [PATCH 08/42] closes #6312

---
 public/src/client/topic/merge.js | 26 ++++++++++++++------------
 1 file changed, 14 insertions(+), 12 deletions(-)

diff --git a/public/src/client/topic/merge.js b/public/src/client/topic/merge.js
index 473f76583c..92e7e86594 100644
--- a/public/src/client/topic/merge.js
+++ b/public/src/client/topic/merge.js
@@ -10,9 +10,6 @@ define('forum/topic/merge', function () {
 
 	Merge.init = function () {
 		$('.category').on('click', '[component="topic/merge"]', onMergeTopicsClicked);
-		if (modal) {
-			$('[component="category/topic"]').on('click', 'a', onTopicClicked);
-		}
 	};
 
 	function onMergeTopicsClicked() {
@@ -28,7 +25,7 @@ define('forum/topic/merge', function () {
 
 			modal.find('.close,#merge_topics_cancel').on('click', closeModal);
 
-			$('[component="category/topic"]').on('click', 'a', onTopicClicked);
+			$('[component="category"]').on('click', '[component="category/topic"] a', onTopicClicked);
 
 			showTopicsSelected();
 
@@ -41,14 +38,19 @@ define('forum/topic/merge', function () {
 	function onTopicClicked(ev) {
 		var tid = $(this).parents('[component="category/topic"]').attr('data-tid');
 		var index = $(this).parents('[component="category/topic"]').attr('data-index');
-		var title = ajaxify.data.topics[index] ? ajaxify.data.topics[index].title : 'No title';
-		if (selectedTids[tid]) {
-			delete selectedTids[tid];
-		} else {
-			selectedTids[tid] = title;
-		}
-		checkButtonEnable();
-		showTopicsSelected();
+		socket.emit('topics.getTopic', tid, function (err, topicData) {
+			if (err) {
+				return app.alertError(err);
+			}
+			var title = topicData ? topicData.title : 'No title';
+			if (selectedTids[tid]) {
+				delete selectedTids[tid];
+			} else {
+				selectedTids[tid] = title;
+			}
+			checkButtonEnable();
+			showTopicsSelected();
+		});
 		ev.preventDefault();
 		ev.stopPropagation();
 		return false;

From 3340db9636ec4c903c3095c75137a2dd4f335e53 Mon Sep 17 00:00:00 2001
From: Baris Usakli <barisusakli@gmail.com>
Date: Wed, 7 Feb 2018 12:56:08 -0500
Subject: [PATCH 09/42] remove unused var

---
 public/src/client/topic/merge.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/public/src/client/topic/merge.js b/public/src/client/topic/merge.js
index 92e7e86594..510acf6765 100644
--- a/public/src/client/topic/merge.js
+++ b/public/src/client/topic/merge.js
@@ -37,7 +37,6 @@ define('forum/topic/merge', function () {
 
 	function onTopicClicked(ev) {
 		var tid = $(this).parents('[component="category/topic"]').attr('data-tid');
-		var index = $(this).parents('[component="category/topic"]').attr('data-index');
 		socket.emit('topics.getTopic', tid, function (err, topicData) {
 			if (err) {
 				return app.alertError(err);

From ecc2b9560d7fb8c9c5e2e3dd7c548fae7de40600 Mon Sep 17 00:00:00 2001
From: Baris Usakli <barisusakli@gmail.com>
Date: Wed, 7 Feb 2018 13:02:04 -0500
Subject: [PATCH 10/42] parseInt data.hidden add tests

---
 src/groups/create.js |  9 +++++----
 test/groups.js       | 41 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 4 deletions(-)

diff --git a/src/groups/create.js b/src/groups/create.js
index a8297b946b..9e0678a36f 100644
--- a/src/groups/create.js
+++ b/src/groups/create.js
@@ -8,13 +8,14 @@ var db = require('../database');
 
 module.exports = function (Groups) {
 	Groups.create = function (data, callback) {
-		var system = isSystemGroup(data);
+		var isSystem = isSystemGroup(data);
 		var groupData;
 		var timestamp = data.timestamp || Date.now();
 		var disableJoinRequests = parseInt(data.disableJoinRequests, 10) === 1 ? 1 : 0;
 		if (data.name === 'administrators') {
 			disableJoinRequests = 1;
 		}
+		var isHidden = parseInt(data.hidden, 10) === 1;
 		async.waterfall([
 			function (next) {
 				validateGroupName(data.name, next);
@@ -38,8 +39,8 @@ module.exports = function (Groups) {
 					description: data.description || '',
 					memberCount: memberCount,
 					deleted: 0,
-					hidden: parseInt(data.hidden, 10) === 1 ? 1 : 0,
-					system: system ? 1 : 0,
+					hidden: isHidden ? 1 : 0,
+					system: isSystem ? 1 : 0,
 					private: isPrivate,
 					disableJoinRequests: disableJoinRequests,
 				};
@@ -58,7 +59,7 @@ module.exports = function (Groups) {
 					groupData.ownerUid = data.ownerUid;
 				}
 
-				if (!data.hidden && !system) {
+				if (!isHidden && !isSystem) {
 					tasks.push(async.apply(db.sortedSetAdd, 'groups:visible:createtime', timestamp, groupData.name));
 					tasks.push(async.apply(db.sortedSetAdd, 'groups:visible:memberCount', groupData.memberCount, groupData.name));
 					tasks.push(async.apply(db.sortedSetAdd, 'groups:visible:name', 0, groupData.name.toLowerCase() + ':' + groupData.name));
diff --git a/test/groups.js b/test/groups.js
index 7c305e1e13..928dc72b3c 100644
--- a/test/groups.js
+++ b/test/groups.js
@@ -265,6 +265,47 @@ describe('Groups', function () {
 			});
 		});
 
+		it('should create a hidden group if hidden is 1', function (done) {
+			Groups.create({
+				name: 'hidden group',
+				hidden: '1',
+			}, function (err) {
+				assert.ifError(err);
+				db.isSortedSetMember('groups:visible:memberCount', 'visible group', function (err, isMember) {
+					assert.ifError(err);
+					assert(!isMember);
+					done();
+				});
+			});
+		});
+
+		it('should create a visible group if hidden is 0', function (done) {
+			Groups.create({
+				name: 'visible group',
+				hidden: '0',
+			}, function (err) {
+				assert.ifError(err);
+				db.isSortedSetMember('groups:visible:memberCount', 'visible group', function (err, isMember) {
+					assert.ifError(err);
+					assert(isMember);
+					done();
+				});
+			});
+		});
+
+		it('should create a visible group if hidden is not passed in', function (done) {
+			Groups.create({
+				name: 'visible group 2',
+			}, function (err) {
+				assert.ifError(err);
+				db.isSortedSetMember('groups:visible:memberCount', 'visible group 2', function (err, isMember) {
+					assert.ifError(err);
+					assert(isMember);
+					done();
+				});
+			});
+		});
+
 		it('should fail to create group with duplicate group name', function (done) {
 			Groups.create({ name: 'foo' }, function (err) {
 				assert(err);

From e99d4a5c612a3ad3841364081a8367187c2276cf Mon Sep 17 00:00:00 2001
From: Baris Usakli <barisusakli@gmail.com>
Date: Wed, 7 Feb 2018 15:46:11 -0500
Subject: [PATCH 11/42] closes #6313

---
 src/messaging/rooms.js | 35 ++++++++++++++++++++++++++++++++++
 src/user/delete.js     |  6 ++----
 test/messaging.js      | 43 +++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 79 insertions(+), 5 deletions(-)

diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js
index 41f09c5125..1bdbd3cef4 100644
--- a/src/messaging/rooms.js
+++ b/src/messaging/rooms.js
@@ -177,9 +177,44 @@ module.exports = function (Messaging) {
 				}));
 				db.sortedSetsRemove(keys, roomId, next);
 			},
+			function (next) {
+				updateOwner(roomId, next);
+			},
 		], callback);
 	};
 
+	Messaging.leaveRooms = function (uid, roomIds, callback) {
+		async.waterfall([
+			function (next) {
+				var roomKeys = roomIds.map(function (roomId) {
+					return 'chat:room:' + roomId + ':uids';
+				});
+				db.sortedSetsRemove(roomKeys, uid, next);
+			},
+			function (next) {
+				db.sortedSetRemove('uid:' + uid + ':chat:rooms', roomIds, next);
+			},
+			function (next) {
+				db.sortedSetRemove('uid:' + uid + ':chat:rooms:unread', roomIds, next);
+			},
+			function (next) {
+				async.eachSeries(roomIds, updateOwner, next);
+			},
+		], callback);
+	};
+
+	function updateOwner(roomId, callback) {
+		async.waterfall([
+			function (next) {
+				db.getSortedSetRange('chat:room:' + roomId + ':uids', 0, 0, next);
+			},
+			function (uids, next) {
+				var newOwner = uids[0] || 0;
+				db.setObjectField('chat:room:' + roomId, 'owner', newOwner, next);
+			},
+		], callback);
+	}
+
 	Messaging.getUidsInRoom = function (roomId, start, stop, callback) {
 		db.getSortedSetRevRange('chat:room:' + roomId + ':uids', start, stop, callback);
 	};
diff --git a/src/user/delete.js b/src/user/delete.js
index 92fd8f27b9..0f1ad61047 100644
--- a/src/user/delete.js
+++ b/src/user/delete.js
@@ -7,6 +7,7 @@ var db = require('../database');
 var posts = require('../posts');
 var topics = require('../topics');
 var groups = require('../groups');
+var messaging = require('../messaging');
 var plugins = require('../plugins');
 var batch = require('../batch');
 
@@ -173,12 +174,9 @@ module.exports = function (User) {
 				var userKeys = roomIds.map(function (roomId) {
 					return 'uid:' + uid + ':chat:room:' + roomId + ':mids';
 				});
-				var roomKeys = roomIds.map(function (roomId) {
-					return 'chat:room:' + roomId + ':uids';
-				});
 
 				async.parallel([
-					async.apply(db.sortedSetsRemove, roomKeys, uid),
+					async.apply(messaging.leaveRooms, uid, roomIds),
 					async.apply(db.deleteAll, userKeys),
 				], next);
 			},
diff --git a/test/messaging.js b/test/messaging.js
index 761500e7e4..80ec29d09b 100644
--- a/test/messaging.js
+++ b/test/messaging.js
@@ -177,7 +177,48 @@ describe('Messaging Library', function () {
 				Messaging.isUserInRoom(bazUid, roomId, function (err, isUserInRoom) {
 					assert.ifError(err);
 					assert.equal(isUserInRoom, false);
-					done();
+					Messaging.getRoomData(roomId, function (err, data) {
+						assert.ifError(err);
+						assert.equal(data.owner, fooUid);
+						done();
+					});
+				});
+			});
+		});
+
+		it('should change owner when owner leaves room', function (done) {
+			socketModules.chats.newRoom({ uid: herpUid }, { touid: fooUid }, function (err, roomId) {
+				assert.ifError(err);
+				socketModules.chats.addUserToRoom({ uid: herpUid }, { roomId: roomId, username: 'baz' }, function (err) {
+					assert.ifError(err);
+					socketModules.chats.leave({ uid: herpUid }, roomId, function (err) {
+						assert.ifError(err);
+						Messaging.getRoomData(roomId, function (err, data) {
+							assert.ifError(err);
+							assert.equal(data.owner, fooUid);
+							done();
+						});
+					});
+				});
+			});
+		});
+
+		it('should change owner if owner is deleted', function (done) {
+			User.create({ username: 'deleted_chat_user' }, function (err, sender) {
+				assert.ifError(err);
+				User.create({ username: 'receiver' }, function (err, receiver) {
+					assert.ifError(err);
+					socketModules.chats.newRoom({ uid: sender }, { touid: receiver }, function (err, roomId) {
+						assert.ifError(err);
+						User.deleteAccount(sender, function (err) {
+							assert.ifError(err);
+							Messaging.getRoomData(roomId, function (err, data) {
+								assert.ifError(err);
+								assert.equal(data.owner, receiver);
+								done();
+							});
+						});
+					});
 				});
 			});
 		});

From bb9528b82eec42a230cd983883e23b9f98cc35ba 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, 7 Feb 2018 18:29:56 -0500
Subject: [PATCH 12/42] closes #6314

---
 src/flags.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/flags.js b/src/flags.js
index 7bbb5168f8..95e5921884 100644
--- a/src/flags.js
+++ b/src/flags.js
@@ -648,7 +648,10 @@ Flags.notify = function (flagObj, uid, callback) {
 				async.waterfall([
 					async.apply(posts.getCidByPid, flagObj.targetId),
 					function (cid, next) {
-						groups.getMembers('cid:' + cid + ':privileges:moderate', 0, -1, next);
+						groups.getMembersOfGroups(['cid:' + cid + ':privileges:moderate', 'cid:' + cid + ':privileges:groups:moderate'], next);
+					},
+					function (members, next) {
+						next(null, _.flatten(members));
 					},
 				], next);
 			},

From 2983fc3e5ebf211c5c9111a898c09fe253a6ac53 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, 7 Feb 2018 20:02:07 -0500
Subject: [PATCH 13/42] get group names first

---
 src/flags.js | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/flags.js b/src/flags.js
index 95e5921884..8ea298b8e8 100644
--- a/src/flags.js
+++ b/src/flags.js
@@ -645,10 +645,15 @@ Flags.notify = function (flagObj, uid, callback) {
 			admins: async.apply(groups.getMembers, 'administrators', 0, -1),
 			globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
 			moderators: function (next) {
+				var cid;
 				async.waterfall([
 					async.apply(posts.getCidByPid, flagObj.targetId),
-					function (cid, next) {
-						groups.getMembersOfGroups(['cid:' + cid + ':privileges:moderate', 'cid:' + cid + ':privileges:groups:moderate'], next);
+					function (_cid, next) {
+						cid = _cid;
+						groups.getMembers('cid:' + cid + ':privileges:groups:moderate', 0, -1, next);
+					},
+					function (moderatorGroups, next) {
+						groups.getMembersOfGroups(moderatorGroups.concat(['cid:' + cid + ':privileges:moderate']), next);
 					},
 					function (members, next) {
 						next(null, _.flatten(members));

From 9d171ca1e2171d8416237caddb95cc168e90873b Mon Sep 17 00:00:00 2001
From: Davis <2466545+bojars@users.noreply.github.com>
Date: Thu, 8 Feb 2018 15:26:27 +0200
Subject: [PATCH 14/42] Add cid for filter:category.update hook (#6319)

---
 src/categories/update.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/categories/update.js b/src/categories/update.js
index 866e6cd748..c0572f1ffb 100644
--- a/src/categories/update.js
+++ b/src/categories/update.js
@@ -41,7 +41,7 @@ module.exports = function (Categories) {
 				}
 			},
 			function (next) {
-				plugins.fireHook('filter:category.update', { category: modifiedFields }, next);
+				plugins.fireHook('filter:category.update', { cid: cid, category: modifiedFields }, next);
 			},
 			function (categoryData, next) {
 				category = categoryData.category;

From f2dcbcd7102047597f860a995e06e5d41b5bebfc 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, 8 Feb 2018 10:23:47 -0500
Subject: [PATCH 15/42] closes #6318

---
 src/user/email.js | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/user/email.js b/src/user/email.js
index 9c61211d9a..598536ad29 100644
--- a/src/user/email.js
+++ b/src/user/email.js
@@ -126,14 +126,24 @@ UserEmail.sendValidationEmail = function (uid, options, callback) {
 };
 
 UserEmail.confirm = function (code, callback) {
+	var confirmObj;
 	async.waterfall([
 		function (next) {
 			db.getObject('confirm:' + code, next);
 		},
-		function (confirmObj, next) {
+		function (_confirmObj, next) {
+			confirmObj = _confirmObj;
 			if (!confirmObj || !confirmObj.uid || !confirmObj.email) {
 				return next(new Error('[[error:invalid-data]]'));
 			}
+
+			user.getUserField(confirmObj.uid, 'email', next);
+		},
+		function (currentEmail, next) {
+			if (!currentEmail || currentEmail.toLowerCase() !== confirmObj.email) {
+				return next(new Error('[[error:invalid-email]]'));
+			}
+
 			async.series([
 				async.apply(user.setUserField, confirmObj.uid, 'email:confirmed', 1),
 				async.apply(db.delete, 'confirm:' + code),

From 7f9d9b765421370d1f7a50f4860b9488f0218714 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, 8 Feb 2018 10:35:20 -0500
Subject: [PATCH 16/42] closes #6316

---
 src/socket.io/user/status.js | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/socket.io/user/status.js b/src/socket.io/user/status.js
index 8849f0210e..ccae98ff4c 100644
--- a/src/socket.io/user/status.js
+++ b/src/socket.io/user/status.js
@@ -39,6 +39,13 @@ module.exports = function (SocketUser) {
 			function (next) {
 				user.setUserFields(socket.uid, data, next);
 			},
+			function (next) {
+				if (status !== 'offline') {
+					user.updateOnlineUsers(socket.uid, next);
+				} else {
+					next();
+				}
+			},
 			function (next) {
 				var data = {
 					uid: socket.uid,

From 00776bdd8e2662a0a5f70f5a3460ee6ee6380cfd Mon Sep 17 00:00:00 2001
From: Ben Lubar <ben.lubar+github@gmail.com>
Date: Thu, 8 Feb 2018 09:50:12 -0600
Subject: [PATCH 17/42] Bookmark optimization (#6315)

* Set the user's bookmark if their current bookmark is past the end of the topic.

* Optimize forked topic bookmark updating.

Remove support for updating bookmarks for users who sort by votes.

Don't even consider updating bookmarks for users who have not read the posts being removed.

Only compute post indices once per fork operation instead of once per user that has ever read the topic.
---
 public/src/client/topic.js |  2 +-
 src/topics/bookmarks.js    | 52 +++++++++++++++++++++++++-------------
 2 files changed, 36 insertions(+), 18 deletions(-)

diff --git a/public/src/client/topic.js b/public/src/client/topic.js
index 54052d47c1..63c63e3051 100644
--- a/public/src/client/topic.js
+++ b/public/src/client/topic.js
@@ -231,7 +231,7 @@ define('forum/topic', [
 		var bookmarkKey = 'topic:' + ajaxify.data.tid + ':bookmark';
 		var currentBookmark = ajaxify.data.bookmark || storage.getItem(bookmarkKey);
 
-		if (ajaxify.data.postcount > ajaxify.data.bookmarkThreshold && (!currentBookmark || parseInt(index, 10) > parseInt(currentBookmark, 10))) {
+		if (ajaxify.data.postcount > ajaxify.data.bookmarkThreshold && (!currentBookmark || parseInt(index, 10) > parseInt(currentBookmark, 10) || ajaxify.data.postcount < parseInt(currentBookmark, 10))) {
 			if (app.user.uid) {
 				socket.emit('topics.bookmark', {
 					tid: ajaxify.data.tid,
diff --git a/src/topics/bookmarks.js b/src/topics/bookmarks.js
index 975a0d54f8..d1782b6713 100644
--- a/src/topics/bookmarks.js
+++ b/src/topics/bookmarks.js
@@ -4,7 +4,7 @@
 var async = require('async');
 
 var db = require('../database');
-var posts = require('../posts');
+var user = require('../user');
 
 module.exports = function (Topics) {
 	Topics.getUserBookmark = function (tid, uid, callback) {
@@ -34,7 +34,9 @@ module.exports = function (Topics) {
 	};
 
 	Topics.updateTopicBookmarks = function (tid, pids, callback) {
+		var minIndex;
 		var maxIndex;
+		var postIndices;
 
 		async.waterfall([
 			function (next) {
@@ -42,38 +44,54 @@ module.exports = function (Topics) {
 			},
 			function (postcount, next) {
 				maxIndex = postcount;
-				Topics.getTopicBookmarks(tid, next);
+
+				db.sortedSetRanks('tid:' + tid + ':posts', pids, next);
 			},
-			function (bookmarks, next) {
-				var forkedPosts = pids.map(function (pid) {
-					return { pid: pid, tid: tid };
+			function (indices, next) {
+				postIndices = indices.map(function (i) {
+					return i === null ? 0 : i + 1;
 				});
+				minIndex = Math.min.apply(Math, postIndices);
 
+				Topics.getTopicBookmarks(tid, next);
+			},
+			function (bookmarks, next) {
 				var uidData = bookmarks.map(function (bookmark) {
 					return {
 						uid: bookmark.value,
-						bookmark: bookmark.score,
+						bookmark: parseInt(bookmark.score, 10),
 					};
+				}).filter(function (data) {
+					return data.bookmark >= minIndex;
 				});
 
 				async.eachLimit(uidData, 50, function (data, next) {
-					posts.getPostIndices(forkedPosts, data.uid, function (err, postIndices) {
-						if (err) {
-							return next(err);
+					var bookmark = data.bookmark;
+					bookmark = Math.min(bookmark, maxIndex);
+
+					postIndices.forEach(function (i) {
+						if (i < data.bookmark) {
+							bookmark -= 1;
 						}
+					});
 
-						var bookmark = data.bookmark;
-						bookmark = bookmark < maxIndex ? bookmark : maxIndex;
+					// make sure the bookmark is valid if we removed the last post
+					bookmark = Math.min(bookmark, maxIndex - pids.length);
 
-						for (var i = 0; i < postIndices.length && postIndices[i] < data.bookmark; i += 1) {
-							bookmark -= 1;
+					if (bookmark === data.bookmark) {
+						return next();
+					}
+
+					user.getSettings(data.uid, function (err, settings) {
+						if (err) {
+							return next(err);
 						}
 
-						if (parseInt(bookmark, 10) !== parseInt(data.bookmark, 10)) {
-							Topics.setUserBookmark(tid, data.uid, bookmark, next);
-						} else {
-							next();
+						if (settings.topicPostSort === 'most_votes') {
+							return next();
 						}
+
+						Topics.setUserBookmark(tid, data.uid, bookmark, next);
 					});
 				}, next);
 			},

From d7722de21090e5dce31631b45ff130b56806e007 Mon Sep 17 00:00:00 2001
From: Andrew Rodrigues <rodrigues.andrew@gmail.com>
Date: Thu, 8 Feb 2018 20:04:48 -0500
Subject: [PATCH 18/42] bump emoji to ^2.1.0

---
 install/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/install/package.json b/install/package.json
index 22585bf839..fd938353e1 100644
--- a/install/package.json
+++ b/install/package.json
@@ -61,7 +61,7 @@
         "nconf": "^0.9.1",
         "nodebb-plugin-composer-default": "6.0.12",
         "nodebb-plugin-dbsearch": "2.0.9",
-        "nodebb-plugin-emoji": "2.0.9",
+        "nodebb-plugin-emoji": "^2.1.0",
         "nodebb-plugin-emoji-android": "2.0.0",
         "nodebb-plugin-markdown": "8.3.0",
         "nodebb-plugin-mentions": "2.2.3",

From 34bacb159f3e0acd966cfe5265e7fa185a0232a8 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, 9 Feb 2018 10:01:44 -0500
Subject: [PATCH 19/42] up themes

---
 install/package.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/install/package.json b/install/package.json
index fd938353e1..8a6c91eb58 100644
--- a/install/package.json
+++ b/install/package.json
@@ -68,10 +68,10 @@
         "nodebb-plugin-soundpack-default": "1.0.0",
         "nodebb-plugin-spam-be-gone": "0.5.2",
         "nodebb-rewards-essentials": "0.0.11",
-        "nodebb-theme-lavender": "5.0.1",
-        "nodebb-theme-persona": "7.2.20",
+        "nodebb-theme-lavender": "5.0.2",
+        "nodebb-theme-persona": "7.2.22",
         "nodebb-theme-slick": "1.1.4",
-        "nodebb-theme-vanilla": "8.1.9",
+        "nodebb-theme-vanilla": "8.1.10",
         "nodebb-widget-essentials": "4.0.2",
         "nodemailer": "4.4.1",
         "passport": "^0.4.0",

From eca593bbac2c2440afb6468d66c1d945c4f52779 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, 9 Feb 2018 10:07:09 -0500
Subject: [PATCH 20/42] up lavender

---
 install/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/install/package.json b/install/package.json
index 8a6c91eb58..6ae6f66977 100644
--- a/install/package.json
+++ b/install/package.json
@@ -68,7 +68,7 @@
         "nodebb-plugin-soundpack-default": "1.0.0",
         "nodebb-plugin-spam-be-gone": "0.5.2",
         "nodebb-rewards-essentials": "0.0.11",
-        "nodebb-theme-lavender": "5.0.2",
+        "nodebb-theme-lavender": "5.0.3",
         "nodebb-theme-persona": "7.2.22",
         "nodebb-theme-slick": "1.1.4",
         "nodebb-theme-vanilla": "8.1.10",

From 52092531c7d2287cc830d9f6d42729cd18f4ab42 Mon Sep 17 00:00:00 2001
From: "Misty (Bot)" <deploy@nodebb.org>
Date: Sat, 10 Feb 2018 09:24:46 +0000
Subject: [PATCH 21/42] Latest translations and fallbacks

---
 .../cs/admin/appearance/customise.json        |  6 +--
 .../language/cs/admin/general/homepage.json   |  2 +-
 public/language/cs/admin/manage/tags.json     |  2 +-
 public/language/cs/admin/manage/users.json    | 38 +++++++++----------
 public/language/cs/admin/menu.json            | 10 ++---
 public/language/cs/admin/settings/chat.json   |  4 +-
 public/language/cs/admin/settings/post.json   | 20 +++++-----
 .../cs/admin/settings/reputation.json         |  6 +--
 public/language/cs/error.json                 | 12 +++---
 public/language/cs/global.json                |  6 +--
 public/language/cs/pages.json                 |  4 +-
 public/language/cs/topic.json                 |  4 +-
 12 files changed, 57 insertions(+), 57 deletions(-)

diff --git a/public/language/cs/admin/appearance/customise.json b/public/language/cs/admin/appearance/customise.json
index 6fa4cbfbca..10876739b9 100644
--- a/public/language/cs/admin/appearance/customise.json
+++ b/public/language/cs/admin/appearance/customise.json
@@ -1,7 +1,7 @@
 {
-	"custom-css": "Custom CSS/LESS",
-	"custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.",
-	"custom-css.enable": "Enable Custom CSS/LESS",
+	"custom-css": "Uživatelský CSS/LESS",
+	"custom-css.description": "Zadejte vlastní definici CSS/LESS, která bude nadřazená ostatním stylům.",
+	"custom-css.enable": "Povolit uživatelský CSS/LESS",
 
 	"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.",
diff --git a/public/language/cs/admin/general/homepage.json b/public/language/cs/admin/general/homepage.json
index 61d64ab3a7..3db45d23c3 100644
--- a/public/language/cs/admin/general/homepage.json
+++ b/public/language/cs/admin/general/homepage.json
@@ -4,5 +4,5 @@
 	"home-page-route": "Cesta k domovské stránce",
 	"custom-route": "Upravit cestu",
 	"allow-user-home-pages": "Povolit uživatelům domovské stránky",
-	"home-page-title": "Title of the home page (default \"Home\")"
+	"home-page-title": "Titulka domovské stránky (výchozí „Domů”)"
 }
\ No newline at end of file
diff --git a/public/language/cs/admin/manage/tags.json b/public/language/cs/admin/manage/tags.json
index 8c4fb5a127..3a2e5682e2 100644
--- a/public/language/cs/admin/manage/tags.json
+++ b/public/language/cs/admin/manage/tags.json
@@ -6,7 +6,7 @@
 	"description": "Vyberte značky pomocí kliknutí a/nebo přetažením, pro vícenásobný výběr, použijte klávesu Shift.",
 	"create": "Vytvořit značku",
 	"modify": "Upravit značky",
-	"rename": "Rename Tags",
+	"rename": "Přejmenovat značky",
 	"delete": "Odstranit vybrané značky",
 	"search": "Hledat značky...",
 	"settings": "Pro přejití na stránku s nastavením značek, klikněte <a href=\"%1\">zde</a>.",
diff --git a/public/language/cs/admin/manage/users.json b/public/language/cs/admin/manage/users.json
index fe95fa070e..ea092e24da 100644
--- a/public/language/cs/admin/manage/users.json
+++ b/public/language/cs/admin/manage/users.json
@@ -5,14 +5,14 @@
 	"remove-admin": "Odebrat správce",
 	"validate-email": "Ověřit e-mail",
 	"send-validation-email": "Poslat ověřovací e-mail",
-	"password-reset-email": "Poslat e-mail pro resetování hesla",
+	"password-reset-email": "Poslat e-mail pro resetování hesla",
 	"ban": "Zakázat uživatele",
 	"temp-ban": "Dočasně zakázat uživatele",
 	"unban": "Zrušit zákaz uživatele",
 	"reset-lockout": "Obnovit uzamčení",
 	"reset-flags": "Obnovit označení",
 	"delete": "Odstranit uživatele",
-	"purge": "Odstranit uživatele a obsah",
+	"purge": "Odstranit uživatele a obsah",
 	"download-csv": "Stáhnout jako CSV",
 	"invite": "Pozvat",
 	"new": "Nový uživatel",
@@ -27,19 +27,19 @@
 	"pills.banned": "Zakázán",
 	"pills.search": "Hledat uživatele",
 
-	"search.uid": "By User ID",
-	"search.uid-placeholder": "Enter a user ID to search",
+	"search.uid": "Dle ID uživatele",
+	"search.uid-placeholder": "Pro hledání, zadejte ID uživatele",
 	"search.username": "Dle jména uživatele",
 	"search.username-placeholder": "Zadejte hledané uživatelské jméno",
 	"search.email": "Podle e-mailu",
 	"search.email-placeholder": "Zadejte hledaný e-mail",
-	"search.ip": "Podle IP adresy",
-	"search.ip-placeholder": "Zadejte hledanou IP adresu",
+	"search.ip": "Podle IP adresy",
+	"search.ip-placeholder": "Zadejte hledanou IP adresu",
 	"search.not-found": "Uživatel nebyl nalezen.",
 
-	"inactive.3-months": "3 měsíce",
-	"inactive.6-months": "6 měsíců",
-	"inactive.12-months": "12 měsíců",
+	"inactive.3-months": "3 měsíce",
+	"inactive.6-months": "6 měsíců",
+	"inactive.12-months": "12 měsíců",
 
 	"users.uid": "uid",
 	"users.username": "jméno",
@@ -71,15 +71,15 @@
 	"alerts.lockout-reset-success": "Uzamčení bylo obnoveno.",
 	"alerts.flag-reset-success": "Označení bylo obnoveno.",
 	"alerts.no-remove-yourself-admin": "Sebe jako správce nemůžete vyjmout.",
-	"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 moderator.",
-	"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.make-admin-success": "Uživatel je nyní správcem",
+	"alerts.confirm-remove-admin": "Opravdu chcete vyjmout tohoto správce?",
+	"alerts.remove-admin-success": "Uživatel již není správcem.",
+	"alerts.make-global-mod-success": "Uživatel je nyní globálním moderátorem.",
+	"alerts.confirm-remove-global-mod": "Opravdu chcete vyjmout tohoto globálního moderátora?",
+	"alerts.remove-global-mod-success": "Uživatel již není globálním moderátorem.",
+	"alerts.make-moderator-success": "Uživatel je nyní moderátorem.",
+	"alerts.confirm-remove-moderator": "Opravdu chcete vyjmout tohoto moderátora?",
+	"alerts.remove-moderator-success": "Uživatel není již moderátorem.",
 	"alerts.confirm-validate-email": "Chcete schválit e-mailové adresy těchto uživatelů?",
 	"alerts.validate-email-success": "E-maily byly ověřeny",
 	"alerts.password-reset-confirm": "Chcete poslat těmto uživatelům e-mail pro resetování hesla?",
@@ -95,5 +95,5 @@
 
 	"alerts.prompt-email": "E-mail:",
 	"alerts.email-sent-to": "E-mail s pozvánkou byl odeslán na %1",
-	"alerts.x-users-found": "Počet nalezených uživatelů: %1 (hledání trvalo %2 ms)"
+	"alerts.x-users-found": "Počet nalezených uživatelů: %1 (hledání trvalo %2 ms)"
 }
\ No newline at end of file
diff --git a/public/language/cs/admin/menu.json b/public/language/cs/admin/menu.json
index 739daa2b71..65b9ee394d 100644
--- a/public/language/cs/admin/menu.json
+++ b/public/language/cs/admin/menu.json
@@ -9,10 +9,10 @@
 
 	"section-manage": "Spravovat",
 	"manage/categories": "Kategorie",
-	"manage/privileges": "Privileges",
+	"manage/privileges": "Oprávnění",
 	"manage/tags": "Značky",
 	"manage/users": "Uživatelé",
-	"manage/admins-mods": "Admins & Mods",
+	"manage/admins-mods": "Správci a moderátoři",
 	"manage/registration": "Registrační fronta",
 	"manage/post-queue": "Fronta příspěvků",
 	"manage/groups": "Skupiny",
@@ -70,8 +70,8 @@
 	"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ů…",
-	"search.start-typing": "Začněte psát pro zobrazení výsledků…",
+	"search.keep-typing": "Pište dále pro zobrazení výsledků…",
+	"search.start-typing": "Začněte psát pro zobrazení výsledků…",
 
-	"connection-lost": "Připojení k %1 bylo ztraceno, snaha o opětovné připojení…"
+	"connection-lost": "Připojení k %1 bylo ztraceno, snaha o opětovné připojení…"
 }
\ No newline at end of file
diff --git a/public/language/cs/admin/settings/chat.json b/public/language/cs/admin/settings/chat.json
index da584e2fe4..57e5e4ad9c 100644
--- a/public/language/cs/admin/settings/chat.json
+++ b/public/language/cs/admin/settings/chat.json
@@ -6,6 +6,6 @@
 	"max-length": "Maximální délka konverzační zprávy",
 	"max-room-size": "Maximální počet uživatelů v konverzační místnosti",
 	"delay": "Čas mezi konverzačními zprávami v milisekundách",
-	"restrictions.seconds-edit-after": "Number of seconds before users are allowed to edit chat messages after posting. (0 disabled)",
-	"restrictions.seconds-delete-after": "Number of seconds before users are allowed to delete chat messages after posting. (0 disabled)"
+	"restrictions.seconds-edit-after": "Počet sekund než je uživateli umožněno upravit zprávy konverzace po jejich odeslání. (0 zákaz)",
+	"restrictions.seconds-delete-after": "Počet sekund než je uživateli umožněno smazat zprávy konverzace po jejich odeslání. (0 zákaz)"
 }
\ 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 d55738c807..b8c9350e8e 100644
--- a/public/language/cs/admin/settings/post.json
+++ b/public/language/cs/admin/settings/post.json
@@ -6,25 +6,25 @@
 	"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",
-	"length": "Post Length",
+	"length": "Délka příspěvku",
 	"restrictions": "Omezení příspěvků",
-	"restrictions-new": "New User Restrictions",
+	"restrictions-new": "Omezení nového uživatele",
 	"restrictions.post-queue": "Povolit frontu pro příspěvky",
-	"restrictions-new.post-queue": "Enable new user restrictions",
+	"restrictions-new.post-queue": "Povolit omezení nových uživatelů",
 	"restrictions.post-queue-help": "Povolení fronty příspěvků bude přidávat příspěvky nových uživatelů do fronty na schválení.",
-	"restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users.",
-	"restrictions.seconds-between": "Seconds between posts",
-	"restrictions.seconds-between-new": "Seconds between posts for new users",
-	"restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted",
+	"restrictions-new.post-queue-help": "Povolení omezení nových uživatelů uvede v činnost omezení na vytvořené příspěvky nových uživatelů.",
+	"restrictions.seconds-between": "Sekund mezi příspěvky",
+	"restrictions.seconds-between-new": "Sekund mezi příspěvky pro nové uživatele",
+	"restrictions.rep-threshold": "Ohraničení reputace než začnou platit tato omezení",
 	"restrictions.seconds-defore-new": "Sekundy předtím, než uživatel může přidat příspěvek",
-	"restrictions.seconds-edit-after": "Number of seconds before users are allowed to edit posts after posting. (0 disabled)",
-	"restrictions.seconds-delete-after": "Number of seconds before users are allowed to delete posts after posting. (0 disabled)",
+	"restrictions.seconds-edit-after": "Počet sekund než bude moci uživatel upravit příspěvek před jeho odesláním. (0 zákaz)",
+	"restrictions.seconds-delete-after": "Počet sekund než bude moci uživatel smazat příspěvek před jeho odesláním. (0 zákaz)",
 	"restrictions.replies-no-delete": "Počet odpovědí, kdy je uživatelům zakázáno odstranit jejich vlastní příspěvek. (0 zakázáno)",
 	"restrictions.min-title-length": "Minimální délka názvu",
 	"restrictions.max-title-length": "Maximální délka názvu",
 	"restrictions.min-post-length": "Minimální délka příspěvku",
 	"restrictions.max-post-length": "Maximální délka příspěvku",
-	"restrictions.days-until-stale": "Days until topic is considered stale",
+	"restrictions.days-until-stale": "Počet dnů, než je téma považováno za neaktuální",
 	"restrictions.stale-help": "Je-li téma považováno za „staré”, uživateli se zobrazí oznámení při pokusu o přidání odpovědi.",
 	"timestamp": "Časový otisk",
 	"timestamp.cut-off": "Datum ukončení (ve dnech)",
diff --git a/public/language/cs/admin/settings/reputation.json b/public/language/cs/admin/settings/reputation.json
index 7339496268..a72f32ad00 100644
--- a/public/language/cs/admin/settings/reputation.json
+++ b/public/language/cs/admin/settings/reputation.json
@@ -6,7 +6,7 @@
 	"thresholds": "Omezení aktivity",
 	"min-rep-downvote": "Minimální reputace pro vyjádření nesouhlasu s příspěvkem",
 	"min-rep-flag": "Minimální reputace pro označení příspěvků",
-	"min-rep-website": "Minimum reputation to add \"Website\" to user profile",
-	"min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile",
-	"min-rep-signature": "Minimum reputation to add \"Signature\" to user profile"
+	"min-rep-website": "Minimální reputace pro přidání „Webové stránky” do uživatelského profilu",
+	"min-rep-aboutme": "Minimální reputace pro přidání „O mně” do uživatelského profilu",
+	"min-rep-signature": "Minimální reputace pro přidání „Podpisu” do uživatelského profilu"
 }
\ No newline at end of file
diff --git a/public/language/cs/error.json b/public/language/cs/error.json
index 6ad76f1ca6..01ff27c810 100644
--- a/public/language/cs/error.json
+++ b/public/language/cs/error.json
@@ -17,7 +17,7 @@
     "invalid-login-credentials": "Neplatné přihlašovací údaje",
     "invalid-username-or-password": "Zadejte prosím uživatelské jméno a i heslo",
     "invalid-search-term": "Neplatný výraz pro vyhledávání",
-    "invalid-url": "Invalid URL",
+    "invalid-url": "Neplatné URL",
     "csrf-invalid": "Není možné vás přihlásit, díky vypršení relace. Zkuste to prosím znovu.",
     "invalid-pagination-value": "Neplatná hodnota stránkování, musí být alespoň %1 a nejvýše %2",
     "username-taken": "Uživatelské jméno je již použito",
@@ -114,16 +114,16 @@
     "cant-edit-chat-message": "Tuto zprávu nemůžete upravit",
     "cant-remove-last-user": "Posledního uživatele nemůžete vyjmout",
     "cant-delete-chat-message": "Tuto zprávu nemůžete odstranit",
-    "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting",
-    "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting",
+    "chat-edit-duration-expired": "Je vám umožněno upravit konverzační zprávy pod dobu %1 sekund/y po jejich odeslání",
+    "chat-delete-duration-expired": "Je vám umožněno odstranit konverzační zprávy pod dobu %1 sekund/y po jejich odeslání",
     "already-voting-for-this-post": "Již jste v tomto příspěvku hlasoval.",
     "reputation-system-disabled": "Systém reputací je zakázán.",
     "downvoting-disabled": "Systém nesouhlasu je zakázán",
     "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",
-    "not-enough-reputation-min-rep-website": "You do not have enough reputation to add a website",
-    "not-enough-reputation-min-rep-aboutme": "You do not have enough reputation to add an about me",
-    "not-enough-reputation-min-rep-signature": "You do not have enough reputation to add a signature",
+    "not-enough-reputation-min-rep-website": "Pro přidání webové stránky nemáte dostatek reputace",
+    "not-enough-reputation-min-rep-aboutme": "Pro přidání „O mně” nemáte dostatek reputace",
+    "not-enough-reputation-min-rep-signature": "Pro přidání podpisu nemáte dostatek reputace",
     "already-flagged": "Tento příspěvek jste již označil",
     "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.",
diff --git a/public/language/cs/global.json b/public/language/cs/global.json
index 90db3ca447..695a4f3946 100644
--- a/public/language/cs/global.json
+++ b/public/language/cs/global.json
@@ -53,7 +53,7 @@
     "topics": "Témata",
     "posts": "Příspěvky",
     "best": "Nejlepší",
-    "votes": "Votes",
+    "votes": "Počet hlasů",
     "upvoters": "Souhlasník",
     "upvoted": "Souhlasů",
     "downvoters": "Nesouhlasník",
@@ -85,7 +85,7 @@
     "language": "Jazyk",
     "guest": "Host",
     "guests": "Hosté",
-    "updated.title": "Fórum zaktualizováno",
+    "updated.title": "Fórum bylo zaktualizováno",
     "updated.message": "Toto fórum bylo právě aktualizováno na poslední verzi. Klikněte zde a obnovte tuto stránku.",
     "privacy": "Soukromí",
     "follow": "Sledovat",
@@ -101,7 +101,7 @@
     "unsaved-changes": "Některé změny nebyly uloženy. Jste si jist, že chcete jít jinam?",
     "reconnecting-message": "Vypadá to, že vaše připojení k %1 bylo ukončeno. Vyčkejte prosím, než obnovíme připojení.",
     "play": "Přehrát",
-    "cookies.message": "Tato webová stránka používá \"cookies\", aby jste mohl plně zažít její funkčnost.",
+    "cookies.message": "Pro využití plné funkčnosti stránek, jsou použity „cookies”.",
     "cookies.accept": "Rozumím.",
     "cookies.learn_more": "Zjistit více",
     "edited": "Upraveno",
diff --git a/public/language/cs/pages.json b/public/language/cs/pages.json
index 151b0a0a4a..31267947ad 100644
--- a/public/language/cs/pages.json
+++ b/public/language/cs/pages.json
@@ -6,7 +6,7 @@
     "popular-month": "Oblíbená témata pro tento měsíc",
     "popular-alltime": "Oblíbená témata za celou dobu",
     "recent": "Aktuální témata",
-    "top": "Top Voted Topics",
+    "top": "Témata s nejvíce hlasy",
     "moderator-tools": "Nástroje moderátora",
     "flagged-content": "Nahlášený obsah",
     "ip-blacklist": "Černá listina IP adres",
@@ -20,7 +20,7 @@
     "users/search": "Hledat uživatele",
     "notifications": "Upozornění",
     "tags": "Značky",
-    "tag": "Topics tagged under &quot;%1&quot;",
+    "tag": "Témata označená &quot;%1&quot;",
     "register": "Zaregistrovat účet",
     "registration-complete": "Registrace dokončena",
     "login": "Přihlásit se ke svému účtu",
diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json
index 2b55844f41..e9b469c64c 100644
--- a/public/language/cs/topic.json
+++ b/public/language/cs/topic.json
@@ -34,7 +34,7 @@
     "flag_title": "Označit tento příspěvek k moderování",
     "deleted_message": "Toto téma bylo odstraněno. Jen uživatelé s oprávněním správy témat ho mohou vidět.",
     "following_topic.message": "Nyní budete dostávat upozornění, jakmile někdo přidá příspěvek do tohoto tématu.",
-    "not_following_topic.message": "Uvidíte toto téma v seznamu nepřečtených témat, ale neobdržíte upozornění. pokud sem někdo přidá příspěvek.",
+    "not_following_topic.message": " Toto téma uvidíte v seznamu nepřečtených témat, ale neobdržíte upozornění, přidá-li někdo nový příspěvek.",
     "ignoring_topic.message": "Již nadále neuvidíte toto téma v seznamu nepřečtených témat. Budete upozorněn, jakmile se někdo o vás zmíní nebo bude vyjádřen souhlas s příspěvkem.",
     "login_to_subscribe": "Pro sledování tohoto tématu se prosím přihlaste nebo zaregistrujte.",
     "markAsUnreadForAll.success": "Téma označeno jako nepřečtené pro všechny.",
@@ -52,7 +52,7 @@
     "not-watching.description": "Neupozorňovat na nové odpovědi. <br/>Zobrazit téma v nepřečtených, není-li tato kategorie ignorována",
     "ignoring.description": "Neupozorňovat na nové odpovědi.<br/>Nezobrazovat téma v nepřečtených.",
     "thread_tools.title": "Nástroje tématu",
-    "thread_tools.markAsUnreadForAll": "Mark Unread For All",
+    "thread_tools.markAsUnreadForAll": "Označit nepřečtené pro všechny",
     "thread_tools.pin": "Připnout téma",
     "thread_tools.unpin": "Odepnout téma",
     "thread_tools.lock": "Zamknout téma",

From cf087b6070773ce9028ef697398e88242b481769 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, 10 Feb 2018 14:41:54 -0500
Subject: [PATCH 22/42] #6272 strip all tags

---
 public/src/utils.js | 10 +++++++++-
 src/routes/feeds.js |  6 +++---
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/public/src/utils.js b/public/src/utils.js
index d921573f7a..77c90c4fd9 100644
--- a/public/src/utils.js
+++ b/public/src/utils.js
@@ -518,7 +518,15 @@
 			}
 		},
 
-		tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr'],
+		tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont',
+			'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
+			'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
+			'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+			'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
+			'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
+			'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
+			'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
+			'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr'],
 
 		stripTags: ['abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'base', 'basefont',
 			'bdi', 'bdo', 'big', 'blink', 'body', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
diff --git a/src/routes/feeds.js b/src/routes/feeds.js
index ccb5af1d76..a60b3b4f3d 100644
--- a/src/routes/feeds.js
+++ b/src/routes/feeds.js
@@ -106,7 +106,7 @@ function generateForTopic(req, res, callback) {
 			var author = topicData.posts.length ? topicData.posts[0].username : '';
 
 			var feed = new rss({
-				title: utils.stripHTMLTags(topicData.title, utils.stripTags),
+				title: utils.stripHTMLTags(topicData.title, utils.tags),
 				description: description,
 				feed_url: nconf.get('url') + '/topic/' + tid + '.rss',
 				site_url: nconf.get('url') + '/topic/' + topicData.slug,
@@ -125,7 +125,7 @@ function generateForTopic(req, res, callback) {
 					dateStamp = new Date(parseInt(parseInt(postData.edited, 10) === 0 ? postData.timestamp : postData.edited, 10)).toUTCString();
 
 					feed.item({
-						title: 'Reply to ' + utils.stripHTMLTags(topicData.title, utils.stripTags) + ' on ' + dateStamp,
+						title: 'Reply to ' + utils.stripHTMLTags(topicData.title, utils.tags) + ' on ' + dateStamp,
 						description: postData.content,
 						url: nconf.get('url') + '/post/' + postData.pid,
 						author: postData.user ? postData.user.username : '',
@@ -301,7 +301,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
 
 	async.each(feedTopics, function (topicData, next) {
 		var feedItem = {
-			title: utils.stripHTMLTags(topicData.title, utils.stripTags),
+			title: utils.stripHTMLTags(topicData.title, utils.tags),
 			url: nconf.get('url') + '/topic/' + topicData.slug,
 			date: new Date(parseInt(topicData.lastposttime, 10)).toUTCString(),
 		};

From eddd1697e4f1ab9659c6f635996d264e4c05ecc1 Mon Sep 17 00:00:00 2001
From: Andrew Rodrigues <rodrigues.andrew@gmail.com>
Date: Sat, 10 Feb 2018 15:03:15 -0500
Subject: [PATCH 23/42] up composer

---
 install/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/install/package.json b/install/package.json
index 6ae6f66977..4e1a6b5d3b 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.12",
+        "nodebb-plugin-composer-default": "6.0.13",
         "nodebb-plugin-dbsearch": "2.0.9",
         "nodebb-plugin-emoji": "^2.1.0",
         "nodebb-plugin-emoji-android": "2.0.0",

From f7c412882a06aff12b24f8f4c9665dfcee273a45 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@nodebb.org>
Date: Mon, 12 Feb 2018 11:13:55 -0500
Subject: [PATCH 24/42] add reset routes to robots.txt disallow

---
 src/controllers/index.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/controllers/index.js b/src/controllers/index.js
index 6e82dbf4a2..f6bfc322f4 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -233,6 +233,7 @@ Controllers.robots = function (req, res) {
 	} else {
 		res.send('User-agent: *\n' +
 			'Disallow: ' + nconf.get('relative_path') + '/admin/\n' +
+			'Disallow: ' + nconf.get('relative_path') + '/reset/\n' +
 			'Sitemap: ' + nconf.get('url') + '/sitemap.xml');
 	}
 };

From d1368eb549ff51472681894c5e8e1fada83edec3 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@nodebb.org>
Date: Mon, 12 Feb 2018 12:27:58 -0500
Subject: [PATCH 25/42] bumping version number

---
 install/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/install/package.json b/install/package.json
index 4e1a6b5d3b..ef5caf1c55 100644
--- a/install/package.json
+++ b/install/package.json
@@ -2,7 +2,7 @@
     "name": "nodebb",
     "license": "GPL-3.0",
     "description": "NodeBB Forum",
-    "version": "1.7.4",
+    "version": "1.7.5",
     "homepage": "http://www.nodebb.org",
     "repository": {
       "type": "git",

From 7b6282f5304c34c0612d95cc9f79e427f00d0da7 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: Mon, 12 Feb 2018 17:12:18 -0500
Subject: [PATCH 26/42] closes #6308

---
 public/language/en-GB/user.json      |  2 +-
 src/controllers/accounts/settings.js | 10 ++++++++++
 src/controllers/api.js               |  1 +
 src/languages.js                     |  7 ++++---
 src/middleware/admin.js              | 13 ++++---------
 src/middleware/render.js             |  3 +++
 src/user/settings.js                 |  2 ++
 7 files changed, 25 insertions(+), 13 deletions(-)

diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json
index 466f6c27e4..3e5d51c4aa 100644
--- a/public/language/en-GB/user.json
+++ b/public/language/en-GB/user.json
@@ -103,7 +103,7 @@
 	"topics_per_page": "Topics per Page",
 	"posts_per_page": "Posts per Page",
 	"max_items_per_page": "Maximum %1",
-
+	"acp_language": "Admin Page Language",
 	"notification_sounds" : "Play a sound when you receive a notification",
 	"notifications_and_sounds": "Notifications & Sounds",
 	"incoming-message-sound": "Incoming message sound",
diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js
index cadb7c12f8..39828c5350 100644
--- a/src/controllers/accounts/settings.js
+++ b/src/controllers/accounts/settings.js
@@ -1,6 +1,7 @@
 'use strict';
 
 var async = require('async');
+var _ = require('lodash');
 
 var user = require('../../user');
 var languages = require('../../languages');
@@ -40,6 +41,9 @@ settingsController.get = function (req, res, callback) {
 		function (results, next) {
 			userData.settings = results.settings;
 			userData.languages = results.languages;
+			if (userData.isAdmin && userData.isSelf) {
+				userData.acpLanguages = _.cloneDeep(results.languages);
+			}
 
 			var types = [
 				'notification',
@@ -135,6 +139,12 @@ settingsController.get = function (req, res, callback) {
 				language.selected = language.code === userData.settings.userLang;
 			});
 
+			if (userData.isAdmin && userData.isSelf) {
+				userData.acpLanguages.forEach(function (language) {
+					language.selected = language.code === userData.settings.acpLang;
+				});
+			}
+
 			var notifFreqOptions = [
 				'all',
 				'everyTen',
diff --git a/src/controllers/api.js b/src/controllers/api.js
index 4f9430826a..7321694107 100644
--- a/src/controllers/api.js
+++ b/src/controllers/api.js
@@ -86,6 +86,7 @@ apiController.loadConfig = function (req, callback) {
 			config.topicsPerPage = settings.topicsPerPage;
 			config.postsPerPage = settings.postsPerPage;
 			config.userLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.userLang || config.defaultLang;
+			config.acpLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.acpLang;
 			config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab;
 			config.topicPostSort = settings.topicPostSort || config.topicPostSort;
 			config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort;
diff --git a/src/languages.js b/src/languages.js
index cdf56bf81d..65d5c2113d 100644
--- a/src/languages.js
+++ b/src/languages.js
@@ -71,12 +71,13 @@ Languages.list = function (callback) {
 				if (err) {
 					return next(err);
 				}
+				var lang;
 				try {
-					var lang = JSON.parse(file);
-					next(null, lang);
+					lang = JSON.parse(file);
 				} catch (e) {
-					next(e);
+					return next(e);
 				}
+				next(null, lang);
 			});
 		}, function (err, languages) {
 			if (err) {
diff --git a/src/middleware/admin.js b/src/middleware/admin.js
index 2d0968d50f..3086f045cc 100644
--- a/src/middleware/admin.js
+++ b/src/middleware/admin.js
@@ -38,7 +38,7 @@ module.exports = function (middleware) {
 			plugins: [],
 			authentication: [],
 		};
-
+		res.locals.config = res.locals.config || {};
 		async.waterfall([
 			function (next) {
 				async.parallel({
@@ -51,9 +51,6 @@ module.exports = function (middleware) {
 					custom_header: function (next) {
 						plugins.fireHook('filter:admin.header.build', custom_header, next);
 					},
-					config: function (next) {
-						controllers.api.getConfig(req, res, next);
-					},
 					configs: function (next) {
 						meta.configs.list(next);
 					},
@@ -64,8 +61,6 @@ module.exports = function (middleware) {
 				userData.uid = req.uid;
 				userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1;
 
-				res.locals.config = results.config;
-
 				var acpPath = req.path.slice(1).split('/');
 				acpPath.forEach(function (path, i) {
 					acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1);
@@ -73,9 +68,9 @@ module.exports = function (middleware) {
 				acpPath = acpPath.join(' > ');
 
 				var templateValues = {
-					config: results.config,
-					configJSON: jsesc(JSON.stringify(results.config), { isScriptContext: true }),
-					relative_path: results.config.relative_path,
+					config: res.locals.config,
+					configJSON: jsesc(JSON.stringify(res.locals.config), { isScriptContext: true }),
+					relative_path: res.locals.config.relative_path,
 					adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)),
 					user: userData,
 					userJSON: jsesc(JSON.stringify(userData), { isScriptContext: true }),
diff --git a/src/middleware/render.js b/src/middleware/render.js
index a4571b6879..1da50e8a74 100644
--- a/src/middleware/render.js
+++ b/src/middleware/render.js
@@ -120,6 +120,9 @@ module.exports = function (middleware) {
 
 	function translate(str, req, res, next) {
 		var language = (res.locals.config && res.locals.config.userLang) || 'en-GB';
+		if (res.locals.renderAdminHeader) {
+			language = (res.locals.config && res.locals.config.acpLang) || 'en-GB';
+		}
 		language = req.query.lang ? validator.escape(String(req.query.lang)) : language;
 		translator.translate(str, language, function (translated) {
 			next(null, translator.unescape(translated));
diff --git a/src/user/settings.js b/src/user/settings.js
index df5ed93d71..cac4430fbc 100644
--- a/src/user/settings.js
+++ b/src/user/settings.js
@@ -70,6 +70,7 @@ module.exports = function (User) {
 				settings.topicsPerPage = Math.min(settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, defaultTopicsPerPage);
 				settings.postsPerPage = Math.min(settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, defaultPostsPerPage);
 				settings.userLang = settings.userLang || meta.config.defaultLang || 'en-GB';
+				settings.acpLang = settings.acpLang || settings.userLang;
 				settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest');
 				settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest');
 				settings.followTopicsOnCreate = parseInt(getSetting(settings, 'followTopicsOnCreate', 1), 10) === 1;
@@ -118,6 +119,7 @@ module.exports = function (User) {
 			topicsPerPage: Math.min(data.topicsPerPage, parseInt(maxTopicsPerPage, 10) || 20),
 			postsPerPage: Math.min(data.postsPerPage, parseInt(maxPostsPerPage, 10) || 20),
 			userLang: data.userLang || meta.config.defaultLang,
+			acpLang: data.acpLang || meta.config.defaultLang,
 			followTopicsOnCreate: data.followTopicsOnCreate,
 			followTopicsOnReply: data.followTopicsOnReply,
 			restrictChat: data.restrictChat,

From 651b1cc9f816963774c8b963cea6d528c43447e0 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: Tue, 13 Feb 2018 10:52:05 -0500
Subject: [PATCH 27/42] closes #6321

---
 install/data/navigation.json                        | 11 -----------
 public/language/en-GB/admin/general/navigation.json |  3 ---
 public/src/modules/helpers.js                       |  3 +--
 src/views/admin/general/navigation.tpl              |  9 ---------
 4 files changed, 1 insertion(+), 25 deletions(-)

diff --git a/install/data/navigation.json b/install/data/navigation.json
index 8c7965dc7e..3f47367ce8 100644
--- a/install/data/navigation.json
+++ b/install/data/navigation.json
@@ -70,16 +70,5 @@
 			"targetBlank": false,
 			"adminOnly": true
 		}
-	},
-	{
-		"route": "/search",
-		"title": "[[global:header.search]]",
-		"enabled": true,
-		"iconClass": "fa-search",
-		"textClass": "visible-xs-inline",
-		"text": "[[global:header.search]]",
-		"properties": {
-			"searchInstalled": true
-		}
 	}
 ]
\ No newline at end of file
diff --git a/public/language/en-GB/admin/general/navigation.json b/public/language/en-GB/admin/general/navigation.json
index 9abf7f58cc..a199668ae2 100644
--- a/public/language/en-GB/admin/general/navigation.json
+++ b/public/language/en-GB/admin/general/navigation.json
@@ -14,9 +14,6 @@
 	"only-guest": "Only display to guests",
 	"open-new-window": "Open in a new window",
 
-	"installed-plugins-required": "Installed Plugins Required:",
-	"search-plugin": "Search plugin",
-
 	"btn.delete": "Delete",
 	"btn.disable": "Disable",
 	"btn.enable": "Enable",
diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js
index d682faf947..0a3eea6b26 100644
--- a/public/src/modules/helpers.js
+++ b/public/src/modules/helpers.js
@@ -46,8 +46,7 @@
 			if ((properties.loggedIn && !loggedIn) ||
 				(properties.guestOnly && loggedIn) ||
 				(properties.globalMod && !data.isGlobalMod && !data.isAdmin) ||
-				(properties.adminOnly && !data.isAdmin) ||
-				(properties.searchInstalled && !data.searchEnabled)) {
+				(properties.adminOnly && !data.isAdmin)) {
 				return false;
 			}
 		}
diff --git a/src/views/admin/general/navigation.tpl b/src/views/admin/general/navigation.tpl
index 7be444f3d1..1ba3477a07 100644
--- a/src/views/admin/general/navigation.tpl
+++ b/src/views/admin/general/navigation.tpl
@@ -91,15 +91,6 @@
 						</label>
 					</div>
 
-					<strong>[[admin/general/navigation:installed-plugins-required]]</strong>
-
-					<div class="checkbox">
-						<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
-							<input class="mdl-switch__input" type="checkbox" name="property:searchInstalled" <!-- IF enabled.properties.searchInstalled -->checked<!-- ENDIF enabled.properties.searchInstalled -->/>
-							<span class="mdl-switch__label"><strong>[[admin/general/navigation:search-plugin]]</strong></span>
-						</label>
-					</div>
-
 					<button class="btn btn-danger delete">[[admin/general/navigation:btn.delete]]</button>
 					<!-- IF enabled.enabled -->
 					<button class="btn btn-warning toggle">[[admin/general/navigation:btn.disable]]</button>

From 379a15632850e4ed0d7982cea2b5506ce06b7741 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: Tue, 13 Feb 2018 14:49:30 -0500
Subject: [PATCH 28/42] add status code to body

---
 src/middleware/render.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/middleware/render.js b/src/middleware/render.js
index 1da50e8a74..1ab21ea351 100644
--- a/src/middleware/render.js
+++ b/src/middleware/render.js
@@ -41,8 +41,7 @@ module.exports = function (middleware) {
 					options.template = { name: template };
 					options.template[template] = true;
 					options.url = (req.baseUrl + req.path.replace(/^\/api/, ''));
-					options.bodyClass = buildBodyClass(req, options);
-
+					options.bodyClass = buildBodyClass(req, res, options);
 					plugins.fireHook('filter:' + template + '.build', { req: req, res: res, templateData: options }, next);
 				},
 				function (data, next) {
@@ -129,7 +128,7 @@ module.exports = function (middleware) {
 		});
 	}
 
-	function buildBodyClass(req, templateData) {
+	function buildBodyClass(req, res, templateData) {
 		var clean = req.path.replace(/^\/api/, '').replace(/^\/|\/$/g, '');
 		var parts = clean.split('/').slice(0, 3);
 		parts.forEach(function (p, index) {
@@ -148,6 +147,7 @@ module.exports = function (middleware) {
 			parts.push('page-topic-category-' + utils.slugify(templateData.category.name));
 		}
 
+		parts.push('page-status-' + res.statusCode);
 		return parts.join(' ');
 	}
 };

From da3ce2e1d5f051d135e6093f65eeeb42f8ada3a2 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@nodebb.org>
Date: Wed, 14 Feb 2018 10:37:56 -0500
Subject: [PATCH 29/42] added error text for multiple associations

---
 public/language/en-GB/error.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json
index df48972483..f206a9b882 100644
--- a/public/language/en-GB/error.json
+++ b/public/language/en-GB/error.json
@@ -159,6 +159,7 @@
 	"wrong-login-type-email": "Please use your email to login",
 	"wrong-login-type-username": "Please use your username to login",
 	"sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first",
+	"sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.",
 
 	"invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).",
 

From 723f31a36246ae7c4545a3aabae4a4fbe31c052f 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, 14 Feb 2018 11:53:51 -0500
Subject: [PATCH 30/42] closes #6323

---
 install/data/defaults.json                    |  8 +-
 .../language/en-GB/admin/settings/user.json   |  3 +-
 src/controllers/accounts/settings.js          |  2 +-
 src/upgrades/1.7.6/notification_types.js      | 26 ++++++
 src/user/settings.js                          |  6 ++
 src/views/admin/settings/user.tpl             | 80 ++++++++++++++++++-
 test/notifications.js                         |  2 +-
 7 files changed, 122 insertions(+), 5 deletions(-)
 create mode 100644 src/upgrades/1.7.6/notification_types.js

diff --git a/install/data/defaults.json b/install/data/defaults.json
index 3d45ff6565..547adb1334 100644
--- a/install/data/defaults.json
+++ b/install/data/defaults.json
@@ -38,5 +38,11 @@
     "bookmarkThreshold": 5,
     "topicsPerList": 20,
     "autoDetectLang": 1,
-    "min:rep:flag": 0
+    "min:rep:flag": 0,
+    "notificationType_upvote": "notification",
+    "notificationType_new-topic": "notification",
+    "notificationType_new-reply": "notification",
+    "notificationType_follow": "notification",
+    "notificationType_new-chat": "notification",
+    "notificationType_group-invite": "notification"
 }
diff --git a/public/language/en-GB/admin/settings/user.json b/public/language/en-GB/admin/settings/user.json
index cbdd4ee91c..664bff67f7 100644
--- a/public/language/en-GB/admin/settings/user.json
+++ b/public/language/en-GB/admin/settings/user.json
@@ -62,5 +62,6 @@
 	"email-chat-notifs": "Send an email if a new chat message arrives and I am not online",
 	"email-post-notif": "Send an email when replies are made to topics I am subscribed to",
 	"follow-created-topics": "Follow topics you create",
-	"follow-replied-topics": "Follow topics that you reply to"
+	"follow-replied-topics": "Follow topics that you reply to",
+	"default-notification-settings": "Default notification settings"
 }
\ No newline at end of file
diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js
index 39828c5350..5166222706 100644
--- a/src/controllers/accounts/settings.js
+++ b/src/controllers/accounts/settings.js
@@ -213,7 +213,7 @@ function getNotificationSettings(userData, callback) {
 		},
 		function (results, next) {
 			function modifyType(type) {
-				var setting = userData.settings[type] || 'notification';
+				var setting = userData.settings[type];
 
 				return {
 					name: type,
diff --git a/src/upgrades/1.7.6/notification_types.js b/src/upgrades/1.7.6/notification_types.js
new file mode 100644
index 0000000000..156acc3769
--- /dev/null
+++ b/src/upgrades/1.7.6/notification_types.js
@@ -0,0 +1,26 @@
+'use strict';
+
+var async = require('async');
+var db = require('../../database');
+
+module.exports = {
+	name: 'Add default settings for notification delivery types',
+	timestamp: Date.UTC(2018, 1, 14),
+	method: function (callback) {
+		async.waterfall([
+			function (next) {
+				db.getObject('config', next);
+			},
+			function (config, next) {
+				db.setObject('config', {
+					notificationType_upvote: config.notificationType_upvote || 'notification',
+					'notificationType_new-topic': config.notificationType_upvote || 'notification',
+					'notificationType_new-reply': config.notificationType_upvote || 'notification',
+					notificationType_follow: config.notificationType_upvote || 'notification',
+					'notificationType_new-chat': config.notificationType_upvote || 'notification',
+					'notificationType_group-invite': config.notificationType_upvote || 'notification',
+				}, next);
+			},
+		], callback);
+	},
+};
diff --git a/src/user/settings.js b/src/user/settings.js
index cac4430fbc..4268db7515 100644
--- a/src/user/settings.js
+++ b/src/user/settings.js
@@ -81,6 +81,12 @@ module.exports = function (User) {
 				settings.delayImageLoading = parseInt(getSetting(settings, 'delayImageLoading', 1), 10) === 1;
 				settings.bootswatchSkin = settings.bootswatchSkin || meta.config.bootswatchSkin || 'default';
 				settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1;
+				settings.notificationType_upvote = getSetting(settings, 'notificationType_upvote', 'notification');
+				settings['notificationType_new-topic'] = getSetting(settings, 'notificationType_new-topic', 'notification');
+				settings['notificationType_new-reply'] = getSetting(settings, 'notificationType_new-reply', 'notification');
+				settings.notificationType_follow = getSetting(settings, 'notificationType_follow', 'notification');
+				settings['notificationType_new-chat'] = getSetting(settings, 'notificationType_new-chat', 'notification');
+				settings['notificationType_group-invite'] = getSetting(settings, 'notificationType_group-invite', 'notification');
 				next(null, settings);
 			},
 		], callback);
diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl
index 8b2528624e..02286cc636 100644
--- a/src/views/admin/settings/user.tpl
+++ b/src/views/admin/settings/user.tpl
@@ -232,7 +232,6 @@
 	<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/user:default-user-settings]]</div>
 	<div class="col-sm-10 col-xs-12">
 		<form>
-
 			<div class="checkbox">
 				<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
 					<input class="mdl-switch__input" type="checkbox" data-field="showemail">
@@ -292,6 +291,85 @@
 				</label>
 			</div>
 
+			<label>[[admin/settings/user:default-notification-settings]]</label>
+			<div class="row">
+				<div class="form-group col-xs-7">
+					<label>[[notifications:notificationType_upvote]]</label>
+				</div>
+				<div class="form-group col-xs-5">
+					<select class="form-control" data-field="notificationType_upvote">
+						<option value="none">[[notifications:none]]</option>
+						<option value="notification">[[notifications:notification_only]]</option>
+						<option value="email">[[notifications:email_only]]</option>
+						<option value="notificationemail">[[notifications:notification_and_email]]</option>
+					</select>
+				</div>
+			</div>
+			<div class="row">
+				<div class="form-group col-xs-7">
+					<label>[[notifications:notificationType_new-topic]]</label>
+				</div>
+				<div class="form-group col-xs-5">
+					<select class="form-control" data-field="notificationType_new-topic">
+						<option value="none">[[notifications:none]]</option>
+						<option value="notification">[[notifications:notification_only]]</option>
+						<option value="email">[[notifications:email_only]]</option>
+						<option value="notificationemail">[[notifications:notification_and_email]]</option>
+					</select>
+				</div>
+			</div>
+			<div class="row">
+				<div class="form-group col-xs-7">
+					<label>[[notifications:notificationType_new-reply]]</label>
+				</div>
+				<div class="form-group col-xs-5">
+					<select class="form-control" data-field="notificationType_new-reply">
+						<option value="none">[[notifications:none]]</option>
+						<option value="notification">[[notifications:notification_only]]</option>
+						<option value="email">[[notifications:email_only]]</option>
+						<option value="notificationemail">[[notifications:notification_and_email]]</option>
+					</select>
+				</div>
+			</div>
+			<div class="row">
+				<div class="form-group col-xs-7">
+					<label>[[notifications:notificationType_follow]]</label>
+				</div>
+				<div class="form-group col-xs-5">
+					<select class="form-control" data-field="notificationType_follow">
+						<option value="none">[[notifications:none]]</option>
+						<option value="notification">[[notifications:notification_only]]</option>
+						<option value="email">[[notifications:email_only]]</option>
+						<option value="notificationemail">[[notifications:notification_and_email]]</option>
+					</select>
+				</div>
+			</div>
+			<div class="row">
+				<div class="form-group col-xs-7">
+					<label>[[notifications:notificationType_new-chat]]</label>
+				</div>
+				<div class="form-group col-xs-5">
+					<select class="form-control" data-field="notificationType_new-chat">
+						<option value="none">[[notifications:none]]</option>
+						<option value="notification">[[notifications:notification_only]]</option>
+						<option value="email">[[notifications:email_only]]</option>
+						<option value="notificationemail">[[notifications:notification_and_email]]</option>
+					</select>
+				</div>
+			</div>
+			<div class="row">
+				<div class="form-group col-xs-7">
+					<label>[[notifications:notificationType_group-invite]]</label>
+				</div>
+				<div class="form-group col-xs-5">
+					<select class="form-control" data-field="notificationType_group-invite">
+						<option value="none">[[notifications:none]]</option>
+						<option value="notification">[[notifications:notification_only]]</option>
+						<option value="email">[[notifications:email_only]]</option>
+						<option value="notificationemail">[[notifications:notification_and_email]]</option>
+					</select>
+				</div>
+			</div>
 		</form>
 	</div>
 </div>
diff --git a/test/notifications.js b/test/notifications.js
index 314e901822..a937510012 100644
--- a/test/notifications.js
+++ b/test/notifications.js
@@ -357,7 +357,7 @@ describe('Notifications', function () {
 				setTimeout(function () {
 					user.notifications.getAll(uid, 'post', function (err, nids) {
 						assert.ifError(err);
-						assert.notEqual(nids.indexOf(nid), -1);
+						assert(nids.includes(nid));
 						done();
 					});
 				}, 1500);

From 708fda9372ba86eb521d7127c1ea0c3bd70c9b65 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, 14 Feb 2018 12:31:33 -0500
Subject: [PATCH 31/42] use old settings if available

---
 src/upgrades/1.7.6/notification_types.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/upgrades/1.7.6/notification_types.js b/src/upgrades/1.7.6/notification_types.js
index 156acc3769..d56e1d3324 100644
--- a/src/upgrades/1.7.6/notification_types.js
+++ b/src/upgrades/1.7.6/notification_types.js
@@ -15,9 +15,9 @@ module.exports = {
 				db.setObject('config', {
 					notificationType_upvote: config.notificationType_upvote || 'notification',
 					'notificationType_new-topic': config.notificationType_upvote || 'notification',
-					'notificationType_new-reply': config.notificationType_upvote || 'notification',
+					'notificationType_new-reply': config.notificationType_upvote || config.sendPostNotifications || 'notification',
 					notificationType_follow: config.notificationType_upvote || 'notification',
-					'notificationType_new-chat': config.notificationType_upvote || 'notification',
+					'notificationType_new-chat': config.notificationType_upvote || config.sendChatNotifications || 'notification',
 					'notificationType_group-invite': config.notificationType_upvote || 'notification',
 				}, next);
 			},

From fa0328fe35618137c3a6cc0f40090a2389c59ec6 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, 14 Feb 2018 12:33:19 -0500
Subject: [PATCH 32/42] fix my copy paste fail

---
 src/upgrades/1.7.6/notification_types.js | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/upgrades/1.7.6/notification_types.js b/src/upgrades/1.7.6/notification_types.js
index d56e1d3324..d8d7636b9c 100644
--- a/src/upgrades/1.7.6/notification_types.js
+++ b/src/upgrades/1.7.6/notification_types.js
@@ -14,11 +14,11 @@ module.exports = {
 			function (config, next) {
 				db.setObject('config', {
 					notificationType_upvote: config.notificationType_upvote || 'notification',
-					'notificationType_new-topic': config.notificationType_upvote || 'notification',
-					'notificationType_new-reply': config.notificationType_upvote || config.sendPostNotifications || 'notification',
-					notificationType_follow: config.notificationType_upvote || 'notification',
-					'notificationType_new-chat': config.notificationType_upvote || config.sendChatNotifications || 'notification',
-					'notificationType_group-invite': config.notificationType_upvote || 'notification',
+					'notificationType_new-topic': config['notificationType_new-topic'] || 'notification',
+					'notificationType_new-reply': config['notificationType_new-reply'] || config.sendPostNotifications || 'notification',
+					notificationType_follow: config.notificationType_follow || 'notification',
+					'notificationType_new-chat': config['notificationType_new-chat'] || config.sendChatNotifications || 'notification',
+					'notificationType_group-invite': config['notificationType_group-invite'] || 'notification',
 				}, next);
 			},
 		], callback);

From dcc896ee052066e765adff1d82ea1e4cfb51a463 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, 14 Feb 2018 13:40:12 -0500
Subject: [PATCH 33/42] add tid to vote notifs

---
 src/socket.io/helpers.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js
index ae4c2b0b5a..4679b73598 100644
--- a/src/socket.io/helpers.js
+++ b/src/socket.io/helpers.js
@@ -113,6 +113,7 @@ SocketHelpers.sendNotificationToPostOwner = function (pid, fromuid, command, not
 				bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]',
 				bodyLong: results.postObj.content,
 				pid: pid,
+				tid: postData.tid,
 				path: '/post/' + pid,
 				nid: command + ':post:' + pid + ':uid:' + fromuid,
 				from: fromuid,

From 575b70b5ab528f05a766ccb1597b2d7b6b169136 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, 14 Feb 2018 16:29:33 -0500
Subject: [PATCH 34/42] add some checks to templatesOnDemand

---
 src/middleware/index.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/middleware/index.js b/src/middleware/index.js
index c0cce7623a..b1e065b3fd 100644
--- a/src/middleware/index.js
+++ b/src/middleware/index.js
@@ -237,12 +237,18 @@ middleware.templatesOnDemand = function (req, res, next) {
 			fs.readFile(filePath.replace(/\.js$/, '.tpl'), 'utf8', cb);
 		},
 		function (source, cb) {
+			if (!source) {
+				return cb(new Error('[[error:templatesOnDemand.source-template-empty]]'));
+			}
 			Benchpress.precompile({
 				source: source,
 				minify: global.env !== 'development',
 			}, cb);
 		},
 		function (compiled, cb) {
+			if (!compiled) {
+				return cb(new Error('[[error:templatesOnDemand.compiled-template-empty]]'));
+			}
 			fs.writeFile(filePath, compiled, cb);
 		},
 	], function (err) {

From 854f79142ecd92e6e01696e46348585d09993ded Mon Sep 17 00:00:00 2001
From: "Misty (Bot)" <deploy@nodebb.org>
Date: Thu, 15 Feb 2018 09:25:16 +0000
Subject: [PATCH 35/42] Latest translations and fallbacks

---
 public/language/ru/user.json | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/public/language/ru/user.json b/public/language/ru/user.json
index 242763bade..24b93da2e8 100644
--- a/public/language/ru/user.json
+++ b/public/language/ru/user.json
@@ -85,7 +85,7 @@
     "has_no_posts": "Участник пока не создал ни одной записи",
     "has_no_topics": "Участник пока не создал ни одной темы",
     "has_no_watched_topics": "Участник пока не посмотрел ни одной темы",
-    "has_no_ignored_topics": "This user hasn't ignored any topics yet.",
+    "has_no_ignored_topics": "Этот пользователь еще не игнорировал ни одной темы.",
     "has_no_upvoted_posts": "Участник пока не голосовал положительно ни за одну запись",
     "has_no_downvoted_posts": "Участник пока не голосовал против ни одной записи",
     "has_no_voted_posts": "Участник пока не голосовал ни за одну запись",
@@ -101,11 +101,11 @@
     "outgoing-message-sound": "Звук исходящего сообщения",
     "notification-sound": "Звук уведомления",
     "no-sound": "Без звука",
-    "upvote-notif-freq": "Upvote Notification Frequency",
-    "upvote-notif-freq.all": "All Upvotes",
+    "upvote-notif-freq": "Частота уведомлений о положительном отзыве",
+    "upvote-notif-freq.all": "Все положительные отзывы",
     "upvote-notif-freq.everyTen": "Every Ten Upvotes",
     "upvote-notif-freq.logarithmic": "On 10, 100, 1000...",
-    "upvote-notif-freq.disabled": "Disabled",
+    "upvote-notif-freq.disabled": "Выключено",
     "browsing": "Настройки просмотра",
     "open_links_in_new_tab": "Открывать внешние ссылки в новом окне",
     "enable_topic_searching": "Поиск во всех записях темы",

From a224c557c0d0f22600e6b880048ead9e3dcb2822 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, 15 Feb 2018 12:46:04 -0500
Subject: [PATCH 36/42] closes #6326

---
 public/language/en-GB/admin/manage/categories.json |  4 +++-
 public/src/admin/manage/categories.js              | 14 ++++++++++++++
 src/views/admin/manage/categories.tpl              |  2 ++
 3 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/public/language/en-GB/admin/manage/categories.json b/public/language/en-GB/admin/manage/categories.json
index 871affe560..6aa607612d 100644
--- a/public/language/en-GB/admin/manage/categories.json
+++ b/public/language/en-GB/admin/manage/categories.json
@@ -65,5 +65,7 @@
 	"alert.find-user": "Find a User",
 	"alert.user-search": "Search for a user here...",
 	"alert.find-group": "Find a Group",
-	"alert.group-search": "Search for a group here..."
+	"alert.group-search": "Search for a group here...",
+	"collapse-all": "Collapse All",
+	"expand-all": "Expand All"
 }
\ No newline at end of file
diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js
index 5d211d00ca..2cd4914ce0 100644
--- a/public/src/admin/manage/categories.js
+++ b/public/src/admin/manage/categories.js
@@ -37,6 +37,20 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri
 			el.find('i').toggleClass('fa-minus').toggleClass('fa-plus');
 			el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden');
 		});
+
+		$('#collapse-all').on('click', function () {
+			toggleAll(false);
+		});
+
+		$('#expand-all').on('click', function () {
+			toggleAll(true);
+		});
+
+		function toggleAll(expand) {
+			var el = $('.categories .toggle');
+			el.find('i').toggleClass('fa-minus', expand).toggleClass('fa-plus', !expand);
+			el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden', !expand);
+		}
 	};
 
 	Categories.throwCreateModal = function () {
diff --git a/src/views/admin/manage/categories.tpl b/src/views/admin/manage/categories.tpl
index 22388d01de..ff46ed5fc9 100644
--- a/src/views/admin/manage/categories.tpl
+++ b/src/views/admin/manage/categories.tpl
@@ -1,3 +1,5 @@
+<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button> <button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
+<hr/>
 <div class="categories"></div>
 
 <button data-action="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">

From 15e9bbac92852d7a5ae76fbed395471b8d85a836 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, 15 Feb 2018 14:52:49 -0500
Subject: [PATCH 37/42] closes #6311

---
 install/package.json                          |  1 +
 .../en-GB/admin/manage/ip-blacklist.json      |  3 +-
 public/language/en-GB/topic.json              |  2 ++
 public/src/client/topic/postTools.js          | 22 +++++++++----
 src/controllers/admin/blacklist.js            |  1 -
 src/meta/blacklist.js                         | 25 ++++++++++++---
 src/meta/js.js                                |  1 +
 src/plugins/hooks.js                          | 31 +++++++++----------
 src/socket.io/blacklist.js                    | 15 +++++++++
 src/socket.io/posts/tools.js                  | 21 ++++++++++---
 test/plugins.js                               | 16 ----------
 11 files changed, 89 insertions(+), 49 deletions(-)

diff --git a/install/package.json b/install/package.json
index ef5caf1c55..42f4dd4303 100644
--- a/install/package.json
+++ b/install/package.json
@@ -25,6 +25,7 @@
         "body-parser": "^1.18.2",
         "bootstrap": "^3.3.7",
         "chart.js": "^2.7.1",
+        "clipboard": "1.7.1",
         "colors": "^1.1.2",
         "compression": "^1.7.1",
         "commander": "^2.12.2",
diff --git a/public/language/en-GB/admin/manage/ip-blacklist.json b/public/language/en-GB/admin/manage/ip-blacklist.json
index cd79294266..588fbd62b6 100644
--- a/public/language/en-GB/admin/manage/ip-blacklist.json
+++ b/public/language/en-GB/admin/manage/ip-blacklist.json
@@ -14,5 +14,6 @@
 	"alerts.applied-success": "Blacklist Applied",
 
 	"analytics.blacklist-hourly": "<strong>Figure 1</strong> &ndash; Blacklist hits per hour",
-	"analytics.blacklist-daily": "<strong>Figure 2</strong> &ndash; Blacklist hits per day"
+	"analytics.blacklist-daily": "<strong>Figure 2</strong> &ndash; Blacklist hits per day",
+	"ip-banned": "IP banned"
 }
\ No newline at end of file
diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json
index a4892868b3..e084a9f24f 100644
--- a/public/language/en-GB/topic.json
+++ b/public/language/en-GB/topic.json
@@ -33,6 +33,8 @@
 	"locked": "Locked",
 	"pinned": "Pinned",
 	"moved": "Moved",
+	"copy-ip": "Copy IP",
+	"ban-ip": "Ban IP",
 
 	"bookmark_instructions" : "Click here to return to the last read post in this thread.",
 
diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js
index 934a3942b7..9cbfdbb366 100644
--- a/public/src/client/topic/postTools.js
+++ b/public/src/client/topic/postTools.js
@@ -8,8 +8,7 @@ define('forum/topic/postTools', [
 	'translator',
 	'forum/topic/votes',
 	'forum/topic/move-post',
-	'benchpress',
-], function (share, navigator, components, translator, votes, movePost, Benchpress) {
+], function (share, navigator, components, translator, votes, movePost) {
 	var PostTools = {};
 
 	var staleReplyAnyway = false;
@@ -45,11 +44,12 @@ define('forum/topic/postTools', [
 				}
 				data.posts.display_move_tools = data.posts.display_move_tools && index !== 0;
 
-				Benchpress.parse('partials/topic/post-menu-list', data, function (html) {
-					translator.translate(html, function (html) {
-						dropdownMenu.html(html);
-						$(window).trigger('action:post.tools.load');
+				app.parseAndTranslate('partials/topic/post-menu-list', data, function (html) {
+					dropdownMenu.html(html);
+					require(['clipboard'], function (clipboard) {
+						new clipboard('[data-clipboard-text]');
 					});
+					$(window).trigger('action:post.tools.load');
 				});
 			});
 		});
@@ -192,6 +192,16 @@ define('forum/topic/postTools', [
 			movePost.openMovePostModal($(this));
 		});
 
+		postContainer.on('click', '[component="post/ban-ip"]', function () {
+			var ip = $(this).attr('data-ip');
+			socket.emit('blacklist.addRule', ip, function (err) {
+				if (err) {
+					return app.alertError(err.message);
+				}
+				app.alertSuccess('[[admin/manage/blacklist:ban-ip]]');
+			});
+		});
+
 		postContainer.on('click', '[component="post/chat"]', function () {
 			openChat($(this));
 		});
diff --git a/src/controllers/admin/blacklist.js b/src/controllers/admin/blacklist.js
index a5f8136463..23d66d7c8a 100644
--- a/src/controllers/admin/blacklist.js
+++ b/src/controllers/admin/blacklist.js
@@ -7,7 +7,6 @@ var analytics = require('../../analytics');
 var blacklistController = module.exports;
 
 blacklistController.get = function (req, res, next) {
-	// Analytics.getBlacklistAnalytics
 	async.parallel({
 		rules: async.apply(meta.blacklist.get),
 		analytics: async.apply(analytics.getBlacklistAnalytics),
diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js
index 2b2f897284..c441c71b95 100644
--- a/src/meta/blacklist.js
+++ b/src/meta/blacklist.js
@@ -9,9 +9,8 @@ var pubsub = require('../pubsub');
 var plugins = require('../plugins');
 var analytics = require('../analytics');
 
-var Blacklist = {
-	_rules: [],
-};
+var Blacklist = module.exports;
+Blacklist._rules = [];
 
 Blacklist.load = function (callback) {
 	callback = callback || function () {};
@@ -182,4 +181,22 @@ Blacklist.validate = function (rules, callback) {
 	});
 };
 
-module.exports = Blacklist;
+Blacklist.addRule = function (rule, callback) {
+	var valid;
+	async.waterfall([
+		function (next) {
+			Blacklist.validate(rule, next);
+		},
+		function (result, next) {
+			valid = result.valid;
+			if (!valid.length) {
+				return next(new Error('[[error:invalid-rule]]'));
+			}
+			Blacklist.get(next);
+		},
+		function (rules, next) {
+			rules = rules + '\n' + valid[0];
+			Blacklist.save(rules, next);
+		},
+	], callback);
+};
diff --git a/src/meta/js.js b/src/meta/js.js
index fb3d12f683..a6dc73331e 100644
--- a/src/meta/js.js
+++ b/src/meta/js.js
@@ -98,6 +98,7 @@ JS.scripts = {
 		'jqueryui.js': 'public/vendor/jquery/js/jquery-ui.js',
 		'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js',
 		ace: 'node_modules/ace-builds/src-min',
+		'clipboard.js': 'node_modules/clipboard/dist/clipboard.min.js',
 	},
 };
 
diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js
index 020ea4e024..c555424377 100644
--- a/src/plugins/hooks.js
+++ b/src/plugins/hooks.js
@@ -82,23 +82,20 @@ module.exports = function (Plugins) {
 
 		var hookList = Plugins.loadedHooks[hook];
 		var hookType = hook.split(':')[0];
-		try {
-			switch (hookType) {
-			case 'filter':
-				fireFilterHook(hook, hookList, params, callback);
-				break;
-			case 'action':
-				fireActionHook(hook, hookList, params, callback);
-				break;
-			case 'static':
-				fireStaticHook(hook, hookList, params, callback);
-				break;
-			default:
-				winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
-				break;
-			}
-		} catch (err) {
-			callback(err);
+		switch (hookType) {
+		case 'filter':
+			fireFilterHook(hook, hookList, params, callback);
+			break;
+		case 'action':
+			fireActionHook(hook, hookList, params, callback);
+			break;
+		case 'static':
+			fireStaticHook(hook, hookList, params, callback);
+			break;
+		default:
+			winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
+			callback();
+			break;
 		}
 	};
 
diff --git a/src/socket.io/blacklist.js b/src/socket.io/blacklist.js
index 1435d20737..d4f1481508 100644
--- a/src/socket.io/blacklist.js
+++ b/src/socket.io/blacklist.js
@@ -26,3 +26,18 @@ SocketBlacklist.save = function (socket, rules, callback) {
 		},
 	], callback);
 };
+
+SocketBlacklist.addRule = function (socket, rule, callback) {
+	async.waterfall([
+		function (next) {
+			user.isAdminOrGlobalMod(socket.uid, next);
+		},
+		function (isAdminOrGlobalMod, next) {
+			if (!isAdminOrGlobalMod) {
+				return callback(new Error('[[error:no-privileges]]'));
+			}
+
+			meta.blacklist.addRule(rule, next);
+		},
+	], callback);
+};
diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js
index a9bbc6137b..a61e50ec6c 100644
--- a/src/socket.io/posts/tools.js
+++ b/src/socket.io/posts/tools.js
@@ -10,6 +10,8 @@ var socketTopics = require('../topics');
 var privileges = require('../../privileges');
 var plugins = require('../../plugins');
 var social = require('../../social');
+var user = require('../../user');
+
 
 module.exports = function (SocketPosts) {
 	SocketPosts.loadPostTools = function (socket, data, callback) {
@@ -20,10 +22,16 @@ module.exports = function (SocketPosts) {
 			function (next) {
 				async.parallel({
 					posts: function (next) {
-						posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid'], next);
+						posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid', 'ip'], next);
+					},
+					isAdmin: function (next) {
+						user.isAdministrator(socket.uid, next);
+					},
+					isGlobalMod: function (next) {
+						user.isGlobalModerator(socket.uid, next);
 					},
-					isAdminOrMod: function (next) {
-						privileges.categories.isAdminOrMod(data.cid, socket.uid, next);
+					isModerator: function (next) {
+						user.isModerator(socket.uid, data.cid, next);
 					},
 					canEdit: function (next) {
 						privileges.posts.canEdit(data.pid, socket.uid, next);
@@ -54,7 +62,12 @@ module.exports = function (SocketPosts) {
 				results.posts.display_delete_tools = results.canDelete.flag;
 				results.posts.display_flag_tools = socket.uid && !results.posts.selfPost && results.canFlag.flag;
 				results.posts.display_moderator_tools = results.posts.display_edit_tools || results.posts.display_delete_tools;
-				results.posts.display_move_tools = results.isAdminOrMod;
+				results.posts.display_move_tools = results.isAdmin || results.isModerator;
+				results.posts.display_ip_ban = (results.isAdmin || results.isGlobalMod) && !results.posts.selfPost;
+
+				if (!results.isAdmin && !results.isGlobalMod && !results.isModerator) {
+					results.posts.ip = undefined;
+				}
 				next(null, results);
 			},
 		], callback);
diff --git a/test/plugins.js b/test/plugins.js
index 47f3969a40..e948cbb160 100644
--- a/test/plugins.js
+++ b/test/plugins.js
@@ -70,22 +70,6 @@ describe('Plugins', function () {
 		});
 	});
 
-	it('should not crash if there is an exception in a hook', function (done) {
-		function filterMethod(data, callback) {
-			var crash;
-			crash.a = 5;
-			callback(null, data);
-		}
-
-		plugins.registerHook('test-plugin-crash', { hook: 'filter:test.crashHook', method: filterMethod });
-
-		plugins.fireHook('filter:test.crashHook', { foo: 1 }, function (err, data) {
-			assert(err);
-			assert.equal(err.message, 'Cannot set property \'a\' of undefined');
-			done();
-		});
-	});
-
 	it('should get plugin data from nbbpm', function (done) {
 		plugins.get('nodebb-plugin-markdown', function (err, data) {
 			assert.ifError(err);

From 1c530c4f09a11171e61eb55dc0e5dd942ba8c143 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, 15 Feb 2018 14:53:59 -0500
Subject: [PATCH 38/42] up persona

---
 install/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/install/package.json b/install/package.json
index 42f4dd4303..0a05ec9ffb 100644
--- a/install/package.json
+++ b/install/package.json
@@ -70,7 +70,7 @@
         "nodebb-plugin-spam-be-gone": "0.5.2",
         "nodebb-rewards-essentials": "0.0.11",
         "nodebb-theme-lavender": "5.0.3",
-        "nodebb-theme-persona": "7.2.22",
+        "nodebb-theme-persona": "7.2.23",
         "nodebb-theme-slick": "1.1.4",
         "nodebb-theme-vanilla": "8.1.10",
         "nodebb-widget-essentials": "4.0.2",

From b240ae89cdbaa9b03919ae396d7eae00dbe74d40 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, 15 Feb 2018 15:50:44 -0500
Subject: [PATCH 39/42] #6289

---
 src/cli/package-install.js | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/src/cli/package-install.js b/src/cli/package-install.js
index af245b08fb..7baaee2a2b 100644
--- a/src/cli/package-install.js
+++ b/src/cli/package-install.js
@@ -41,11 +41,18 @@ function installAll() {
 	} catch (e) {
 		// ignore
 	}
-
-	cproc.execSync(command + (prod ? ' --production' : ''), {
-		cwd: path.join(__dirname, '../../'),
-		stdio: [0, 1, 2],
-	});
+	try {
+		cproc.execSync(command + (prod ? ' --production' : ''), {
+			cwd: path.join(__dirname, '../../'),
+			stdio: [0, 1, 2],
+		});
+	} catch (e) {
+		console.log('Error installing dependencies!');
+		console.log('message: ' + e.message);
+		console.log('stdout: ' + e.stdout);
+		console.log('stderr: ' + e.stderr);
+		throw e;
+	}
 }
 
 exports.installAll = installAll;

From b126d7ef0921ea8b3579b719c87ad8a21be38cf4 Mon Sep 17 00:00:00 2001
From: "Misty (Bot)" <deploy@nodebb.org>
Date: Fri, 16 Feb 2018 09:25:00 +0000
Subject: [PATCH 40/42] Latest translations and fallbacks

---
 public/language/ru/global.json  |  6 +++---
 public/language/ru/modules.json |  2 +-
 public/language/ru/pages.json   | 16 ++++++++--------
 public/language/ru/topic.json   |  8 ++++----
 public/language/ru/unread.json  |  4 ++--
 public/language/ru/user.json    | 18 +++++++++---------
 6 files changed, 27 insertions(+), 27 deletions(-)

diff --git a/public/language/ru/global.json b/public/language/ru/global.json
index 3bb8473b2c..777f9aa376 100644
--- a/public/language/ru/global.json
+++ b/public/language/ru/global.json
@@ -53,7 +53,7 @@
     "topics": "Темы",
     "posts": "Записи",
     "best": "Лучшие",
-    "votes": "Votes",
+    "votes": "Голоса",
     "upvoters": "Кому понравилось",
     "upvoted": "Понравилось",
     "downvoters": "Кому не понравилось",
@@ -105,6 +105,6 @@
     "cookies.accept": "Понял",
     "cookies.learn_more": "Подробнее",
     "edited": "Отредактированный",
-    "disabled": "Disabled",
-    "select": "Select"
+    "disabled": "Отключено",
+    "select": "Выбрать"
 }
\ No newline at end of file
diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json
index 442137ca92..f000b93b6b 100644
--- a/public/language/ru/modules.json
+++ b/public/language/ru/modules.json
@@ -20,7 +20,7 @@
     "chat.three_months": "3 месяца",
     "chat.delete_message_confirm": "Вы уверены, что хотите удалить это сообщение?",
     "chat.add-users-to-room": "Добавить участников в комнату",
-    "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": "Этот пользователь установил статус \"Не беспокоить\". Вы все еще хотите написать ему?",
     "composer.compose": "Редактор сообщений",
     "composer.show_preview": "Показать предпросмотр сообщения",
     "composer.hide_preview": "Скрыть предпросмотр",
diff --git a/public/language/ru/pages.json b/public/language/ru/pages.json
index 7eabfad92f..ede173f40a 100644
--- a/public/language/ru/pages.json
+++ b/public/language/ru/pages.json
@@ -6,11 +6,11 @@
     "popular-month": "Популярные темы этого месяца",
     "popular-alltime": "Популярные темы за всё время",
     "recent": "Последние темы",
-    "top": "Top Voted Topics",
-    "moderator-tools": "Moderator Tools",
-    "flagged-content": "Flagged Content",
+    "top": "Самые популярные темы",
+    "moderator-tools": "Инструменты модератора",
+    "flagged-content": "Выбранное содержимое",
     "ip-blacklist": "Чёрный список IP",
-    "post-queue": "Post Queue",
+    "post-queue": "Очередь публикации",
     "users/online": "В сети",
     "users/latest": "Новые участники",
     "users/sort-posts": "Участники по количеству сообщений",
@@ -20,7 +20,7 @@
     "users/search": "Поиск участников",
     "notifications": "Уведомления",
     "tags": "Метки",
-    "tag": "Topics tagged under &quot;%1&quot;",
+    "tag": "Темы помеченные как &quot;%1&quot;",
     "register": "Зарегистрироваться",
     "registration-complete": "Регистрация завершена",
     "login": "Войти",
@@ -30,8 +30,8 @@
     "group": "Группа %1",
     "chats": "Чаты",
     "chat": "Чат с участником %1",
-    "flags": "Flags",
-    "flag-details": "Flag %1 Details",
+    "flags": "Отметки",
+    "flag-details": "Отметка %1 детали",
     "account/edit": "Редактирование \"%1\"",
     "account/edit/password": "Сменить пароль \"%1\"",
     "account/edit/username": "Изменить имя пользователя \"%1\"",
@@ -45,7 +45,7 @@
     "account/bookmarks": "%1 сообщений в закладках",
     "account/settings": "Настройки учётной записи",
     "account/watched": "Тему просмотрели %1",
-    "account/ignored": "Topics ignored by %1",
+    "account/ignored": "Игнорируемые темы %1",
     "account/upvoted": "Рейтинг записей поднят %1",
     "account/downvoted": "Рейтинг записей снижен %1",
     "account/best": "Лучшие записи участника %1",
diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json
index 777e81e154..2563f68277 100644
--- a/public/language/ru/topic.json
+++ b/public/language/ru/topic.json
@@ -52,7 +52,7 @@
     "not-watching.description": "Не уведомлять меня о новых ответах.<br/>Показать тему в непрочитанных, если категория мной не игнорируется.",
     "ignoring.description": "Не уведомлять меня о новых ответах.<br/>Не отображать тему в непрочитанных.",
     "thread_tools.title": "Настройки темы",
-    "thread_tools.markAsUnreadForAll": "Mark Unread For All",
+    "thread_tools.markAsUnreadForAll": "Выделить непрочитанные для всех",
     "thread_tools.pin": "Прикрепить тему",
     "thread_tools.unpin": "Открепить тему",
     "thread_tools.lock": "Закрыть тему",
@@ -68,8 +68,8 @@
     "thread_tools.restore_confirm": "Вы уверены, что хотите восстановить тему?",
     "thread_tools.purge": "Стереть тему",
     "thread_tools.purge_confirm": "Вы уверены, что хотите стереть эту тему?",
-    "thread_tools.merge_topics": "Merge Topics",
-    "thread_tools.merge": "Merge",
+    "thread_tools.merge_topics": "Объединить темы",
+    "thread_tools.merge": "Объединить",
     "topic_move_success": "Эта тема успешно перемещена в %1",
     "post_delete_confirm": "Вы уверены, что хотите удалить эту запись?",
     "post_restore_confirm": "Вы уверены, что хотите восстановить эту запись?",
@@ -91,7 +91,7 @@
     "fork_pid_count": "Отмечено %1 сообщений",
     "fork_success": "Готово! Просмотр отделённой темы.",
     "delete_posts_instruction": "Отметьте записи, которые вы хотите удалить",
-    "merge_topics_instruction": "Click the topics you want to merge",
+    "merge_topics_instruction": "Выберите темы которые вы хотите объединить",
     "composer.title_placeholder": "Введите название темы...",
     "composer.handle_placeholder": "Название",
     "composer.discard": "Отменить",
diff --git a/public/language/ru/unread.json b/public/language/ru/unread.json
index 1658c72740..bdead98f5d 100644
--- a/public/language/ru/unread.json
+++ b/public/language/ru/unread.json
@@ -10,6 +10,6 @@
     "all-topics": "Все темы",
     "new-topics": "Новые темы",
     "watched-topics": "Подписанные темы",
-    "unreplied-topics": "Unreplied Topics",
-    "multiple-categories-selected": "Multiple Selected"
+    "unreplied-topics": "Неотвеченные темы",
+    "multiple-categories-selected": "Выбрано несколько"
 }
\ No newline at end of file
diff --git a/public/language/ru/user.json b/public/language/ru/user.json
index 24b93da2e8..a30280a04e 100644
--- a/public/language/ru/user.json
+++ b/public/language/ru/user.json
@@ -25,7 +25,7 @@
     "reputation": "Репутация",
     "bookmarks": "Закладки",
     "watched": "Подписка",
-    "ignored": "Ignored",
+    "ignored": "Игнорировать",
     "followers": "Подписчиков",
     "following": "Подписок",
     "aboutme": "Обо мне",
@@ -34,7 +34,7 @@
     "chat": "Чат",
     "chat_with": "Продолжить чат с %1",
     "new_chat_with": "Начать новый чат с %1",
-    "flag-profile": "Flag Profile",
+    "flag-profile": "Идентификатор профиля",
     "follow": "Подписаться",
     "unfollow": "Отписаться",
     "more": "Больше",
@@ -94,17 +94,17 @@
     "paginate_description": "Разбить на страницы, а не выводить бесконечным списком",
     "topics_per_page": "Тем на странице",
     "posts_per_page": "Записей на странице",
-    "max_items_per_page": "Maximum %1",
+    "max_items_per_page": "Максимум %1",
     "notification_sounds": "Воспроизводить звук во время получения уведомления",
     "notifications_and_sounds": "Уведомления и звуки",
     "incoming-message-sound": "Звук входящего сообщения",
     "outgoing-message-sound": "Звук исходящего сообщения",
     "notification-sound": "Звук уведомления",
     "no-sound": "Без звука",
-    "upvote-notif-freq": "Частота уведомлений о положительном отзыве",
+    "upvote-notif-freq": "Частота уведомлений о понравившемся отзыве",
     "upvote-notif-freq.all": "Все положительные отзывы",
-    "upvote-notif-freq.everyTen": "Every Ten Upvotes",
-    "upvote-notif-freq.logarithmic": "On 10, 100, 1000...",
+    "upvote-notif-freq.everyTen": "Каждые десять понравившихся отзывов",
+    "upvote-notif-freq.logarithmic": "На 10, 100, 1000...",
     "upvote-notif-freq.disabled": "Выключено",
     "browsing": "Настройки просмотра",
     "open_links_in_new_tab": "Открывать внешние ссылки в новом окне",
@@ -126,9 +126,9 @@
     "sso.title": "Для вашего удобства вы можете связать ваши учётные записи на других социальных сервисах с учётной записью на нашем сайте",
     "sso.associated": "Связан с",
     "sso.not-associated": "Нажмите здесь, что бы связать учётную запись с",
-    "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": "Открепить",
+    "sso.dissociate-confirm-title": "Подтверждение открепления",
+    "sso.dissociate-confirm": "Вы уверены, что хотите открепить свой аккаунт от %1?",
     "info.latest-flags": "Новые отмеченные сообщения",
     "info.no-flags": "Отмеченных сообщений не найдено",
     "info.ban-history": "Недавно заблокированы",

From 825c493c409004ede26ddfdfa67d56f5a458d194 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, 16 Feb 2018 09:48:32 -0500
Subject: [PATCH 41/42] show error

---
 public/src/admin/settings.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js
index 493afc91f0..803a3f923c 100644
--- a/public/src/admin/settings.js
+++ b/public/src/admin/settings.js
@@ -89,7 +89,7 @@ define('admin/settings', ['uploader'], function (uploader) {
 						alert_id: 'config_status',
 						timeout: 2500,
 						title: 'Changes Not Saved',
-						message: 'NodeBB encountered a problem saving your changes',
+						message: 'NodeBB encountered a problem saving your changes. (' + err.message + ')',
 						type: 'danger',
 					});
 				}

From 2b95b1339792676bfbcc5f973567919dfb284fbb Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@nodebb.org>
Date: Fri, 16 Feb 2018 16:59:29 -0500
Subject: [PATCH 42/42] closes #6328

---
 src/meta/configs.js | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/meta/configs.js b/src/meta/configs.js
index 16445a2c32..bf4bfbb907 100644
--- a/src/meta/configs.js
+++ b/src/meta/configs.js
@@ -4,6 +4,7 @@
 var async = require('async');
 var nconf = require('nconf');
 var path = require('path');
+var winston = require('winston');
 
 var db = require('../database');
 var pubsub = require('../pubsub');
@@ -83,9 +84,17 @@ function processConfig(data, callback) {
 			var image = require('../image');
 			if (data['brand:logo']) {
 				image.size(path.join(nconf.get('upload_path'), 'system', 'site-logo-x50.png'), function (err, size) {
-					if (err) {
+					if (err && err.code === 'ENOENT') {
+						// For whatever reason the x50 logo wasn't generated, gracefully error out
+						winston.warn('[logo] The email-safe logo doesn\'t seem to have been created, please re-upload your site logo.');
+						size = {
+							height: 0,
+							width: 0,
+						};
+					} else if (err) {
 						return next(err);
 					}
+
 					data['brand:emailLogo:height'] = size.height;
 					data['brand:emailLogo:width'] = size.width;
 					next();