diff --git a/Dockerfile b/Dockerfile
index 89b051103d..94f93699a7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # The base image is the latest 8.x node (LTS)
-FROM node:8.15.0@sha256:5aebe186c00da3308c8fde5b3a246d1927a56947a1b51f5c4308b7318adf74f4
+FROM node:8.15.0@sha256:cb66110c9c7d84bae9a6db8675f49d5c9e34d528023ef185b186e29ae5461051
 
 RUN mkdir -p /usr/src/app
 WORKDIR /usr/src/app
diff --git a/install/package.json b/install/package.json
index 6f56bba6b0..73bbf4a763 100644
--- a/install/package.json
+++ b/install/package.json
@@ -32,7 +32,7 @@
         "ace-builds": "^1.2.9",
         "archiver": "^3.0.0",
         "async": "2.6.1",
-        "autoprefixer": "^9.0.0",
+        "autoprefixer": "^9.4.6",
         "bcryptjs": "2.4.3",
         "benchpressjs": "^1.2.5",
         "body-parser": "^1.18.2",
@@ -75,7 +75,7 @@
         "material-design-lite": "^1.3.0",
         "mime": "^2.2.0",
         "mkdirp": "^0.5.1",
-        "mongodb": "3.1.12",
+        "mongodb": "3.1.13",
         "morgan": "^1.9.0",
         "mousetrap": "^1.6.1",
         "mubsub-nbb": "^1.5.0",
@@ -90,16 +90,16 @@
         "nodebb-plugin-spam-be-gone": "0.5.5",
         "nodebb-rewards-essentials": "0.0.13",
         "nodebb-theme-lavender": "5.0.8",
-        "nodebb-theme-persona": "9.1.10",
+        "nodebb-theme-persona": "9.1.13",
         "nodebb-theme-slick": "1.2.19",
-        "nodebb-theme-vanilla": "10.1.15",
+        "nodebb-theme-vanilla": "10.1.18",
         "nodebb-widget-essentials": "4.0.12",
         "nodemailer": "^5.0.0",
         "passport": "^0.4.0",
         "passport-local": "1.0.0",
         "pg": "^7.4.0",
         "pg-cursor": "^2.0.0",
-        "postcss": "7.0.12",
+        "postcss": "7.0.14",
         "postcss-clean": "1.1.0",
         "promise-polyfill": "^8.0.0",
         "prompt": "^1.0.0",
@@ -110,7 +110,7 @@
         "sanitize-html": "^1.16.3",
         "semver": "^5.4.1",
         "serve-favicon": "^2.4.5",
-        "sharp": "0.21.2",
+        "sharp": "0.21.3",
         "sitemap": "^2.0.0",
         "socket.io": "2.2.0",
         "socket.io-adapter-cluster": "^1.0.1",
@@ -133,9 +133,9 @@
         "@commitlint/cli": "7.3.2",
         "@commitlint/config-angular": "7.3.1",
         "coveralls": "3.0.2",
-        "eslint": "5.12.0",
+        "eslint": "5.12.1",
         "eslint-config-airbnb-base": "13.1.0",
-        "eslint-plugin-import": "2.14.0",
+        "eslint-plugin-import": "2.15.0",
         "grunt": "1.0.3",
         "grunt-contrib-watch": "1.1.0",
         "husky": "1.3.1",
diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json
index 35fc87011a..9b8658dceb 100644
--- a/public/language/en-GB/flags.json
+++ b/public/language/en-GB/flags.json
@@ -9,6 +9,7 @@
 	"updated": "Updated",
 	"target-purged": "The content this flag referred to has been purged and is no longer available.",
 
+	"graph-label": "Daily Flags",
 	"quick-filters": "Quick Filters",
 	"filter-active": "There are one or more filters active in this list of flags",
 	"filter-reset": "Remove Filters",
diff --git a/public/language/nl/admin/advanced/database.json b/public/language/nl/admin/advanced/database.json
index a3187bb795..b31a089494 100644
--- a/public/language/nl/admin/advanced/database.json
+++ b/public/language/nl/admin/advanced/database.json
@@ -18,16 +18,16 @@
 	"mongo.resident-memory": "Resident geheugen",
 	"mongo.virtual-memory": "Virtueel geheugen",
 	"mongo.mapped-memory": "Mapped geheugen",
-	"mongo.bytes-in": "Bytes In",
-	"mongo.bytes-out": "Bytes Out",
-	"mongo.num-requests": "Number of Requests",
+	"mongo.bytes-in": "Bytes Inkomend",
+	"mongo.bytes-out": "Bytes Uitgaand",
+	"mongo.num-requests": "Aantal requests",
 	"mongo.raw-info": "MongoDB Raw Info",
 
 	"redis": "Redis",
 	"redis.version": "Redis versie",
-	"redis.keys": "Keys",
-	"redis.expires": "Expires",
-	"redis.avg-ttl": "Average TTL",
+	"redis.keys": "Sleutels",
+	"redis.expires": "Verloopt",
+	"redis.avg-ttl": "Gemiddelde TTL",
 	"redis.connected-clients": "Verbonden clients",
 	"redis.connected-slaves": "Verbonden slaves",
 	"redis.blocked-clients": "Geblokkeerde clients",
diff --git a/public/language/nl/admin/general/dashboard.json b/public/language/nl/admin/general/dashboard.json
index cfec2cd754..7e99e1f24b 100644
--- a/public/language/nl/admin/general/dashboard.json
+++ b/public/language/nl/admin/general/dashboard.json
@@ -49,7 +49,7 @@
 	"active-users.users": "Users",
 	"active-users.guests": "Guests",
 	"active-users.total": "Total",
-	"active-users.connections": "Connections",
+	"active-users.connections": "Connecties",
 
 	"anonymous-registered-users": "Anonymous vs Registered Users",
 	"anonymous": "Anonymous",
@@ -71,6 +71,6 @@
 	"graphs.unique-visitors": "Unique Visitors",
 	"graphs.registered-users": "Registered Users",
 	"graphs.anonymous-users": "Anonymous Users",
-	"last-restarted-by": "Last restarted by",
+	"last-restarted-by": "Laatst herstart door",
 	"no-users-browsing": "No users browsing"
 }
diff --git a/public/language/nl/admin/manage/categories.json b/public/language/nl/admin/manage/categories.json
index c14eadea0c..a48f5a9301 100644
--- a/public/language/nl/admin/manage/categories.json
+++ b/public/language/nl/admin/manage/categories.json
@@ -62,7 +62,7 @@
 	"alert.copy-success": "Settings Copied!",
 	"alert.set-parent-category": "Set Parent Category",
 	"alert.updated": "Updated Categories",
-	"alert.updated-success": "Category IDs %1 successfully updated.",
+	"alert.updated-success": "Category IDs %1 zijn succesvol geüpdatet.",
 	"alert.upload-image": "Upload category image",
 	"alert.find-user": "Find a User",
 	"alert.user-search": "Search for a user here...",
diff --git a/public/language/nl/admin/settings/general.json b/public/language/nl/admin/settings/general.json
index c47481fe11..1977c9678a 100644
--- a/public/language/nl/admin/settings/general.json
+++ b/public/language/nl/admin/settings/general.json
@@ -6,7 +6,7 @@
 	"title.url-help": "Wanneer de titel word aangeklikt stuur gebruikers naar dit adress. Is het leeg gelaten dan worden gebruikers naar de forum hoofdpagina gestuurd.",
 	"title.name": "Jouw Communiy Naam",
 	"title.show-in-header": "Toon Site Titel in Header",
-	"browser-title": "Browser Title",
+	"browser-title": "Browser Titel",
 	"browser-title-help": "Als geen browser titel is gespecificeerd dan word de site titel gebruikt",
 	"title-layout": "Titel Lay-out",
 	"title-layout-help": "Defineer hoe de browser titel gestructureerd word. bijv: {paginaTitel} | {browserTitel}",
@@ -27,7 +27,7 @@
 	"favicon.upload": "Uploaden",
 	"touch-icon": "Startscherm / Aanraakpictogram",
 	"touch-icon.upload": "Uploaden",
-	"touch-icon.help": "Recommended size and format: 192x192, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.",
+	"touch-icon.help": "Aangeraden grootte en formaat: 192x192, alleen PNG formaat. Als er geen touch icoon is gespecificeerd zal NodeBB terugvallen op het favicon.",
 	"outgoing-links": "Outgoing Links",
 	"outgoing-links.warning-page": "Use Outgoing Links Warning Page",
 	"search-default-sort-by": "Search default sort by",
diff --git a/public/language/pt-PT/topic.json b/public/language/pt-PT/topic.json
index c2dc7f548e..74945e942e 100644
--- a/public/language/pt-PT/topic.json
+++ b/public/language/pt-PT/topic.json
@@ -101,7 +101,7 @@
     "composer.title_placeholder": "Insere aqui o título do tópico...",
     "composer.handle_placeholder": "Nome",
     "composer.discard": "Descartar",
-    "composer.submit": "Submeter",
+    "composer.submit": "Publicar",
     "composer.replying_to": "Respondendo a %1",
     "composer.new_topic": "Novo tópico",
     "composer.uploading": "carregando...",
diff --git a/public/src/app.js b/public/src/app.js
index bf68b520c8..c1a21be21e 100644
--- a/public/src/app.js
+++ b/public/src/app.js
@@ -149,7 +149,7 @@ app.cacheBuster = null;
 				Unread.initUnreadTopics();
 				Notifications.prepareDOM();
 				Chat.prepareDOM();
-				app.reskin(data.config.bootswatchSkin);
+				app.reskin(data.header.bootswatchSkin);
 				translator.switchTimeagoLanguage(callback);
 				bootbox.setLocale(config.userLang);
 
@@ -194,6 +194,11 @@ app.cacheBuster = null;
 
 					$(window).trigger('action:app.loggedOut', data);
 					if (data.next) {
+						if (data.next.startsWith('http')) {
+							window.location.href = data.next;
+							return;
+						}
+
 						ajaxify.go(data.next);
 					} else {
 						ajaxify.refresh();
@@ -770,6 +775,17 @@ app.cacheBuster = null;
 			return;
 		}
 
+		var currentSkinClassName = $('body').attr('class').split(/\s+/).filter(function (className) {
+			return className.startsWith('skin-');
+		});
+		var currentSkin = currentSkinClassName[0].slice(5);
+		currentSkin = currentSkin !== 'noskin' ? currentSkin : '';
+
+		// Stop execution if skin didn't change
+		if (skinName === currentSkin) {
+			return;
+		}
+
 		var linkEl = document.createElement('link');
 		linkEl.rel = 'stylesheet';
 		linkEl.type = 'text/css';
@@ -778,13 +794,8 @@ app.cacheBuster = null;
 			clientEl.parentNode.removeChild(clientEl);
 
 			// Update body class with proper skin name
-			var currentSkinClassName = $('body').attr('class').split(/\s+/).filter(function (className) {
-				return className.startsWith('skin-');
-			});
 			$('body').removeClass(currentSkinClassName.join(' '));
-			if (skinName) {
-				$('body').addClass('skin-' + skinName);
-			}
+			$('body').addClass('skin-' + (skinName || 'noskin'));
 		};
 
 		document.head.appendChild(linkEl);
diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js
index 8bce483e44..0495b23306 100644
--- a/public/src/modules/translator.js
+++ b/public/src/modules/translator.js
@@ -586,6 +586,10 @@
 
 			if (!adaptor.timeagoShort) {
 				var languageCode = utils.userLangToTimeagoCode(config.userLang);
+				if (!config.timeagoCodes.includes(languageCode + '-short')) {
+					languageCode = 'en';
+				}
+
 				var originalSettings = assign({}, jQuery.timeago.settings.strings);
 				jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () {
 					adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
@@ -602,6 +606,9 @@
 			delete adaptor.timeagoShort;
 
 			var languageCode = utils.userLangToTimeagoCode(config.userLang);
+			if (!config.timeagoCodes.includes(languageCode + '-short')) {
+				languageCode = 'en';
+			}
 			jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').done(callback);
 		},
 
diff --git a/src/controllers/404.js b/src/controllers/404.js
index baa8d05650..b34926aa5e 100644
--- a/src/controllers/404.js
+++ b/src/controllers/404.js
@@ -9,7 +9,7 @@ var plugins = require('../plugins');
 
 exports.handle404 = function handle404(req, res) {
 	var relativePath = nconf.get('relative_path');
-	var isClientScript = new RegExp('^' + relativePath + '\\/assets\\/src\\/.+\\.js$');
+	var isClientScript = new RegExp('^' + relativePath + '\\/assets\\/src\\/.+\\.js(\\?v=\\w+)?$');
 
 	if (plugins.hasListeners('action:meta.override404')) {
 		return plugins.fireHook('action:meta.override404', {
diff --git a/src/controllers/api.js b/src/controllers/api.js
index 5f37721935..746f67184f 100644
--- a/src/controllers/api.js
+++ b/src/controllers/api.js
@@ -12,6 +12,7 @@ var categories = require('../categories');
 var privileges = require('../privileges');
 var plugins = require('../plugins');
 var translator = require('../translator');
+var languages = require('../languages');
 
 var apiController = module.exports;
 
@@ -62,6 +63,7 @@ apiController.loadConfig = function (req, callback) {
 	config.bootswatchSkin = meta.config.bootswatchSkin || '';
 	config.enablePostHistory = (meta.config.enablePostHistory || 1) === 1;
 	config.notificationAlertTimeout = meta.config.notificationAlertTimeout || 5000;
+	config.timeagoCodes = languages.timeagoCodes;
 
 	if (config.useOutgoingLinksPage) {
 		config.outgoingLinksWhitelist = meta.config['outgoingLinks:whitelist'];
diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js
index 98b352050a..d4bf9620aa 100644
--- a/src/controllers/authentication.js
+++ b/src/controllers/authentication.js
@@ -498,10 +498,12 @@ authenticationController.logout = function (req, res, next) {
 						return res.status(500);
 					}
 
-					res.status(200).send({
+					payload = {
 						header: payload.header,
 						config: res.locals.config,
-					});
+					};
+					plugins.fireHook('filter:user.logout', payload);
+					res.status(200).send(payload);
 				});
 			}
 		},
diff --git a/src/controllers/index.js b/src/controllers/index.js
index ed94cd4713..494b0e9f02 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -92,7 +92,7 @@ Controllers.login = function (req, res, next) {
 	var registrationType = meta.config.registrationType || 'normal';
 
 	var allowLoginWith = (meta.config.allowLoginWith || 'username-email');
-	var returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('base_url'), '');
+	var returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('base_url') + nconf.get('relative_path'), '');
 
 	var errorText;
 	if (req.query.error === 'csrf-invalid') {
@@ -214,7 +214,7 @@ Controllers.registerInterstitial = function (req, res, next) {
 				// No interstitials, redirect to home
 				const returnTo = req.session.returnTo || req.session.registration.returnTo;
 				delete req.session.registration;
-				return helpers.redirect(res, returnTo || nconf.get('relative_path') + '/');
+				return helpers.redirect(res, returnTo || '/');
 			}
 			var renders = data.interstitials.map(function (interstitial) {
 				return async.apply(req.app.render.bind(req.app), interstitial.template, interstitial.data || {});
@@ -252,6 +252,7 @@ Controllers.robots = function (req, res) {
 		res.send('User-agent: *\n' +
 			'Disallow: ' + nconf.get('relative_path') + '/admin/\n' +
 			'Disallow: ' + nconf.get('relative_path') + '/reset/\n' +
+			'Disallow: ' + nconf.get('relative_path') + '/compose\n' +
 			'Sitemap: ' + nconf.get('url') + '/sitemap.xml');
 	}
 };
diff --git a/src/middleware/header.js b/src/middleware/header.js
index 38e5aa2bb3..65ec79d219 100644
--- a/src/middleware/header.js
+++ b/src/middleware/header.js
@@ -140,7 +140,9 @@ module.exports = function (middleware) {
 				results.user['email:confirmed'] = results.user['email:confirmed'] === 1;
 				results.user.isEmailConfirmSent = !!results.isEmailConfirmSent;
 
-				templateValues.bootswatchSkin = parseInt(meta.config.disableCustomUserSkins, 10) !== 1 ? res.locals.config.bootswatchSkin || '' : '';
+				templateValues.bootswatchSkin = (parseInt(meta.config.disableCustomUserSkins, 10) !== 1 ? res.locals.config.bootswatchSkin : '') || meta.config.bootswatchSkin || '';
+				templateValues.config.bootswatchSkin = templateValues.bootswatchSkin || 'noskin';	// TODO remove in v1.12.0+
+
 				const unreadCounts = results.unreadData.counts;
 				var unreadCount = {
 					topic: unreadCounts[''] || 0,
diff --git a/src/routes/authentication.js b/src/routes/authentication.js
index 81af7fe550..a4341ddf36 100644
--- a/src/routes/authentication.js
+++ b/src/routes/authentication.js
@@ -3,10 +3,10 @@
 var async = require('async');
 var passport = require('passport');
 var passportLocal = require('passport-local').Strategy;
-var nconf = require('nconf');
 var winston = require('winston');
 
 var controllers = require('../controllers');
+var helpers = require('../controllers/helpers');
 var plugins = require('../plugins');
 
 var loginStrategies = [];
@@ -88,10 +88,27 @@ Auth.reloadRoutes = function (router, callback) {
 					// passport seems to remove `req.session.returnTo` after it redirects
 					req.session.registration.returnTo = req.session.returnTo;
 					next();
-				}, passport.authenticate(strategy.name, {
-					successReturnToOrRedirect: nconf.get('relative_path') + (strategy.successUrl !== undefined ? strategy.successUrl : '/'),
-					failureRedirect: nconf.get('relative_path') + (strategy.failureUrl !== undefined ? strategy.failureUrl : '/login'),
-				}));
+				}, function (req, res, next) {
+					passport.authenticate(strategy.name, function (err, user) {
+						if (err) {
+							delete req.session.registration;
+							return next(err);
+						}
+
+						if (!user) {
+							delete req.session.registration;
+							return helpers.redirect(res, strategy.failureUrl !== undefined ? strategy.failureUrl : '/login');
+						}
+
+						req.login(user, function (err) {
+							if (err) {
+								return next(err);
+							}
+
+							helpers.redirect(res, strategy.successUrl !== undefined ? strategy.successUrl : '/');
+						});
+					})(req, res, next);
+				});
 			});
 
 			router.post('/register', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.register);
diff --git a/test/uploads.js b/test/uploads.js
index 077ce55704..578b99515d 100644
--- a/test/uploads.js
+++ b/test/uploads.js
@@ -158,14 +158,21 @@ describe('Upload Controllers', function () {
 
 		it('should fail if file is not an image', function (done) {
 			file.isFileTypeAllowed(path.join(__dirname, '../test/files/notanimage.png'), function (err) {
-				assert.equal(err.message, 'Input file is missing or of an unsupported image format');
+				assert.equal(err.message, 'Input file contains unsupported image format');
 				done();
 			});
 		});
 
 		it('should fail if file is not an image', function (done) {
 			image.size(path.join(__dirname, '../test/files/notanimage.png'), function (err) {
-				assert.equal(err.message, 'Input file is missing or of an unsupported image format');
+				assert.equal(err.message, 'Input file contains unsupported image format');
+				done();
+			});
+		});
+
+		it('should fail if file is missing', function (done) {
+			image.size(path.join(__dirname, '../test/files/doesnotexist.png'), function (err) {
+				assert.equal(err.message, 'Input file is missing');
 				done();
 			});
 		});