--- a/adm/style/acp_jabber.html
+++ b/adm/style/acp_jabber.html
@@ -39,7 +39,7 @@
 </dl>
 <dl>
 	<dt><label for="jab_password">{L_JAB_PASSWORD}{L_COLON}</label><br /><span>{L_JAB_PASSWORD_EXPLAIN}</span></dt>
-	<dd><input type="password" id="jab_password" name="jab_password" value="{JAB_PASSWORD}" /></dd>
+	<dd><input type="password" id="jab_password" name="jab_password" value="{JAB_PASSWORD}" autocomplete="off" /></dd>
 </dl>
 <!-- IF S_CAN_USE_SSL -->
 <dl>
--- a/assets/javascript/core.js
+++ b/assets/javascript/core.js
@@ -1,3 +1,5 @@
+/* global bbfontstyle */
+
 var phpbb = {};
 phpbb.alertTime = 100;
 
@@ -13,7 +15,7 @@ var keymap = {
 };
 
 var $dark = $('#darkenwrapper');
-var $loadingIndicator = $('#loading_indicator');
+var $loadingIndicator;
 var phpbbAlertTimer = null;
 
 phpbb.isTouch = (window && typeof window.ontouchstart !== 'undefined');
@@ -21,9 +23,14 @@ phpbb.isTouch = (window && typeof window.ontouchstart !== 'undefined');
 /**
  * Display a loading screen
  *
- * @returns object Returns loadingIndicator.
+ * @returns {object} Returns loadingIndicator.
  */
 phpbb.loadingIndicator = function() {
+	if (!$loadingIndicator) {
+		$loadingIndicator = $('<div />', { id: 'loading_indicator' });
+		$loadingIndicator.appendTo('#page-footer');
+	}
+
 	if (!$loadingIndicator.is(':visible')) {
 		$loadingIndicator.fadeIn(phpbb.alertTime);
 		// Wait fifteen seconds and display an error if nothing has been returned by then.
@@ -54,7 +61,7 @@ phpbb.clearLoadingTimeout = function() {
 /**
 * Close popup alert after a specified delay
 *
-* @param int Delay in ms until darkenwrapper's click event is triggered
+* @param {int} delay Delay in ms until darkenwrapper's click event is triggered
 */
 phpbb.closeDarkenWrapper = function(delay) {
 	phpbbAlertTimer = setTimeout(function() {
@@ -67,14 +74,12 @@ phpbb.closeDarkenWrapper = function(delay) {
  *
  * You can only call one alert or confirm box at any one time.
  *
- * @param string title Title of the message, eg "Information" (HTML).
- * @param string msg Message to display (HTML).
- * @param bool fadedark Remove the dark background when done? Defaults
- *     to yes.
+ * @param {string} title Title of the message, eg "Information" (HTML).
+ * @param {string} msg Message to display (HTML).
  *
- * @returns object Returns the div created.
+ * @returns {object} Returns the div created.
  */
-phpbb.alert = function(title, msg, fadedark) {
+phpbb.alert = function(title, msg) {
 	var $alert = $('#phpbb_alert');
 	$alert.find('.alert_title').html(title);
 	$alert.find('.alert_text').html(msg);
@@ -94,14 +99,14 @@ phpbb.alert = function(title, msg, fadedark) {
 /**
 * Handler for opening an alert box.
 *
-* @param jQuery $alert			jQuery object.
+* @param {jQuery} $alert jQuery object.
 */
 phpbb.alert.open = function($alert) {
 	if (!$dark.is(':visible')) {
 		$dark.fadeIn(phpbb.alertTime);
 	}
 
-	if ($loadingIndicator.is(':visible')) {
+	if ($loadingIndicator && $loadingIndicator.is(':visible')) {
 		$loadingIndicator.fadeOut(phpbb.alertTime, function() {
 			$dark.append($alert);
 			$alert.fadeIn(phpbb.alertTime);
@@ -134,8 +139,8 @@ phpbb.alert.open = function($alert) {
 /**
 * Handler for closing an alert box.
 *
-* @param jQuery $alert			jQuery object.
-* @param bool fadedark			Whether to remove dark background.
+* @param {jQuery} $alert jQuery object.
+* @param {bool} fadedark Whether to remove dark background.
 */
 phpbb.alert.close = function($alert, fadedark) {
 	var $fade = (fadedark) ? $dark : $alert;
@@ -153,13 +158,13 @@ phpbb.alert.close = function($alert, fadedark) {
  *
  * You can only call one alert or confirm box at any one time.
  *
- * @param string msg Message to display (HTML).
- * @param function callback Callback. Bool param, whether the user pressed
+ * @param {string} msg Message to display (HTML).
+ * @param {function} callback Callback. Bool param, whether the user pressed
  *     yes or no (or whatever their language is).
- * @param bool fadedark Remove the dark background when done? Defaults
+ * @param {bool} fadedark Remove the dark background when done? Defaults
  *     to yes.
  *
- * @returns object Returns the div created.
+ * @returns {object} Returns the div created.
  */
 phpbb.confirm = function(msg, callback, fadedark) {
 	var $confirmDiv = $('#phpbb_confirm');
@@ -197,8 +202,8 @@ phpbb.confirm = function(msg, callback, fadedark) {
 /**
  * Turn a querystring into an array.
  *
- * @argument string string The querystring to parse.
- * @returns object The object created.
+ * @argument {string} string The querystring to parse.
+ * @returns {object} The object created.
  */
 phpbb.parseQuerystring = function(string) {
 	var params = {}, i, split;
@@ -223,12 +228,7 @@ phpbb.parseQuerystring = function(string) {
  * For more info, view the following page on the phpBB wiki:
  * http://wiki.phpbb.com/JavaScript_Function.phpbb.ajaxify
  *
- * @param object options Options.
- * @param bool/function refresh If we are sent back a refresh, should it be
- *     acted upon? This can either be true / false / a function.
- * @param function callback Callback to call on completion of event. Has
- *     three parameters: the element that the event was evoked from, the JSON
- *     that was returned and (if it is a form) the form action.
+ * @param {object} options Options.
  */
 phpbb.ajaxify = function(options) {
 	var $elements = $(options.selector),
@@ -262,11 +262,16 @@ phpbb.ajaxify = function(options) {
 				console.log('AJAX error. status: ' + textStatus + ', message: ' + errorThrown);
 			}
 			phpbb.clearLoadingTimeout();
-			var errorText = false;
-			if (typeof errorThrown === 'string' && errorThrown.length > 0) {
+			var responseText, errorText = false;
+			try {
+				responseText = JSON.parse(jqXHR.responseText);
+				responseText = responseText.message;
+			} catch (e) {}
+			if (typeof responseText === 'string' && responseText.length > 0) {
+				errorText = responseText;
+			} else if (typeof errorThrown === 'string' && errorThrown.length > 0) {
 				errorText = errorThrown;
-			}
-			else {
+			} else {
 				errorText = $dark.attr('data-ajax-error-text-' + textStatus);
 				if (typeof errorText !== 'string' || !errorText.length) {
 					errorText = $dark.attr('data-ajax-error-text');
@@ -283,7 +288,7 @@ phpbb.ajaxify = function(options) {
 		 * It cannot be called from outside this function, and is purely here to
 		 * avoid repetition of code.
 		 *
-		 * @param object res The object sent back by the server.
+		 * @param {object} res The object sent back by the server.
 		 */
 		function returnHandler(res) {
 			var alert;
@@ -427,10 +432,10 @@ phpbb.search = {
 };
 
 /**
- * Get cached search data. 
+ * Get cached search data.
  *
- * @param string id		Search ID.
- * @return bool|object.	Cached data object. Returns false if no data exists.
+ * @param {string} id Search ID.
+ * @returns {bool|object} Cached data object. Returns false if no data exists.
  */
 phpbb.search.cache.get = function(id) {
 	if (this.data[id]) {
@@ -440,39 +445,35 @@ phpbb.search.cache.get = function(id) {
 };
 
 /**
- * Set search cache data value. 
+ * Set search cache data value.
  *
- * @param string id		Search ID.
- * @param string key	Data key.
- * @param string value	Data value.
- *
- * @return undefined
+ * @param {string} id		Search ID.
+ * @param {string} key		Data key.
+ * @param {string} value	Data value.
  */
 phpbb.search.cache.set = function(id, key, value) {
 	if (!this.data[id]) {
-		this.data[id] = {results: []};
+		this.data[id] = { results: [] };
 	}
 	this.data[id][key] = value;
 };
 
 /**
- * Cache search result. 
- *
- * @param string id			Search ID.
- * @param string keyword	Keyword.
- * @param array results		Search results.
+ * Cache search result.
  *
- * @return undefined
+ * @param {string} id		Search ID.
+ * @param {string} keyword	Keyword.
+ * @param {Array} results	Search results.
  */
-phpbb.search.cache.setResults = function(id, keyword, value) {
-	this.data[id].results[keyword] = value;
+phpbb.search.cache.setResults = function(id, keyword, results) {
+	this.data[id].results[keyword] = results;
 };
 
 /**
  * Trim spaces from keyword and lower its case.
  *
- * @param string keyword	Search keyword to clean.
- * @return string Cleaned string.
+ * @param {string} keyword Search keyword to clean.
+ * @returns {string} Cleaned string.
  */
 phpbb.search.cleanKeyword = function(keyword) {
 	return $.trim(keyword).toLowerCase();
@@ -482,11 +483,11 @@ phpbb.search.cleanKeyword = function(keyword) {
  * Get clean version of search keyword. If textarea supports several keywords
  * (one per line), it fetches the current keyword based on the caret position.
  *
- * @param jQuery $input		Search input|textarea.
- * @param string keyword	Input|textarea value.
- * @param bool multiline	Whether textarea supports multiple search keywords.
+ * @param {jQuery} $input	Search input|textarea.
+ * @param {string} keyword	Input|textarea value.
+ * @param {bool} multiline	Whether textarea supports multiple search keywords.
  *
- * @return string Clean string.
+ * @returns string Clean string.
  */
 phpbb.search.getKeyword = function($input, keyword, multiline) {
 	if (multiline) {
@@ -498,10 +499,10 @@ phpbb.search.getKeyword = function($input, keyword, multiline) {
 
 /**
  * Get the textarea line number on which the keyword resides - for textareas
- * that support multiple keywords (one per line). 
+ * that support multiple keywords (one per line).
  *
- * @param jQuery $textarea	Search textarea.
- * @return int
+ * @param {jQuery} $textarea Search textarea.
+ * @returns {int} The line number.
  */
 phpbb.search.getKeywordLine = function ($textarea) {
 	var selectionStart = $textarea.get(0).selectionStart;
@@ -512,11 +513,9 @@ phpbb.search.getKeywordLine = function ($textarea) {
  * Set the value on the input|textarea. If textarea supports multiple
  * keywords, only the active keyword is replaced.
  *
- * @param jQuery $input		Search input|textarea.
- * @param string value		Value to set.
- * @param bool multiline	Whether textarea supports multiple search keywords.	
- *
- * @return undefined
+ * @param {jQuery} $input	Search input|textarea.
+ * @param {string} value	Value to set.
+ * @param {bool} multiline	Whether textarea supports multiple search keywords.
  */
 phpbb.search.setValue = function($input, value, multiline) {
 	if (multiline) {
@@ -529,14 +528,13 @@ phpbb.search.setValue = function($input, value, multiline) {
 };
 
 /**
- * Sets the onclick event to set the value on the input|textarea to the selected search result. 
- *
- * @param jQuery $input		Search input|textarea.
- * @param object value		Result object.
- * @param jQuery $row		Result element.
- * @param jQuery $container	jQuery object for the search container.
+ * Sets the onclick event to set the value on the input|textarea to the
+ * selected search result.
  *
- * @return undefined
+ * @param {jQuery} $input		Search input|textarea.
+ * @param {object} value		Result object.
+ * @param {jQuery} $row			Result element.
+ * @param {jQuery} $container	jQuery object for the search container.
  */
 phpbb.search.setValueOnClick = function($input, value, $row, $container) {
 	$row.click(function() {
@@ -551,16 +549,16 @@ phpbb.search.setValueOnClick = function($input, value, $row, $container) {
  * already, those are displayed instead. Executes the AJAX request function
  * itself due to the need to use a timeout to limit the number of requests.
  *
- * @param array data			Data to be sent to the server.
- * @param object event			Onkeyup event object.
- * @param function sendRequest	Function to execute AJAX request.
+ * @param {Array} data				Data to be sent to the server.
+ * @param {object} event			Onkeyup event object.
+ * @param {function} sendRequest	Function to execute AJAX request.
  *
- * @return bool Returns false.
+ * @returns {bool} Returns false.
  */
 phpbb.search.filter = function(data, event, sendRequest) {
 	var $this = $(this),
 		dataName = ($this.attr('data-name') !== undefined) ? $this.attr('data-name') : $this.attr('name'),
-		minLength = parseInt($this.attr('data-min-length')),
+		minLength = parseInt($this.attr('data-min-length'), 10),
 		searchID = $this.attr('data-results'),
 		keyword = phpbb.search.getKeyword($this, data[dataName], $this.attr('data-multiline')),
 		cache = phpbb.search.cache.get(searchID),
@@ -582,7 +580,10 @@ phpbb.search.filter = function(data, event, sendRequest) {
 			} else {
 				// Do we already have results for this?
 				if (cache.results[keyword]) {
-					var response = {keyword: keyword, results: cache.results[keyword]};
+					var response = {
+						keyword: keyword,
+						results: cache.results[keyword]
+					};
 					phpbb.search.handleResponse(response, $this, true);
 					proceed = false;
 				}
@@ -593,8 +594,8 @@ phpbb.search.filter = function(data, event, sendRequest) {
 					phpbb.search.cache.set(searchID, 'lastSearch', keyword);
 					phpbb.search.cache.setResults(searchID, keyword, []);
 					proceed = false;
-				}		
-			}		
+				}
+			}
 		}
 
 		if (proceed) {
@@ -607,14 +608,12 @@ phpbb.search.filter = function(data, event, sendRequest) {
 };
 
 /**
- * Handle search result response. 
+ * Handle search result response.
  *
- * @param object res		Data received from server.
- * @param jQuery $input		Search input|textarea.
- * @param bool fromCache	Whether the results are from the cache.
- * @param function callback	Optional callback to run when assigning each search result.
- *
- * @return undefined
+ * @param {object} res			Data received from server.
+ * @param {jQuery} $input		Search input|textarea.
+ * @param {bool} fromCache		Whether the results are from the cache.
+ * @param {function} callback	Optional callback to run when assigning each search result.
  */
 phpbb.search.handleResponse = function(res, $input, fromCache, callback) {
 	if (typeof res !== 'object') {
@@ -641,12 +640,10 @@ phpbb.search.handleResponse = function(res, $input, fromCache, callback) {
 /**
  * Show search results.
  *
- * @param array results		Search results.
- * @param jQuery $input		Search input|textarea.
- * @param jQuery $container	Search results container element.
- * @param function callback	Optional callback to run when assigning each search result.
- *
- * @return undefined
+ * @param {Array} results		Search results.
+ * @param {jQuery} $input		Search input|textarea.
+ * @param {jQuery} $container	Search results container element.
+ * @param {function} callback	Optional callback to run when assigning each search result.
  */
 phpbb.search.showResults = function(results, $input, $container, callback) {
 	var $resultContainer = $('.search-results', $container);
@@ -683,8 +680,7 @@ phpbb.search.showResults = function(results, $input, $container, callback) {
 /**
  * Clear search results.
  *
- * @param jQuery $container	Search results container.
- * @return undefined
+ * @param {jQuery} $container Search results container.
  */
 phpbb.search.clearResults = function($container) {
 	$container.children(':not(.search-result-tpl)').remove();
@@ -703,8 +699,8 @@ phpbb.history = {};
 /**
 * Check whether a method in the native history object is supported.
 *
-* @param string fn	Method name.
-* @return bool Returns true if the method is supported.
+* @param {string} fn Method name.
+* @returns {bool} Returns true if the method is supported.
 */
 phpbb.history.isSupported = function(fn) {
 	return !(typeof history === 'undefined' || typeof history[fn] === 'undefined');
@@ -714,12 +710,10 @@ phpbb.history.isSupported = function(fn) {
 * Wrapper for the pushState and replaceState methods of the
 * native history object.
 *
-* @param string mode	Mode. Either push or replace.
-* @param string url	New URL.
-* @param string title Optional page title.
-* @patam object obj	Optional state object.
-*
-* @return undefined
+* @param {string} mode		Mode. Either push or replace.
+* @param {string} url		New URL.
+* @param {string} [title]	Optional page title.
+* @param {object} [obj]		Optional state object.
 */
 phpbb.history.alterUrl = function(mode, url, title, obj) {
 	var fn = mode + 'State';
@@ -740,11 +734,9 @@ phpbb.history.alterUrl = function(mode, url, title, obj) {
 /**
 * Wrapper for the native history.replaceState method.
 *
-* @param string url	New URL.
-* @param string title Optional page title.
-* @patam object obj	Optional state object.
-*
-* @return undefined
+* @param {string} url		New URL.
+* @param {string} [title]	Optional page title.
+* @param {object} [obj]		Optional state object.
 */
 phpbb.history.replaceUrl = function(url, title, obj) {
 	phpbb.history.alterUrl('replace', url, title, obj);
@@ -753,11 +745,9 @@ phpbb.history.replaceUrl = function(url, title, obj) {
 /**
 * Wrapper for the native history.pushState method.
 *
-* @param string url	New URL.
-* @param string title Optional page title.
-* @patam object obj	Optional state object.
-*
-* @return undefined
+* @param {string} url		New URL.
+* @param {string} [title]	Optional page title.
+* @param {object} [obj]		Optional state object.
 */
 phpbb.history.pushUrl = function(url, title, obj) {
 	phpbb.history.alterUrl('push', url, title, obj);
@@ -766,7 +756,8 @@ phpbb.history.pushUrl = function(url, title, obj) {
 /**
 * Hide the optgroups that are not the selected timezone
 *
-* @param	bool	keepSelection		Shall we keep the value selected, or shall the user be forced to repick one.
+* @param {bool} keepSelection Shall we keep the value selected, or shall the
+* 	user be forced to repick one.
 */
 phpbb.timezoneSwitchDate = function(keepSelection) {
 	var $timezoneCopy = $('#timezone_copy');
@@ -789,7 +780,7 @@ phpbb.timezoneSwitchDate = function(keepSelection) {
 	}
 
 	if ($tzDate.val() !== '') {
-		$timezone.children('optgroup').remove(':not([data-tz-value="' + $('#tz_date').val() + '"])');
+		$timezone.children('optgroup').remove(':not([data-tz-value="' + $tzDate.val() + '"])');
 	}
 
 	if ($tzDate.val() === $tzSelectDateSuggest.attr('data-suggested-tz')) {
@@ -797,7 +788,7 @@ phpbb.timezoneSwitchDate = function(keepSelection) {
 	} else {
 		$tzSelectDateSuggest.css('display', 'inline');
 	}
-	
+
 	var $tzOptions = $timezone.children('optgroup[data-tz-value="' + $tzDate.val() + '"]').children('option');
 
 	if ($tzOptions.length === 1) {
@@ -824,7 +815,7 @@ phpbb.timezoneEnableDateSelection = function() {
 /**
 * Preselect a date/time or suggest one, if it is not picked.
 *
-* @param	bool	forceSelector		Shall we select the suggestion?
+* @param {bool} forceSelector Shall we select the suggestion?
 */
 phpbb.timezonePreselectSelect = function(forceSelector) {
 
@@ -893,8 +884,8 @@ phpbb.ajaxCallbacks = {};
  *
  * See the phpbb.ajaxify comments for information on stuff like parameters.
  *
- * @param string id The name of the callback.
- * @param function callback The callback to be called.
+ * @param {string} id The name of the callback.
+ * @param {function} callback The callback to be called.
  */
 phpbb.addAjaxCallback = function(id, callback) {
 	if (typeof callback === 'function') {
@@ -985,7 +976,7 @@ phpbb.addAjaxCallback('toggle_link', function() {
 * types text.
 *
 * @param {jQuery} $items jQuery object(s) to resize
-* @param {object} options Optional parameter that adjusts default
+* @param {object} [options] Optional parameter that adjusts default
 * 	configuration. See configuration variable
 *
 * Optional parameters:
@@ -1022,15 +1013,19 @@ phpbb.resizeTextArea = function($items, options) {
 	function resetAutoResize(item) {
 		var $item = $(item);
 		if ($item.hasClass('auto-resized')) {
-			$(item).css({height: '', resize: ''}).removeClass('auto-resized');
+			$(item)
+				.css({ height: '', resize: '' })
+				.removeClass('auto-resized');
 			configuration.resetCallback.call(item, $item);
 		}
 	}
 
 	function autoResize(item) {
 		function setHeight(height) {
-			height += parseInt($item.css('height')) - $item.height();
-			$item.css({height: height + 'px', resize: 'none'}).addClass('auto-resized');
+			height += parseInt($item.css('height'), 10) - $item.height();
+			$item
+				.css({ height: height + 'px', resize: 'none' })
+				.addClass('auto-resized');
 			configuration.resizeCallback.call(item, $item);
 		}
 
@@ -1046,7 +1041,7 @@ phpbb.resizeTextArea = function($items, options) {
 				configuration.maxHeight
 			),
 			$item = $(item),
-			height = parseInt($item.height()),
+			height = parseInt($item.height(), 10),
 			scrollHeight = (item.scrollHeight) ? item.scrollHeight : 0;
 
 		if (height < 0) {
@@ -1055,8 +1050,7 @@ phpbb.resizeTextArea = function($items, options) {
 
 		if (height > maxHeight) {
 			setHeight(maxHeight);
-		}
-		else if (scrollHeight > (height + 5)) {
+		} else if (scrollHeight > (height + 5)) {
 			setHeight(Math.min(maxHeight, scrollHeight));
 		}
 	}
@@ -1085,7 +1079,7 @@ phpbb.resizeTextArea = function($items, options) {
 * @param {Array} endTags List of end tags to look for
 *		For example, Array('[/code]')
 *
-* @return {boolean} True if cursor is in bbcode tag
+* @returns {boolean} True if cursor is in bbcode tag
 */
 phpbb.inBBCodeTag = function(textarea, startTags, endTags) {
 	var start = textarea.selectionStart,
@@ -1157,7 +1151,7 @@ phpbb.applyCodeEditor = function(textarea) {
 	* @param {boolean} stripCodeStart If true, only part of line
 	*		after [code] tag will be returned.
 	*
-	* @return {string} Line of text
+	* @returns {string} Line of text
 	*/
 	function getLastLine(stripCodeStart) {
 		var start = textarea.selectionStart,
@@ -1173,7 +1167,7 @@ phpbb.applyCodeEditor = function(textarea) {
 					var tagLength = startTags[i].length;
 
 					value = value.substring(index + tagLength);
-					if (startTags[i].lastIndexOf(startTagsEnd) != tagLength) {
+					if (startTags[i].lastIndexOf(startTagsEnd) !== tagLength) {
 						index = value.indexOf(startTagsEnd);
 
 						if (index >= 0) {
@@ -1190,7 +1184,7 @@ phpbb.applyCodeEditor = function(textarea) {
 	/**
 	* Append text at cursor position
 	*
-	* @param {string} Text Text to append
+	* @param {string} text Text to append
 	*/
 	function appendText(text) {
 		var start = textarea.selectionStart,
@@ -1233,6 +1227,31 @@ phpbb.applyCodeEditor = function(textarea) {
 };
 
 /**
+ * Show drag and drop animation when textarea is present
+ *
+ * This function will enable the drag and drop animation for a specified
+ * textarea.
+ *
+ * @param {HTMLElement} textarea Textarea DOM object to apply editor to
+ */
+phpbb.showDragNDrop = function(textarea) {
+	if (!textarea) {
+		return;
+	}
+
+	$('body').on('dragenter dragover', function () {
+		$(textarea).addClass('drag-n-drop');
+	}).on('dragleave dragout dragend drop', function() {
+		$(textarea).removeClass('drag-n-drop');
+	});
+	$(textarea).on('dragenter dragover', function () {
+		$(textarea).addClass('drag-n-drop-highlight');
+	}).on('dragleave dragout dragend drop', function() {
+		$(textarea).removeClass('drag-n-drop-highlight');
+	});
+};
+
+/**
 * List of classes that toggle dropdown menu,
 * list of classes that contain visible dropdown menu
 *
@@ -1324,12 +1343,15 @@ phpbb.toggleDropdown = function() {
 					fullFreeSpace = freeSpace + parent.outerWidth();
 
 				options.dropdown.find('.dropdown-contents').each(function() {
-					contentWidth = parseInt($(this).outerWidth());
-					$(this).css({marginLeft: 0, left: 0});
+					contentWidth = parseInt($(this).outerWidth(), 10);
+					$(this).css({ marginLeft: 0, left: 0 });
 				});
 
 				var maxOffset = Math.min(contentWidth, fullFreeSpace) + 'px';
-				options.dropdown.css({'width': maxOffset, 'margin-left': '-' + maxOffset});
+				options.dropdown.css({
+					width: maxOffset,
+					marginLeft: -maxOffset
+				});
 			}
 		} else {
 			options.dropdown.css('margin-right', '-' + (windowWidth + freeSpace) + 'px');
@@ -1391,14 +1413,12 @@ phpbb.registerDropdown = function(toggle, dropdown, options) {
 /**
 * Get the HTML for a color palette table.
 *
-* @param string dir Palette direction - either v or h
-* @param int width Palette cell width.
-* @param int height Palette cell height.
+* @param {string} dir Palette direction - either v or h
+* @param {int} width Palette cell width.
+* @param {int} height Palette cell height.
 */
 phpbb.colorPalette = function(dir, width, height) {
-	var r = 0, 
-		g = 0, 
-		b = 0,
+	var r, g, b,
 		numberList = new Array(6),
 		color = '',
 		html = '';
@@ -1409,32 +1429,33 @@ phpbb.colorPalette = function(dir, width, height) {
 	numberList[3] = 'BF';
 	numberList[4] = 'FF';
 
-	var tableClass = (dir == 'h') ? 'horizontal-palette' : 'vertical-palette';
+	var tableClass = (dir === 'h') ? 'horizontal-palette' : 'vertical-palette';
 	html += '<table class="not-responsive colour-palette ' + tableClass + '" style="width: auto;">';
 
 	for (r = 0; r < 5; r++) {
-		if (dir == 'h') {
+		if (dir === 'h') {
 			html += '<tr>';
 		}
 
 		for (g = 0; g < 5; g++) {
-			if (dir == 'v') {
+			if (dir === 'v') {
 				html += '<tr>';
 			}
 
 			for (b = 0; b < 5; b++) {
-				color = String(numberList[r]) + String(numberList[g]) + String(numberList[b]);
-				html += '<td style="background-color: #' + color + '; width: ' + width + 'px; height: ' + height + 'px;">';
-				html += '<a href="#" data-color="' + color + '" style="display: block; width: ' + width + 'px; height: ' + height + 'px; " alt="#' + color + '" title="#' + color + '"></a>';
+				color = '' + numberList[r] + numberList[g] + numberList[b];
+				html += '<td style="background-color: #' + color + '; width: ' + width + 'px; height: ' +
+					height + 'px;"><a href="#" data-color="' + color + '" style="display: block; width: ' +
+					width + 'px; height: ' + height + 'px; " alt="#' + color + '" title="#' + color + '"></a>';
 				html += '</td>';
 			}
 
-			if (dir == 'v') {
+			if (dir === 'v') {
 				html += '</tr>';
 			}
 		}
 
-		if (dir == 'h') {
+		if (dir === 'h') {
 			html += '</tr>';
 		}
 	}
@@ -1445,7 +1466,7 @@ phpbb.colorPalette = function(dir, width, height) {
 /**
 * Register a color palette.
 *
-* @param object el jQuery object for the palette container.
+* @param {jQuery} el jQuery object for the palette container.
 */
 phpbb.registerPalette = function(el) {
 	var	orientation	= el.attr('data-orientation'),
@@ -1474,15 +1495,15 @@ phpbb.registerPalette = function(el) {
 		}
 		e.preventDefault();
 	});
-}
+};
 
 /**
 * Set display of page element
 *
-* @param string	id	The ID of the element to change
-* @param int	action	Set to 0 if element display should be toggled, -1 for
+* @param {string} id The ID of the element to change
+* @param {int} action Set to 0 if element display should be toggled, -1 for
 *			hiding the element, and 1 for showing it.
-* @param string	type	Display type that should be used, e.g. inline, block or
+* @param {string} type Display type that should be used, e.g. inline, block or
 *			other CSS "display" types
 */
 phpbb.toggleDisplay = function(id, action, type) {
@@ -1503,8 +1524,7 @@ phpbb.toggleDisplay = function(id, action, type) {
 * Toggle additional settings based on the selected
 * option of select element.
 *
-* @param jQuery el jQuery select element object.
-* @return undefined
+* @param {jQuery} el jQuery select element object.
 */
 phpbb.toggleSelectSettings = function(el) {
 	el.children().each(function() {
@@ -1518,11 +1538,11 @@ phpbb.toggleSelectSettings = function(el) {
 * Get function from name.
 * Based on http://stackoverflow.com/a/359910
 *
-* @param string functionName Function to get.
-* @return function
+* @param {string} functionName Function to get.
+* @returns function
 */
 phpbb.getFunctionByName = function (functionName) {
- 	var namespaces = functionName.split('.'),
+	var namespaces = functionName.split('.'),
 		func = namespaces.pop(),
 		context = window;
 
--- a/assets/javascript/editor.js
+++ b/assets/javascript/editor.js
@@ -159,7 +159,7 @@ function insert_text(text, spaces, popup) {
 /**
 * Add inline attachment at position
 */
-function attach_inline(index, filename) {
+function attachInline(index, filename) {
 	insert_text('[attachment=' + index + ']' + filename + '[/attachment]');
 	document.forms[form_name].elements[text_name].focus();
 }
@@ -355,6 +355,9 @@ function getCaretPosition(txtarea) {
 		textarea = doc.forms[form_name].elements[text_name];
 
 		phpbb.applyCodeEditor(textarea);
+		if ($('#attach-panel').length) {
+			phpbb.showDragNDrop(textarea);
+		}
 	});
 })(jQuery);
 
--- a/assets/javascript/plupload.js
+++ b/assets/javascript/plupload.js
@@ -1,26 +1,26 @@
+/* global phpbb, plupload, attachInline */
+
 plupload.addI18n(phpbb.plupload.i18n);
 phpbb.plupload.ids = [];
 
 (function($) {  // Avoid conflicts with other libraries
 
-"use strict";
+'use strict';
 
 /**
  * Set up the uploader.
- *
- * @return undefined
  */
 phpbb.plupload.initialize = function() {
 	// Initialize the Plupload uploader.
-	uploader.init();
+	phpbb.plupload.uploader.init();
 
 	// Set attachment data.
 	phpbb.plupload.setData(phpbb.plupload.data);
 	phpbb.plupload.updateMultipartParams(phpbb.plupload.getSerializedData());
 
 	// Only execute if Plupload initialized successfully.
-	uploader.bind('Init', function() {
-		phpbb.plupload.form = $(phpbb.plupload.config.form_hook)[0],
+	phpbb.plupload.uploader.bind('Init', function() {
+		phpbb.plupload.form = $(phpbb.plupload.config.form_hook)[0];
 		phpbb.plupload.rowTpl = $('#attach-row-tpl')[0].outerHTML;
 
 		// Hide the basic upload panel and remove the attach row template.
@@ -29,18 +29,18 @@ phpbb.plupload.initialize = function() {
 		$('#attach-panel-multi').show();
 	});
 
-	uploader.bind('PostInit', function() {
+	phpbb.plupload.uploader.bind('PostInit', function() {
 		// Point out the drag-and-drop zone if it's supported.
-		if (uploader.features.dragdrop) {
+		if (phpbb.plupload.uploader.features.dragdrop) {
 			$('#drag-n-drop-message').show();
 		}
 
 		// Ensure "Add files" button position is correctly calculated.
 		if ($('#attach-panel-multi').is(':visible')) {
-			uploader.refresh();
+			phpbb.plupload.uploader.refresh();
 		}
 		$('[data-subpanel="attach-panel"]').one('click', function() {
-			uploader.refresh();
+			phpbb.plupload.uploader.refresh();
 		});
 	});
 };
@@ -48,37 +48,32 @@ phpbb.plupload.initialize = function() {
 /**
  * Unsets all elements in the object uploader.settings.multipart_params whose keys
  * begin with 'attachment_data['
- *
- * @return undefined
  */
 phpbb.plupload.clearParams = function() {
-	var obj = uploader.settings.multipart_params;
+	var obj = phpbb.plupload.uploader.settings.multipart_params;
 	for (var key in obj) {
 		if (!obj.hasOwnProperty(key) || key.indexOf('attachment_data[') !== 0) {
 			continue;
 		}
 
-		delete uploader.settings.multipart_params[key];
+		delete phpbb.plupload.uploader.settings.multipart_params[key];
 	}
 };
 
 /**
  * Update uploader.settings.multipart_params object with new data.
  *
- * @param object obj
- * @return undefined
+ * @param {object} obj
  */
 phpbb.plupload.updateMultipartParams = function(obj) {
-	uploader.settings.multipart_params = $.extend(
-		uploader.settings.multipart_params,
-		obj
-	);
+	var settings = phpbb.plupload.uploader.settings;
+	settings.multipart_params = $.extend(settings.multipart_params, obj);
 };
 
 /**
  * Convert the array of attachment objects into an object that PHP would expect as POST data.
  *
- * @return object An object in the form 'attachment_data[i][key]': value as
+ * @returns {object} An object in the form 'attachment_data[i][key]': value as
  * 	expected by the server
  */
 phpbb.plupload.getSerializedData = function() {
@@ -100,22 +95,19 @@ phpbb.plupload.getSerializedData = function() {
  * Get the index from the phpbb.plupload.data array where the given
  * attachment id appears.
  *
- * @param int attach_id The attachment id of the file.
- * @return bool	Returns false if the id cannot be found.
- * @return int	Returns the index of the file if it exists.
+ * @param {int} attachId The attachment id of the file.
+ * @returns {bool|int} Index of the file if exists, otherwise false.
  */
-phpbb.plupload.getIndex = function(attach_id) {
-	var index = $.inArray(Number(attach_id), phpbb.plupload.ids);
+phpbb.plupload.getIndex = function(attachId) {
+	var index = $.inArray(Number(attachId), phpbb.plupload.ids);
 	return (index !== -1) ? index : false;
 };
 
 /**
  * Set the data in phpbb.plupload.data and phpbb.plupload.ids arrays.
- * 
- * @param array data	Array containing the new data to use. In the form of 
- * array(index => object(property: value). Requires attach_id to be one of the object properties.
  *
- * @return undefined
+ * @param {Array} data	Array containing the new data to use. In the form of
+ * array(index => object(property: value). Requires attach_id to be one of the object properties.
  */
 phpbb.plupload.setData = function(data) {
 	// Make sure that the array keys are reset.
@@ -129,12 +121,11 @@ phpbb.plupload.setData = function(data) {
 
 /**
  * Update the attachment data in the HTML and the phpbb & phpbb.plupload objects.
- * 
- * @param array data		Array containing the new data to use.
- * @param string action		The action that required the update. Used to update the inline attachment bbcodes.
- * @param int index			The index from phpbb.plupload_ids that was affected by the action.
- * @param array downloadUrl	Optional array of download urls to update.
- * @return undefined
+ *
+ * @param {Array} data			Array containing the new data to use.
+ * @param {string} action		The action that required the update. Used to update the inline attachment bbcodes.
+ * @param {int} index			The index from phpbb.plupload_ids that was affected by the action.
+ * @param {Array} downloadUrl	Optional array of download urls to update.
  */
 phpbb.plupload.update = function(data, action, index, downloadUrl) {
 
@@ -147,9 +138,8 @@ phpbb.plupload.update = function(data, action, index, downloadUrl) {
 
 /**
  * Update the relevant elements and hidden data for all attachments.
- * 
- * @param array downloadUrl	Optional array of download urls to update.
- * @return undefined
+ *
+ * @param {Array} downloadUrl Optional array of download urls to update.
  */
 phpbb.plupload.updateRows = function(downloadUrl) {
 	for (var i = 0; i < phpbb.plupload.ids.length; i++) {
@@ -162,9 +152,8 @@ phpbb.plupload.updateRows = function(downloadUrl) {
  * using the id "attach-row-tpl" to be present. This snippet is cloned and the
  * data for the file inserted into it. The row is then appended or prepended to
  * #file-list based on the attach_order setting.
- * 
- * @param object file	Plupload file object for the new attachment.
- * @return undefined
+ *
+ * @param {object} file Plupload file object for the new attachment.
  */
 phpbb.plupload.insertRow = function(file) {
 	var row = $(phpbb.plupload.rowTpl);
@@ -173,7 +162,7 @@ phpbb.plupload.insertRow = function(file) {
 	row.find('.file-name').html(plupload.xmlEncode(file.name));
 	row.find('.file-size').html(plupload.formatSize(file.size));
 
-	if (phpbb.plupload.order == 'desc') {
+	if (phpbb.plupload.order === 'desc') {
 		$('#file-list').prepend(row);
 	} else {
 		$('#file-list').append(row);
@@ -182,10 +171,9 @@ phpbb.plupload.insertRow = function(file) {
 
 /**
  * Update the relevant elements and hidden data for an attachment.
- * 
- * @param int index	The index from phpbb.plupload.ids of the attachment to edit.
- * @param array downloadUrl	Optional array of download urls to update. 
- * @return undefined
+ *
+ * @param {int} index The index from phpbb.plupload.ids of the attachment to edit.
+ * @param {Array} downloadUrl Optional array of download urls to update.
  */
 phpbb.plupload.updateRow = function(index, downloadUrl) {
 	var attach = phpbb.plupload.data[index],
@@ -197,7 +185,7 @@ phpbb.plupload.updateRow = function(index, downloadUrl) {
 			link = $('<a></a>');
 
 		link.attr('href', url).html(attach.real_filename);
-		row.find('.file-name').html(link)	
+		row.find('.file-name').html(link);
 	}
 
 	row.find('textarea').attr('name', 'comment_list[' + index + ']');
@@ -207,18 +195,21 @@ phpbb.plupload.updateRow = function(index, downloadUrl) {
 /**
  * Update hidden input data for an attachment.
  *
- * @param object row	jQuery object for the attachment row.
- * @param object attach	Attachment data object from phpbb.plupload.data
- * @param int index		Attachment index from phpbb.plupload.ids
- * @return undefined
+ * @param {object} row		jQuery object for the attachment row.
+ * @param {object} attach	Attachment data object from phpbb.plupload.data
+ * @param {int} index		Attachment index from phpbb.plupload.ids
  */
 phpbb.plupload.updateHiddenData = function(row, attach, index) {
 	row.find('input[type="hidden"]').remove();
 
 	for (var key in attach) {
+		if (!attach.hasOwnProperty(key)) {
+			return;
+		}
+
 		var input = $('<input />')
 			.attr('type', 'hidden')
-			.attr('name', 'attachment_data[' + index + '][' + key +']')
+			.attr('name', 'attachment_data[' + index + '][' + key + ']')
 			.attr('value', attach[key]);
 		$('textarea', row).after(input);
 	}
@@ -230,16 +221,14 @@ phpbb.plupload.updateHiddenData = function(row, attach, index) {
  * responds with the updated attachment data list so that any future
  * uploads can maintain state with the server
  *
- * @param object row	jQuery object for the attachment row.
- * @param int attachId	Attachment id of the file to be removed.
- *
- * @return undefined
+ * @param {object} row		jQuery object for the attachment row.
+ * @param {int} attachId	Attachment id of the file to be removed.
  */
 phpbb.plupload.deleteFile = function(row, attachId) {
 	// If there's no attach id, then the file hasn't been uploaded. Simply delete the row.
 	if (typeof attachId === 'undefined') {
-		var file = uploader.getFile(row.attr('id'));
-		uploader.removeFile(file);
+		var file = phpbb.plupload.uploader.getFile(row.attr('id'));
+		phpbb.plupload.uploader.removeFile(file);
 
 		row.slideUp(100, function() {
 			row.remove();
@@ -267,7 +256,7 @@ phpbb.plupload.deleteFile = function(row, attachId) {
 
 		// trigger_error() was called which likely means a permission error was encountered.
 		if (typeof response.title !== 'undefined') {
-			uploader.trigger('Error', {message: response.message});
+			phpbb.plupload.uploader.trigger('Error', { message: response.message });
 			// We will have to assume that the deletion failed. So leave the file status as uploaded.
 			row.find('.file-status').toggleClass('file-uploaded');
 
@@ -278,21 +267,21 @@ phpbb.plupload.deleteFile = function(row, attachId) {
 		phpbb.plupload.handleMaxFilesReached();
 
 		if (row.attr('id')) {
-			var file = uploader.getFile(row.attr('id'));
-			uploader.removeFile(file);
+			var file = phpbb.plupload.uploader.getFile(row.attr('id'));
+			phpbb.plupload.uploader.removeFile(file);
 		}
 		row.slideUp(100, function() {
 			row.remove();
 			// Hide the file list if it's empty now.
 			phpbb.plupload.hideEmptyList();
 		});
-		uploader.trigger('FilesRemoved');
+		phpbb.plupload.uploader.trigger('FilesRemoved');
 	};
 
 	$.ajax(phpbb.plupload.config.url, {
 		type: 'POST',
 		data: $.extend(fields, phpbb.plupload.getSerializedData()),
-		headers: {'X-PHPBB-USING-PLUPLOAD': '1', 'X-Requested-With': 'XMLHttpRequest'}
+		headers: { 'X-PHPBB-USING-PLUPLOAD': '1', 'X-Requested-With': 'XMLHttpRequest' }
 	})
 	.always(always)
 	.done(done);
@@ -300,25 +289,21 @@ phpbb.plupload.deleteFile = function(row, attachId) {
 
 /**
  * Check the attachment list and hide its container if it's empty.
- *
- * @return undefined
  */
 phpbb.plupload.hideEmptyList = function() {
 	if (!$('#file-list').children().length) {
 		$('#file-list-container').slideUp(100);
 	}
-}
+};
 
 /**
- * Update the indices used in inline attachment bbcodes. This ensures that the bbcodes
- * correspond to the correct file after a file is added or removed. This should be called 
- * before the phpbb.plupload,data and phpbb.plupload.ids arrays are updated, otherwise it will
- * not work correctly.
+ * Update the indices used in inline attachment bbcodes. This ensures that the
+ * bbcodes correspond to the correct file after a file is added or removed.
+ * This should be called before the phpbb.plupload,data and phpbb.plupload.ids
+ * arrays are updated, otherwise it will not work correctly.
  *
- * @param string action	The action that occurred -- either "addition" or "removal"
- * @param int index		The index of the attachment from phpbb.plupload.ids that was affected.
- *
- * @return undefined
+ * @param {string} action	The action that occurred -- either "addition" or "removal"
+ * @param {int} index		The index of the attachment from phpbb.plupload.ids that was affected.
  */
 phpbb.plupload.updateBbcode = function(action, index) {
 	var	textarea = $('#message', phpbb.plupload.form),
@@ -330,64 +315,59 @@ phpbb.plupload.updateBbcode = function(action, index) {
 		return;
 	}
 
-	// Private function used to replace the bbcode.
-	var updateBbcode = function(match, fileName) {
-		// Remove the bbcode if the file was removed.
-		if (removal && index === i) {
-			return '';
-		}
-		var newIndex = i + ((removal) ? -1 : 1);
-		return '[attachment=' + newIndex +']' + fileName + '[/attachment]';
-	};
-
-	// Private function used to generate search regexp
-	var searchRegexp = function(index) {
-		return new RegExp('\\[attachment=' + index + '\\](.*?)\\[\\/attachment\\]', 'g');
+	function runUpdate(i) {
+		var regex = new RegExp('\\[attachment=' + i + '\\](.*?)\\[\\/attachment\\]', 'g');
+		text = text.replace(regex, function updateBbcode(_, fileName) {
+			// Remove the bbcode if the file was removed.
+			if (removal && index === i) {
+				return '';
+			}
+			var newIndex = i + ((removal) ? -1 : 1);
+			return '[attachment=' + newIndex + ']' + fileName + '[/attachment]';
+		});
 	}
-	// The update order of the indices is based on the action taken to ensure that we don't corrupt
-	// the bbcode index by updating it several times as we move through the loop.
-	// Removal loop starts at the removed index and moves to the end of the array.
-	// Addition loop starts at the end of the array and moves to the added index at 0.
-	var searchLoop = function() {
-		if (typeof i === 'undefined') {
-			i = (removal) ? index : phpbb.plupload.ids.length - 1;
+
+	// Loop forwards when removing and backwards when adding ensures we don't
+	// corrupt the bbcode index.
+	var i;
+	if (removal) {
+		for (i = index; i < phpbb.plupload.ids.length; i++) {
+			runUpdate(i);
+		}
+	} else {
+		for (i = phpbb.plupload.ids.length - 1; i >= index; i--) {
+			runUpdate(i);
 		}
-		return (removal) ? (i < phpbb.plupload.ids.length): (i >= index);
 	}
-	var i;
 
-	while (searchLoop()) {
-		text = text.replace(searchRegexp(i), updateBbcode);
-		(removal) ? i++ : i--;
-	}
 	textarea.val(text);
 };
 
 /**
  * Get Plupload file objects based on their upload status.
  *
- * @param int status Plupload status - plupload.DONE, plupload.FAILED, plupload.QUEUED,
- * plupload.STARTED, plupload.STOPPED
+ * @param {int} status Plupload status - plupload.DONE, plupload.FAILED,
+ * plupload.QUEUED, plupload.STARTED, plupload.STOPPED
  *
- * @return Returns an array of the Plupload file objects matching the status.
+ * @returns {Array} The Plupload file objects matching the status.
  */
 phpbb.plupload.getFilesByStatus = function(status) {
 	var files = [];
 
-	$.each(uploader.files, function(i, file) {
+	$.each(phpbb.plupload.uploader.files, function(i, file) {
 		if (file.status === status) {
 			files.push(file);
 		}
 	});
 	return files;
-}
+};
 
 /**
  * Check whether the user has reached the maximun number of files that he's allowed
  * to upload. If so, disables the uploader and marks the queued files as failed. Otherwise
  * makes sure that the uploader is enabled.
  *
- * @return bool Returns true if the limit has been reached. False if otherwise.
+ * @returns {bool} True if the limit has been reached. False if otherwise.
  */
 phpbb.plupload.handleMaxFilesReached = function() {
 	// If there is no limit, the user is an admin or moderator.
@@ -400,41 +380,36 @@ phpbb.plupload.handleMaxFilesReached = function() {
 		phpbb.plupload.markQueuedFailed(phpbb.plupload.lang.TOO_MANY_ATTACHMENTS);
 		// Disable the uploader.
 		phpbb.plupload.disableUploader();
-		uploader.trigger('Error', {message: phpbb.plupload.lang.TOO_MANY_ATTACHMENTS});
+		phpbb.plupload.uploader.trigger('Error', { message: phpbb.plupload.lang.TOO_MANY_ATTACHMENTS });
 
 		return true;
-	} else if(phpbb.plupload.maxFiles > phpbb.plupload.ids.length) {
+	} else if (phpbb.plupload.maxFiles > phpbb.plupload.ids.length) {
 		// Enable the uploader if the user is under the limit
 		phpbb.plupload.enableUploader();
 	}
 	return false;
-}
+};
 
 /**
  * Disable the uploader
- *
- * @return undefined
  */
 phpbb.plupload.disableUploader = function() {
 	$('#add_files').addClass('disabled');
-	uploader.disableBrowse();
-}
+	phpbb.plupload.uploader.disableBrowse();
+};
 
 /**
  * Enable the uploader
- *
- * @return undefined
  */
 phpbb.plupload.enableUploader = function() {
 	$('#add_files').removeClass('disabled');
-	uploader.disableBrowse(false);
-}
+	phpbb.plupload.uploader.disableBrowse(false);
+};
 
 /**
  * Mark all queued files as failed.
  *
- * @param string error Error message to present to the user.
- * @return undefined
+ * @param {string} error Error message to present to the user.
  */
 phpbb.plupload.markQueuedFailed = function(error) {
 	var files = phpbb.plupload.getFilesByStatus(plupload.QUEUED);
@@ -443,48 +418,49 @@ phpbb.plupload.markQueuedFailed = function(error) {
 		$('#' + file.id).find('.file-progress').hide();
 		phpbb.plupload.fileError(file, error);
 	});
-}
+};
 
 /**
  * Marks a file as failed and sets the error message for it.
  *
- * @param object file	Plupload file object that failed.
- * @param string error	Error message to present to the user.
- * @return undefined
+ * @param {object} file		Plupload file object that failed.
+ * @param {string} error	Error message to present to the user.
  */
 phpbb.plupload.fileError = function(file, error) {
 	file.status = plupload.FAILED;
 	file.error = error;
-	$('#' + file.id).find('.file-status').addClass('file-error').attr({'data-error-title': phpbb.plupload.lang.ERROR, 'data-error-message': error});
-}
-
-
+	$('#' + file.id).find('.file-status')
+		.addClass('file-error')
+		.attr({
+			'data-error-title': phpbb.plupload.lang.ERROR,
+			'data-error-message': error
+		});
+};
 
 
 /**
  * Set up the Plupload object and get some basic data.
  */
-var	uploader = new plupload.Uploader(phpbb.plupload.config);
+phpbb.plupload.uploader = new plupload.Uploader(phpbb.plupload.config);
 phpbb.plupload.initialize();
 
-
-
+var $fileList = $('#file-list');
 
 /**
  * Insert inline attachment bbcode.
  */
- $('#file-list').on('click', '.file-inline-bbcode', function(e) {
+$fileList.on('click', '.file-inline-bbcode', function(e) {
 	var attachId = $(this).parents('.attach-row').attr('data-attach-id'),
 		index = phpbb.plupload.getIndex(attachId);
 
-	attach_inline(index, phpbb.plupload.data[index].real_filename);	
+	attachInline(index, phpbb.plupload.data[index].real_filename);
 	e.preventDefault();
 });
 
 /**
  * Delete a file.
  */
-$('#file-list').on('click', '.file-delete', function(e) {
+$fileList.on('click', '.file-delete', function(e) {
 	var row = $(this).parents('.attach-row'),
 		attachId = row.attr('data-attach-id');
 
@@ -495,7 +471,7 @@ $('#file-list').on('click', '.file-delete', function(e) {
 /**
  * Display the error message for a particular file when the error icon is clicked.
  */
-$('#file-list').on('click', '.file-error', function(e) {
+$fileList.on('click', '.file-error', function(e) {
 	phpbb.alert($(this).attr('data-error-title'), $(this).attr('data-error-message'));
 	e.preventDefault();
 });
@@ -503,7 +479,7 @@ $('#file-list').on('click', '.file-error', function(e) {
 /**
  * Fires when an error occurs.
  */
-uploader.bind('Error', function(up, error) {
+phpbb.plupload.uploader.bind('Error', function(up, error) {
 	error.file.name = plupload.xmlEncode(error.file.name);
 
 	// The error message that Plupload provides for these is vague, so we'll be more specific.
@@ -520,18 +496,15 @@ uploader.bind('Error', function(up, error) {
  * send the real filename along with the chunk. This is necessary because
  * for some reason the filename is set to 'blob' whenever a file is chunked
  *
- * @param object up		The plupload.Uploader object
- * @param object file	The plupload.File object that is about to be
- * 	uploaded
- *
- * @return undefined
+ * @param {object} up	The plupload.Uploader object
+ * @param {object} file	The plupload.File object that is about to be uploaded
  */
-uploader.bind('BeforeUpload', function(up, file) {
+phpbb.plupload.uploader.bind('BeforeUpload', function(up, file) {
 	if (phpbb.plupload.handleMaxFilesReached()) {
 		return;
 	}
 
-	phpbb.plupload.updateMultipartParams({'real_filename': file.name});
+	phpbb.plupload.updateMultipartParams({ real_filename: file.name });
 });
 
 /**
@@ -539,14 +512,12 @@ uploader.bind('BeforeUpload', function(up, file) {
  * response from the server and checks for an error. If an error occurs it
  * is reported to the user and the upload of this particular file is halted
  *
- * @param object up			The plupload.Uploader object
- * @param object file		The plupload.File object whose chunk has just
+ * @param {object} up		The plupload.Uploader object
+ * @param {object} file		The plupload.File object whose chunk has just
  * 	been uploaded
- * @param object response	The response object from the server
- *
- * @return undefined
+ * @param {object} response	The response object from the server
  */
-uploader.bind('ChunkUploaded', function(up, file, response) {
+phpbb.plupload.uploader.bind('ChunkUploaded', function(up, file, response) {
 	if (response.chunk >= response.chunks - 1) {
 		return;
 	}
@@ -567,7 +538,7 @@ uploader.bind('ChunkUploaded', function(up, file, response) {
 
 	// If trigger_error() was called, then a permission error likely occurred.
 	if (typeof json.title !== 'undefined') {
-		json.error = {message: json.message};
+		json.error = { message: json.message };
 	}
 
 	if (json.error) {
@@ -584,10 +555,8 @@ uploader.bind('ChunkUploaded', function(up, file, response) {
 
 /**
  * Fires when files are added to the queue.
- *
- * @return undefined
  */
-uploader.bind('FilesAdded', function(up, files) {
+phpbb.plupload.uploader.bind('FilesAdded', function(up, files) {
 	// Prevent unnecessary requests to the server if the user already uploaded
 	// the maximum number of files allowed.
 	if (phpbb.plupload.handleMaxFilesReached()) {
@@ -595,13 +564,14 @@ uploader.bind('FilesAdded', function(up, files) {
 	}
 
 	// Switch the active tab if the style supports it
-	if (typeof activateSubPanel == 'function') {
-		activateSubPanel('attach-panel');
+	if (typeof activateSubPanel === 'function') {
+		activateSubPanel('attach-panel'); // jshint ignore: line
 	}
 
 	// Show the file list if there aren't any files currently.
-	if (!$('#file-list-container').is(':visible')) {
-		$('#file-list-container').show(100);
+	var $fileListContainer = $('#file-list-container');
+	if (!$fileListContainer.is(':visible')) {
+		$fileListContainer.show(100);
 	}
 
 	$.each(files, function(i, file) {
@@ -609,7 +579,7 @@ uploader.bind('FilesAdded', function(up, files) {
 	});
 
 	up.bind('UploadProgress', function(up, file) {
-		$('#' + file.id + " .file-progress-bar").css('width', file.percent + '%');
+		$('.file-progress-bar', '#' + file.id).css('width', file.percent + '%');
 		$('#file-total-progress-bar').css('width', up.total.percent + '%');
 	});
 
@@ -627,14 +597,12 @@ uploader.bind('FilesAdded', function(up, files) {
  * appends it to the next file upload so that the server can maintain state
  * with regards to the attachments in a given post
  *
- * @param object up			The plupload.Uploader object
- * @param object file		The plupload.File object that has just been
+ * @param {object} up		The plupload.Uploader object
+ * @param {object} file		The plupload.File object that has just been
  * 	uploaded
- * @param string response	The response string from the server
- *
- * @return undefined
+ * @param {string} response	The response string from the server
  */
-uploader.bind('FileUploaded', function(up, file, response) {
+phpbb.plupload.uploader.bind('FileUploaded', function(up, file, response) {
 	var json = {},
 		row = $('#' + file.id),
 		error;
@@ -643,7 +611,7 @@ uploader.bind('FileUploaded', function(up, file, response) {
 	row.find('.file-progress').hide();
 
 	try {
-		json = $.parseJSON(response.response);
+		json = JSON.parse(response.response);
 	} catch (e) {
 		error = 'Error parsing server response.';
 	}
@@ -651,7 +619,7 @@ uploader.bind('FileUploaded', function(up, file, response) {
 	// If trigger_error() was called, then a permission error likely occurred.
 	if (typeof json.title !== 'undefined') {
 		error = json.message;
-		up.trigger('Error', {message: error});
+		up.trigger('Error', { message: error });
 
 		// The rest of the queue will fail.
 		phpbb.plupload.markQueuedFailed(error);
@@ -662,25 +630,19 @@ uploader.bind('FileUploaded', function(up, file, response) {
 	if (typeof error !== 'undefined') {
 		phpbb.plupload.fileError(file, error);
 	} else if (file.status === plupload.DONE) {
-		file.attachment_data = json['data'][0];
+		file.attachment_data = json.data[0];
 
 		row.attr('data-attach-id', file.attachment_data.attach_id);
 		row.find('.file-inline-bbcode').show();
 		row.find('.file-status').addClass('file-uploaded');
-		phpbb.plupload.update(json['data'], 'addition', 0, [json['download_url']]);
+		phpbb.plupload.update(json.data, 'addition', 0, [json.download_url]);
 	}
 });
 
 /**
- * Fires when the entire queue of files have been uploaded. 
- *
- * @param object up		The plupload.Uploader object
- * @param array files	An array of plupload.File objects that have just
- * 	been uploaded as part of a queue
- *
- * @return undefined
+ * Fires when the entire queue of files have been uploaded.
  */
-uploader.bind('UploadComplete', function(up, files) {
+phpbb.plupload.uploader.bind('UploadComplete', function() {
 	// Hide the progress bar
 	setTimeout(function() {
 		$('#file-total-progress-bar').fadeOut(500, function() {
--- a/composer.json
+++ b/composer.json
@@ -38,11 +38,11 @@
 	},
 	"require-dev": {
 		"fabpot/goutte": "1.0.*",
+		"phing/phing": "2.4.*",
 		"phpunit/dbunit": "1.3.*",
 		"phpunit/phpunit": "4.1.*",
-		"phing/phing": "2.4.*",
 		"sami/sami": "1.*",
-		"squizlabs/php_codesniffer": "1.*",
+		"squizlabs/php_codesniffer": "2.*",
 		"symfony/browser-kit": "2.3.*",
 		"symfony/css-selector": "2.3.*",
 		"symfony/debug": "2.3.*",
--- a/composer.lock
+++ b/composer.lock
@@ -1,9 +1,10 @@
 {
     "_readme": [
         "This file locks the dependencies of your project to a known state",
-        "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
+        "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+        "This file is @generated automatically"
     ],
-    "hash": "d93446768ea0665b7c55c01890153a67",
+    "hash": "17b51553237b78392baf2ec78bfdfbc0",
     "packages": [
         {
             "name": "lusitanian/oauth",
@@ -652,12 +653,12 @@
             "version": "v1.13.2",
             "source": {
                 "type": "git",
-                "url": "https://github.com/fabpot/Twig.git",
+                "url": "https://github.com/twigphp/Twig.git",
                 "reference": "6d6a1009427d1f398c9d40904147bf9f723d5755"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/fabpot/Twig/zipball/6d6a1009427d1f398c9d40904147bf9f723d5755",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/6d6a1009427d1f398c9d40904147bf9f723d5755",
                 "reference": "6d6a1009427d1f398c9d40904147bf9f723d5755",
                 "shasum": ""
             },
@@ -682,14 +683,11 @@
             "authors": [
                 {
                     "name": "Fabien Potencier",
-                    "email": "fabien@symfony.com",
-                    "homepage": "http://fabien.potencier.org",
-                    "role": "Lead Developer"
+                    "email": "fabien@symfony.com"
                 },
                 {
                     "name": "Armin Ronacher",
-                    "email": "armin.ronacher@active-4.com",
-                    "role": "Project Founder"
+                    "email": "armin.ronacher@active-4.com"
                 }
             ],
             "description": "Twig, the flexible, fast, and secure template language for PHP",
@@ -1914,41 +1912,45 @@
         },
         {
             "name": "squizlabs/php_codesniffer",
-            "version": "1.5.2",
+            "version": "2.3.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
-                "reference": "a76a39b317ce8106abe6264daa505e24e1731860"
+                "reference": "e96d8579fbed0c95ecf2a0501ec4f307a4aa6404"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/a76a39b317ce8106abe6264daa505e24e1731860",
-                "reference": "a76a39b317ce8106abe6264daa505e24e1731860",
+                "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e96d8579fbed0c95ecf2a0501ec4f307a4aa6404",
+                "reference": "e96d8579fbed0c95ecf2a0501ec4f307a4aa6404",
                 "shasum": ""
             },
             "require": {
                 "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
                 "php": ">=5.1.2"
             },
-            "suggest": {
-                "phpunit/php-timer": "dev-master"
-            },
             "bin": [
-                "scripts/phpcs"
+                "scripts/phpcs",
+                "scripts/phpcbf"
             ],
             "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
             "autoload": {
                 "classmap": [
                     "CodeSniffer.php",
                     "CodeSniffer/CLI.php",
                     "CodeSniffer/Exception.php",
                     "CodeSniffer/File.php",
+                    "CodeSniffer/Fixer.php",
                     "CodeSniffer/Report.php",
                     "CodeSniffer/Reporting.php",
                     "CodeSniffer/Sniff.php",
                     "CodeSniffer/Tokens.php",
                     "CodeSniffer/Reports/",
-                    "CodeSniffer/CommentParser/",
                     "CodeSniffer/Tokenizers/",
                     "CodeSniffer/DocGenerators/",
                     "CodeSniffer/Standards/AbstractPatternSniff.php",
@@ -1974,13 +1976,13 @@
                     "role": "lead"
                 }
             ],
-            "description": "PHP_CodeSniffer tokenises PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+            "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
             "homepage": "http://www.squizlabs.com/php-codesniffer",
             "keywords": [
                 "phpcs",
                 "standards"
             ],
-            "time": "2014-02-04 23:49:58"
+            "time": "2015-04-28 23:28:20"
         },
         {
             "name": "symfony/browser-kit",
@@ -2236,17 +2238,13 @@
             "time": "2014-10-01 05:38:33"
         }
     ],
-    "aliases": [
-
-    ],
+    "aliases": [],
     "minimum-stability": "stable",
-    "stability-flags": [
-
-    ],
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
     "platform": {
         "php": ">=5.3.3"
     },
-    "platform-dev": [
-
-    ]
+    "platform-dev": []
 }
--- a/docs/CHANGELOG.html
+++ b/docs/CHANGELOG.html
@@ -49,6 +49,7 @@
 <ol>
 	<li><a href="#changelog">Changelog</a>
 	<ul>
+		<li><a href="#v314">Changes since 3.1.4</a></li>
 		<li><a href="#v313">Changes since 3.1.3</a></li>
 		<li><a href="#v313rc1">Changes since 3.1.3-RC1</a></li>
 		<li><a href="#v312">Changes since 3.1.2</a></li>
@@ -111,6 +112,90 @@
 
 		<div class="content">
 
+	<a name="v314"></a><h3>Changes since 3.1.4</h3>
+
+	<h4>Bug</h4>
+	<ul>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-9563">PHPBB3-9563</a>] - Empty categories showing up on index</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-11521">PHPBB3-11521</a>] - Missing language string when migration is invalid</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-11532">PHPBB3-11532</a>] - acp_users_overview.html autocompletes &quot;confirm email&quot; and &quot;password&quot; fields in chrome</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13516">PHPBB3-13516</a>] - icc-profiler check should skip extensions vendor</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13564">PHPBB3-13564</a>] - User is not removed from oauth tables upon deletion</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13664">PHPBB3-13664</a>] - Allow changing total list for unapproved posts in mcp_front</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13755">PHPBB3-13755</a>] - uploading attachments results in error: parsing server response. </li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13763">PHPBB3-13763</a>] - Language Spelling Error: Completly</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13771">PHPBB3-13771</a>] - AJAX responses do not support exceptions messages</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13772">PHPBB3-13772</a>] - Error in @param variable type for phpbb\passwords\manager</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13779">PHPBB3-13779</a>] - Permission set migration tool grants regular users heightened permissions</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13787">PHPBB3-13787</a>] - Duplicate entry of poll_delete in prosilver template</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13792">PHPBB3-13792</a>] - Travis fails installing hhvm-nigthly</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13819">PHPBB3-13819</a>] - Add missing sql_freeresult() to files in includes/</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13822">PHPBB3-13822</a>] - Permissions are in the wrong category</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13823">PHPBB3-13823</a>] - Update package is missing file with inline whitespace changes</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13827">PHPBB3-13827</a>] - controller\helper::message does not return JSON object for AJAX requests</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13830">PHPBB3-13830</a>] - phpcs doesn't detect class using in catch blocks</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13833">PHPBB3-13833</a>] - Submit a lot of messages without timeout between messages</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13838">PHPBB3-13838</a>] - Add a sniff to ensure that the opening brace of a control statement is on the line after</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13852">PHPBB3-13852</a>] - Inconsistent tab navigation when login in</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13861">PHPBB3-13861</a>] - Old styles are not removed by style update migration</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13868">PHPBB3-13868</a>] - Adding multiple language files for acp/mcp/ucp modules is incorrectly handled for extensions</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13873">PHPBB3-13873</a>] - Remove broken print stylesheet in preference of &amp;view=print</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13875">PHPBB3-13875</a>] - Lint test should ignore cache, ext, and store folder</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13878">PHPBB3-13878</a>] - Properly display background images when printing with webkit browser</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13888">PHPBB3-13888</a>] - &quot;Couldn't fetch mysqli_result&quot; error on username search and egosearch</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13913">PHPBB3-13913</a>] - Post subject of password protected and list-only forums shown on board index</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13922">PHPBB3-13922</a>] - Whitespace found at end of line phpBB/includes/functions_admin.php</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13928">PHPBB3-13928</a>] - Fix build_cfg_template_test after ticket/sec-184 </li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13939">PHPBB3-13939</a>] - phpBB no longer shows error messages when uploading files with drag/drop</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13942">PHPBB3-13942</a>] - phpbb\user::set_lang() adds extra path and PHP extension to ext lang files</li>
+	</ul>
+	<h4>Improvement</h4>
+	<ul>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-12101">PHPBB3-12101</a>] - Redirect for Microsoft servers in /includes/functions.php:redirect()</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-12542">PHPBB3-12542</a>] - Highlight textarea when files are dragged over it</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-12717">PHPBB3-12717</a>] - Improve the code sniffer</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13200">PHPBB3-13200</a>] - Add autocomplete=&quot;off&quot; to additional password fields</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13648">PHPBB3-13648</a>] - Allow extensions using custom bbcode validation methods</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13699">PHPBB3-13699</a>] - Add template events in viewforum_body.html before and after the title</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13745">PHPBB3-13745</a>] - Add veiwtopic.php core event to allow manipulating poll data</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13750">PHPBB3-13750</a>] - Add generate_forum_nav() core event to allow modifying navlinks text</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13752">PHPBB3-13752</a>] - Add viewonline.php core event to allow modifying forum data SQL query</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13753">PHPBB3-13753</a>] - Add template events to forum category header</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13778">PHPBB3-13778</a>] - Misleading instruction text for recaptcha plugin</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13790">PHPBB3-13790</a>] - Update phpcs to 2.3.2</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13791">PHPBB3-13791</a>] - Add more post buttons template events to viewtopic_body.html</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13808">PHPBB3-13808</a>] - Add event before and after the search form</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13809">PHPBB3-13809</a>] - Test php parsing on php7 on travis</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13841">PHPBB3-13841</a>] - Add event when topics are moved</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13858">PHPBB3-13858</a>] - Make the Plupload uploader instance available in the global namespace</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13872">PHPBB3-13872</a>] - Allow template events to have a changed tag</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13876">PHPBB3-13876</a>] - Use async webfontloader to load webfont from googles CDN</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13905">PHPBB3-13905</a>] - loading.gif loaded before document load when it isn't needed</li>
+	</ul>
+	<h4>Security Issue</h4>
+	<ul>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13917">PHPBB3-13917</a>] - Use hash_equals() if possible in password driver helper</li>
+		<li>[SECURITY-184] - Do not output passwords to HTML input fields</li>
+	</ul>
+	<h4>Sub-task</h4>
+	<ul>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13660">PHPBB3-13660</a>] - Allow changing the query for total reports in mcp_front</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13661">PHPBB3-13661</a>] - Allow changing how and which logs are retrieved</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13668">PHPBB3-13668</a>] - Allow modifying the query to get details from the post report</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13672">PHPBB3-13672</a>] - Allow changing the query to obtain the user-submitted report.</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13685">PHPBB3-13685</a>] - Allow modifying the keywords search query for mysql fulltext search</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13686">PHPBB3-13686</a>] - Allow modifying the fulltext native search query for total results before before</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13689">PHPBB3-13689</a>] - Allow modifying the Postgres native search query for results</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13691">PHPBB3-13691</a>] - Allow modifying the fulltext native search query for total results for author</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13693">PHPBB3-13693</a>] - Allow modifying the MySQL author search query for results</li>
+	</ul>
+	<h4>Task</h4>
+	<ul>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13807">PHPBB3-13807</a>] - Extend event exporter to filter by min or max version to allow generating event diffs for releases</li>
+		<li>[<a href="http://tracker.phpbb.com/browse/PHPBB3-13887">PHPBB3-13887</a>] - JS could use some refactoring</li>
+	</ul>
+
+
 	<a name="v313"></a><h3>Changes since 3.1.3</h3>
 
 	<h4>Security</h4>
--- a/docs/events.md
+++ b/docs/events.md
@@ -293,6 +293,20 @@ forumlist_body_category_header_before
 * Since: 3.1.0-a4
 * Purpose: Add content before the header of the category on the forum list.
 
+forumlist_body_category_header_row_append
+===
+* Locations:
+    + styles/prosilver/template/forumlist_body.html
+* Since: 3.1.5-RC1
+* Purpose: Add content after the header row of the category on the forum list.
+
+forumlist_body_category_header_row_prepend
+===
+* Locations:
+    + styles/prosilver/template/forumlist_body.html
+* Since: 3.1.5-RC1
+* Purpose: Add content before the header row of the category on the forum list.
+
 forumlist_body_forum_row_after
 ===
 * Locations:
@@ -517,6 +531,14 @@ mcp_front_latest_unapproved_before
 * Since: 3.1.3-RC1
 * Purpose: Add content before latest unapproved posts list
 
+mcp_post_additional_options
+===
+* Locations:
+    + styles/prosilver/template/mcp_post.html
+    + styles/subsilver2/template/mcp_post.html
+* Since: 3.1.5-RC1
+* Purpose: Add content within the list of post moderation actions
+
 memberlist_body_username_append
 ===
 * Locations:
@@ -1062,6 +1084,14 @@ quickreply_editor_message_before
 * Since: 3.1.0-a4
 * Purpose: Add content before the quick reply textbox
 
+search_body_form_before
+===
+* Locations:
+    + styles/prosilver/template/search_body.html
+    + styles/subsilver2/template/search_body.html
+* Since: 3.1.5-RC1
+* Purpose: Add content before the search form
+
 search_results_header_after
 ===
 * Locations:
@@ -1509,6 +1539,22 @@ viewforum_forum_name_prepend
 * Since: 3.1.0-b3
 * Purpose: Add content directly before the forum name link on the View forum screen
 
+viewforum_forum_title_after
+===
+* Locations:
+    + styles/prosilver/template/viewforum_body.html
+    + styles/subsilver2/template/viewforum_body.html
+* Since: 3.1.5-RC1
+* Purpose: Add content directly after the forum title on the View forum screen
+
+viewforum_forum_title_before
+===
+* Locations:
+    + styles/prosilver/template/viewforum_body.html
+    + styles/subsilver2/template/viewforum_body.html
+* Since: 3.1.5-RC1
+* Purpose: Add content directly before the forum title on the View forum screen
+
 viewtopic_print_head_append
 ===
 * Locations:
@@ -1635,6 +1681,22 @@ viewtopic_body_post_buttons_before
 * Purpose: Add post button to posts (next to edit, quote etc), at the start of
 the list.
 
+viewtopic_body_post_buttons_list_after
+===
+* Locations:
+    + styles/prosilver/template/viewtopic_body.html
+* Since: 3.1.5-RC1
+* Purpose: Add post button custom list to posts (next to edit, quote etc),
+after the original list.
+
+viewtopic_body_post_buttons_list_before
+===
+* Locations:
+    + styles/prosilver/template/viewtopic_body.html
+* Since: 3.1.5-RC1
+* Purpose: Add post button custom list to posts (next to edit, quote etc),
+before the original list.
+
 viewtopic_body_postrow_custom_fields_after
 ===
 * Locations:
--- a/includes/acp/acp_board.php
+++ b/includes/acp/acp_board.php
@@ -500,7 +500,7 @@ class acp_board
 		}
 
 		// We go through the display_vars to make sure no one is trying to set variables he/she is not allowed to...
-		foreach ($display_vars['vars'] as $config_name => $null)
+		foreach ($display_vars['vars'] as $config_name => $data)
 		{
 			if (!isset($cfg_array[$config_name]) || strpos($config_name, 'legend') !== false)
 			{
@@ -514,7 +514,8 @@ class acp_board
 
 			if ($config_name == 'guest_style')
 			{
-				if (isset($cfg_array[$config_name])) {
+				if (isset($cfg_array[$config_name]))
+				{
 					$this->guest_style_set($cfg_array[$config_name]);
 				}
 				continue;
@@ -531,6 +532,13 @@ class acp_board
 
 			if ($submit)
 			{
+				if (strpos($data['type'], 'password') === 0 && $config_value === '********')
+				{
+					// Do not update password fields if the content is ********,
+					// because that is the password replacement we use to not
+					// send the password to the output
+					continue;
+				}
 				set_config($config_name, $config_value);
 
 				if ($config_name == 'allow_quick_reply' && isset($_POST['allow_quick_reply_enable']))
@@ -559,6 +567,7 @@ class acp_board
 			$old_auth_config = array();
 			foreach ($auth_providers as $provider)
 			{
+				/** @var \phpbb\auth\provider\provider_interface $provider */
 				if ($fields = $provider->acp())
 				{
 					// Check if we need to create config fields for this plugin and save config when submit was pressed
@@ -574,6 +583,14 @@ class acp_board
 							continue;
 						}
 
+						if (substr($field, -9) === '_password' && $cfg_array[$field] === '********')
+						{
+							// Do not update password fields if the content is ********,
+							// because that is the password replacement we use to not
+							// send the password to the output
+							continue;
+						}
+
 						$old_auth_config[$field] = $this->new_config[$field];
 						$config_value = $cfg_array[$field];
 						$this->new_config[$field] = $config_value;
--- a/includes/acp/acp_database.php
+++ b/includes/acp/acp_database.php
@@ -1173,6 +1173,7 @@ class postgres_extractor extends base_extractor
 				$this->flush($sql_data . ";\n");
 			}
 		}
+		$db->sql_freeresult($result);
 
 		$sql_data = '-- Table: ' . $table_name . "\n";
 		$sql_data .= "DROP TABLE $table_name;\n";
--- a/includes/acp/acp_jabber.php
+++ b/includes/acp/acp_jabber.php
@@ -107,7 +107,10 @@ class acp_jabber
 			set_config('jab_host', $jab_host);
 			set_config('jab_port', $jab_port);
 			set_config('jab_username', $jab_username);
-			set_config('jab_password', $jab_password);
+			if ($jab_password !== '********')
+			{
+				set_config('jab_password', $jab_password);
+			}
 			set_config('jab_package_size', $jab_package_size);
 			set_config('jab_use_ssl', $jab_use_ssl);
 
@@ -122,7 +125,7 @@ class acp_jabber
 			'JAB_HOST'				=> $jab_host,
 			'JAB_PORT'				=> ($jab_port) ? $jab_port : '',
 			'JAB_USERNAME'			=> $jab_username,
-			'JAB_PASSWORD'			=> $jab_password,
+			'JAB_PASSWORD'			=> $jab_password !== '' ? '********' : '',
 			'JAB_PACKAGE_SIZE'		=> $jab_package_size,
 			'JAB_USE_SSL'			=> $jab_use_ssl,
 			'S_CAN_USE_SSL'			=> jabber::can_use_ssl(),
--- a/includes/acp/acp_main.php
+++ b/includes/acp/acp_main.php
@@ -632,7 +632,7 @@ class acp_main
 		{
 			$error = false;
 			$search_type = $config['search_type'];
-			$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user);
+			$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher);
 
 			if (!$search->index_created())
 			{
--- a/includes/acp/acp_prune.php
+++ b/includes/acp/acp_prune.php
@@ -506,9 +506,9 @@ class acp_prune
 				WHERE ug.group_id = ' . (int) $group_id . '
 					AND ug.user_id <> ' . ANONYMOUS . '
 					AND u.user_type <> ' . USER_FOUNDER . '
-					AND ug.user_pending = 0 ' .
-					((!empty($user_ids)) ? ' AND ' . $db->sql_in_set('ug.user_id', $user_ids) : '') . '
-					AND u.user_id = ug.user_id';
+					AND ug.user_pending = 0
+					AND u.user_id = ug.user_id
+					' . (!empty($user_ids) ? ' AND ' . $db->sql_in_set('ug.user_id', $user_ids) : '');
 			$result = $db->sql_query($sql);
 
 			// we're performing an intersection operation, so all the relevant users
@@ -532,10 +532,10 @@ class acp_prune
 			$sql = 'SELECT u.user_id, u.username, COUNT(p.post_id) AS queue_posts
 				FROM ' . POSTS_TABLE . ' p, ' . USERS_TABLE . ' u
 				WHERE u.user_id <> ' . ANONYMOUS . '
-					AND u.user_type <> ' . USER_FOUNDER .
-					((!empty($user_ids)) ? ' AND ' . $db->sql_in_set('p.poster_id', $user_ids) : '') . '
+					AND u.user_type <> ' . USER_FOUNDER . '
 					AND ' . $db->sql_in_set('p.post_visibility', array(ITEM_UNAPPROVED, ITEM_REAPPROVE)) . '
 					AND u.user_id = p.poster_id
+					' . (!empty($user_ids) ? ' AND ' . $db->sql_in_set('p.poster_id', $user_ids) : '') . '
 				GROUP BY p.poster_id
 				HAVING queue_posts ' . $key_match[$queue_select] . ' ' . $posts_on_queue;
 			$result = $db->sql_query($sql);
--- a/includes/acp/acp_search.php
+++ b/includes/acp/acp_search.php
@@ -598,7 +598,7 @@ class acp_search
 	*/
 	function init_search($type, &$search, &$error)
 	{
-		global $phpbb_root_path, $phpEx, $user, $auth, $config, $db;
+		global $phpbb_root_path, $phpEx, $user, $auth, $config, $db, $phpbb_dispatcher;
 
 		if (!class_exists($type) || !method_exists($type, 'keyword_search'))
 		{
@@ -607,7 +607,7 @@ class acp_search
 		}
 
 		$error = false;
-		$search = new $type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user);
+		$search = new $type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher);
 
 		return $error;
 	}
--- a/includes/bbcode.php
+++ b/includes/bbcode.php
@@ -182,6 +182,8 @@ class bbcode
 			$db->sql_freeresult($result);
 		}
 
+		// To perform custom second pass in extension, use $this->bbcode_second_pass_by_extension()
+		// method which accepts variable number of parameters
 		foreach ($bbcode_ids as $bbcode_id)
 		{
 			switch ($bbcode_id)
@@ -613,4 +615,36 @@ class bbcode
 
 		return $code;
 	}
+
+	/**
+	* Function to perform custom bbcode second pass by extensions
+	* can be used to assign bbcode pattern replacement
+	* Example: '#\[list=([^\[]+):$uid\]#e'	=> "\$this->bbcode_second_pass_by_extension('\$1')"
+	*
+	* Accepts variable number of parameters
+	*
+	* @return mixed Second pass result
+	*/
+	function bbcode_second_pass_by_extension()
+	{
+		global $phpbb_dispatcher;
+
+		$return = false;
+		$params_array = func_get_args();
+
+		/**
+		* Event to perform bbcode second pass with
+		* the custom validating methods provided by extensions
+		*
+		* @event core.bbcode_second_pass_by_extension
+		* @var array	params_array	Array with the function parameters
+		* @var mixed	return			Second pass result to return
+		*
+		* @since 3.1.5-RC1
+		*/
+		$vars = array('params_array', 'return');
+		extract($phpbb_dispatcher->trigger_event('core.bbcode_second_pass_by_extension', compact($vars)));
+
+		return $return;
+	}
 }
--- a/includes/constants.php
+++ b/includes/constants.php
@@ -28,7 +28,7 @@ if (!defined('IN_PHPBB'))
 */
 
 // phpBB Version
-define('PHPBB_VERSION', '3.1.4');
+define('PHPBB_VERSION', '3.1.5');
 
 // QA-related
 // define('PHPBB_QA', 1);
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -1159,7 +1159,7 @@ function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $
 	/**
 	 * This event is used for performing actions directly before marking forums,
 	 * topics or posts as read.
-	 * 
+	 *
 	 * It is also possible to prevent the marking. For that, the $should_markread parameter
 	 * should be set to FALSE.
 	 *
@@ -2389,7 +2389,7 @@ function redirect($url, $return = false, $disable_cd_check = false)
 	}
 
 	// Redirect via an HTML form for PITA webservers
-	if (@preg_match('#Microsoft|WebSTAR|Xitami#', getenv('SERVER_SOFTWARE')))
+	if (@preg_match('#WebSTAR|Xitami#', getenv('SERVER_SOFTWARE')))
 	{
 		header('Refresh: 0; URL=' . $url);
 
--- a/includes/functions_acp.php
+++ b/includes/functions_acp.php
@@ -245,8 +245,13 @@ function build_cfg_template($tpl_type, $key, &$new, $config_key, $vars)
 
 	switch ($tpl_type[0])
 	{
-		case 'text':
 		case 'password':
+			if ($new[$config_key] !== '')
+			{
+				// replace passwords with asterixes
+				$new[$config_key] = '********';
+			}
+		case 'text':
 		case 'url':
 		case 'email':
 		case 'color':
--- a/includes/functions_admin.php
+++ b/includes/functions_admin.php
@@ -500,7 +500,7 @@ function filelist($rootdir, $dir = '', $type = 'gif|jpg|jpeg|png')
 */
 function move_topics($topic_ids, $forum_id, $auto_sync = true)
 {
-	global $db;
+	global $db, $phpbb_dispatcher;
 
 	if (empty($topic_ids))
 	{
@@ -534,6 +534,27 @@ function move_topics($topic_ids, $forum_id, $auto_sync = true)
 	}
 
 	$table_ary = array(TOPICS_TABLE, POSTS_TABLE, LOG_TABLE, DRAFTS_TABLE, TOPICS_TRACK_TABLE);
+
+	/**
+	 * Perform additional actions before topics move
+	 *
+	 * @event core.move_topics_before_query
+	 * @var	array	table_ary	Array of tables from which forum_id will be updated for all rows that hold the moved topics
+	 * @var	array	topic_ids	Array of the moved topic ids
+	 * @var	string	forum_id	The forum id from where the topics are moved
+	 * @var	array	forum_ids	Array of the forums where the topics are moving (includes also forum_id)
+	 * @var bool	auto_sync	Whether or not to perform auto sync
+	 * @since 3.1.5-RC1
+	 */
+	$vars = array(
+			'table_ary',
+			'topic_ids',
+			'forum_id',
+			'forum_ids',
+			'auto_sync',
+	);
+	extract($phpbb_dispatcher->trigger_event('core.move_topics_before_query', compact($vars)));
+
 	foreach ($table_ary as $table)
 	{
 		$sql = "UPDATE $table
@@ -920,7 +941,7 @@ function delete_posts($where_type, $where_ids, $auto_sync = true, $posted_sync =
 	}
 
 	$error = false;
-	$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user);
+	$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher);
 
 	if ($error)
 	{
--- a/includes/functions_display.php
+++ b/includes/functions_display.php
@@ -150,7 +150,7 @@ function display_forums($root_data = '', $display_moderators = true, $return_mod
 	$sql = $db->sql_build_query('SELECT', $sql_ary);
 	$result = $db->sql_query($sql);
 
-	$forum_tracking_info = array();
+	$forum_tracking_info = $valid_categories = array();
 	$branch_root_id = $root_data['forum_id'];
 
 	$phpbb_content_visibility = $phpbb_container->get('content.visibility');
@@ -250,6 +250,12 @@ function display_forums($root_data = '', $display_moderators = true, $return_mod
 			}
 		}
 
+		// Fill list of categories with forums
+		if (isset($forum_rows[$row['parent_id']]))
+		{
+			$valid_categories[$row['parent_id']] = true;
+		}
+
 		//
 		if ($row['parent_id'] == $root_data['forum_id'] || $row['parent_id'] == $branch_root_id)
 		{
@@ -267,6 +273,7 @@ function display_forums($root_data = '', $display_moderators = true, $return_mod
 				$branch_root_id = $forum_id;
 			}
 			$forum_rows[$parent_id]['forum_id_last_post'] = $row['forum_id'];
+			$forum_rows[$parent_id]['forum_password_last_post'] = $row['forum_password'];
 			$forum_rows[$parent_id]['orig_forum_last_post_time'] = $row['forum_last_post_time'];
 		}
 		else if ($row['forum_type'] != FORUM_CAT)
@@ -308,6 +315,7 @@ function display_forums($root_data = '', $display_moderators = true, $return_mod
 				$forum_rows[$parent_id]['forum_last_poster_name'] = $row['forum_last_poster_name'];
 				$forum_rows[$parent_id]['forum_last_poster_colour'] = $row['forum_last_poster_colour'];
 				$forum_rows[$parent_id]['forum_id_last_post'] = $forum_id;
+				$forum_rows[$parent_id]['forum_password_last_post'] = $row['forum_password'];
 			}
 		}
 
@@ -404,6 +412,12 @@ function display_forums($root_data = '', $display_moderators = true, $return_mod
 		// Category
 		if ($row['parent_id'] == $root_data['forum_id'] && $row['forum_type'] == FORUM_CAT)
 		{
+			// Do not display categories without any forums to display
+			if (!isset($valid_categories[$row['forum_id']]))
+			{
+				continue;
+			}
+
 			$cat_row = array(
 				'S_IS_CAT'				=> true,
 				'FORUM_ID'				=> $row['forum_id'],
@@ -522,8 +536,15 @@ function display_forums($root_data = '', $display_moderators = true, $return_mod
 		// Create last post link information, if appropriate
 		if ($row['forum_last_post_id'])
 		{
-			$last_post_subject = $row['forum_last_post_subject'];
-			$last_post_subject_truncated = truncate_string(censor_text($last_post_subject), 30, 255, false, $user->lang['ELLIPSIS']);
+			if ($row['forum_password_last_post'] === '' && $auth->acl_get('f_read', $row['forum_id_last_post']))
+			{
+				$last_post_subject = censor_text($row['forum_last_post_subject']);
+				$last_post_subject_truncated = truncate_string($last_post_subject, 30, 255, false, $user->lang['ELLIPSIS']);
+			}
+			else
+			{
+				$last_post_subject = $last_post_subject_truncated = '';
+			}
 			$last_post_time = $user->format_date($row['forum_last_post_time']);
 			$last_post_url = append_sid("{$phpbb_root_path}viewtopic.$phpEx", 'f=' . $row['forum_id_last_post'] . '&amp;p=' . $row['forum_last_post_id']) . '#p' . $row['forum_last_post_id'];
 		}
@@ -583,7 +604,7 @@ function display_forums($root_data = '', $display_moderators = true, $return_mod
 			'S_LOCKED_FORUM'	=> ($row['forum_status'] == ITEM_LOCKED) ? true : false,
 			'S_LIST_SUBFORUMS'	=> ($row['display_subforum_list']) ? true : false,
 			'S_SUBFORUMS'		=> (sizeof($subforums_list)) ? true : false,
-			'S_DISPLAY_SUBJECT'	=>	($last_post_subject && $config['display_last_subject'] && !$row['forum_password'] && $auth->acl_get('f_read', $row['forum_id'])) ? true : false,
+			'S_DISPLAY_SUBJECT'	=>	($last_post_subject !== '' && $config['display_last_subject']) ? true : false,
 			'S_FEED_ENABLED'	=> ($config['feed_forum'] && !phpbb_optionget(FORUM_OPTION_FEED_EXCLUDE, $row['forum_options']) && $row['forum_type'] == FORUM_POST) ? true : false,
 
 			'FORUM_ID'				=> $row['forum_id'],
@@ -596,8 +617,8 @@ function display_forums($root_data = '', $display_moderators = true, $return_mod
 			'FORUM_FOLDER_IMG_ALT'	=> isset($user->lang[$folder_alt]) ? $user->lang[$folder_alt] : '',
 			'FORUM_IMAGE'			=> ($row['forum_image']) ? '<img src="' . $phpbb_root_path . $row['forum_image'] . '" alt="' . $user->lang[$folder_alt] . '" />' : '',
 			'FORUM_IMAGE_SRC'		=> ($row['forum_image']) ? $phpbb_root_path . $row['forum_image'] : '',
-			'LAST_POST_SUBJECT'		=> (!$row['forum_password'] && $auth->acl_get('f_read', $row['forum_id'])) ? censor_text($last_post_subject) : "",
-			'LAST_POST_SUBJECT_TRUNCATED'	=> (!$row['forum_password'] && $auth->acl_get('f_read', $row['forum_id'])) ? $last_post_subject_truncated : "",
+			'LAST_POST_SUBJECT'		=> $last_post_subject,
+			'LAST_POST_SUBJECT_TRUNCATED'	=> $last_post_subject_truncated,
 			'LAST_POST_TIME'		=> $last_post_time,
 			'LAST_POSTER'			=> get_username_string('username', $row['forum_last_poster_id'], $row['forum_last_poster_name'], $row['forum_last_poster_colour']),
 			'LAST_POSTER_COLOUR'	=> get_username_string('colour', $row['forum_last_poster_id'], $row['forum_last_poster_name'], $row['forum_last_poster_colour']),
@@ -732,13 +753,15 @@ function generate_forum_rules(&$forum_data)
 function generate_forum_nav(&$forum_data)
 {
 	global $db, $user, $template, $auth, $config;
-	global $phpEx, $phpbb_root_path;
+	global $phpEx, $phpbb_root_path, $phpbb_dispatcher;
 
 	if (!$auth->acl_get('f_list', $forum_data['forum_id']))
 	{
 		return;
 	}
 
+	$navlinks = $navlinks_parents = $forum_template_data = array();
+
 	// Get forum parents
 	$forum_parents = get_forum_parents($forum_data);
 
@@ -757,35 +780,59 @@ function generate_forum_nav(&$forum_data)
 				continue;
 			}
 
-			$template->assign_block_vars('navlinks', array(
+			$navlinks_parents[] = array(
 				'S_IS_CAT'		=> ($parent_type == FORUM_CAT) ? true : false,
 				'S_IS_LINK'		=> ($parent_type == FORUM_LINK) ? true : false,
 				'S_IS_POST'		=> ($parent_type == FORUM_POST) ? true : false,
 				'FORUM_NAME'	=> $parent_name,
 				'FORUM_ID'		=> $parent_forum_id,
 				'MICRODATA'		=> $microdata_attr . '="' . $parent_forum_id . '"',
-				'U_VIEW_FORUM'	=> append_sid("{$phpbb_root_path}viewforum.$phpEx", 'f=' . $parent_forum_id))
+				'U_VIEW_FORUM'	=> append_sid("{$phpbb_root_path}viewforum.$phpEx", 'f=' . $parent_forum_id),
 			);
 		}
 	}
 
-	$template->assign_block_vars('navlinks', array(
+	$navlinks = array(
 		'S_IS_CAT'		=> ($forum_data['forum_type'] == FORUM_CAT) ? true : false,
 		'S_IS_LINK'		=> ($forum_data['forum_type'] == FORUM_LINK) ? true : false,
 		'S_IS_POST'		=> ($forum_data['forum_type'] == FORUM_POST) ? true : false,
 		'FORUM_NAME'	=> $forum_data['forum_name'],
 		'FORUM_ID'		=> $forum_data['forum_id'],
 		'MICRODATA'		=> $microdata_attr . '="' . $forum_data['forum_id'] . '"',
-		'U_VIEW_FORUM'	=> append_sid("{$phpbb_root_path}viewforum.$phpEx", 'f=' . $forum_data['forum_id']))
+		'U_VIEW_FORUM'	=> append_sid("{$phpbb_root_path}viewforum.$phpEx", 'f=' . $forum_data['forum_id']),
 	);
 
-	$template->assign_vars(array(
+	$forum_template_data = array(
 		'FORUM_ID' 		=> $forum_data['forum_id'],
 		'FORUM_NAME'	=> $forum_data['forum_name'],
 		'FORUM_DESC'	=> generate_text_for_display($forum_data['forum_desc'], $forum_data['forum_desc_uid'], $forum_data['forum_desc_bitfield'], $forum_data['forum_desc_options']),
 
 		'S_ENABLE_FEEDS_FORUM'	=> ($config['feed_forum'] && $forum_data['forum_type'] == FORUM_POST && !phpbb_optionget(FORUM_OPTION_FEED_EXCLUDE, $forum_data['forum_options'])) ? true : false,
-	));
+	);
+
+	/**
+	* Event to modify the navlinks text
+	*
+	* @event core.generate_forum_nav
+	* @var	array	forum_data				Array with the forum data
+	* @var	array	forum_template_data		Array with generic forum template data
+	* @var	string	microdata_attr			The microdata attribute
+	* @var	array	navlinks_parents		Array with the forum parents navlinks data
+	* @var	array	navlinks				Array with the forum navlinks data
+	* @since 3.1.5-RC1
+	*/
+	$vars = array(
+		'forum_data',
+		'forum_template_data',
+		'microdata_attr',
+		'navlinks_parents',
+		'navlinks',
+	);
+	extract($phpbb_dispatcher->trigger_event('core.generate_forum_nav', compact($vars)));
+
+	$template->assign_block_vars_array('navlinks', $navlinks_parents);
+	$template->assign_block_vars('navlinks', $navlinks);
+	$template->assign_vars($forum_template_data);
 
 	return;
 }
--- a/includes/functions_module.php
+++ b/includes/functions_module.php
@@ -976,7 +976,7 @@ class p_master
 	*
 	* @param string $class module class (acp/mcp/ucp)
 	* @param string $name module name (class name of the module, or its basename
-    *                     phpbb_ext_foo_acp_bar_module, ucp_zebra or zebra)
+	*                     phpbb_ext_foo_acp_bar_module, ucp_zebra or zebra)
 	* @param string $mode mode, as passed through to the module
 	*
 	*/
@@ -1086,7 +1086,7 @@ class p_master
 			->core_path('language/' . $user->lang_name . '/mods/')
 			->find();
 
-		$lang_files = array_unique(array_merge($user_lang_files, $english_lang_files, $default_lang_files));
+		$lang_files = array_merge($english_lang_files, $default_lang_files, $user_lang_files);
 		foreach ($lang_files as $lang_file => $ext_name)
 		{
 			$user->add_lang_ext($ext_name, $lang_file);
--- a/includes/functions_posting.php
+++ b/includes/functions_posting.php
@@ -2209,7 +2209,7 @@ function submit_post($mode, $subject, $username, $topic_type, &$poll, &$data, $u
 		}
 
 		$error = false;
-		$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user);
+		$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher);
 
 		if ($error)
 		{
--- a/includes/functions_upload.php
+++ b/includes/functions_upload.php
@@ -213,6 +213,8 @@ class filespec
 	*/
 	static public function get_extension($filename)
 	{
+		$filename = utf8_basename($filename);
+
 		if (strpos($filename, '.') === false)
 		{
 			return '';
--- a/includes/functions_user.php
+++ b/includes/functions_user.php
@@ -500,6 +500,9 @@ function user_delete($mode, $user_ids, $retain_username = true)
 
 	$num_users_delta = 0;
 
+	// Get auth provider collection in case accounts might need to be unlinked
+	$provider_collection = $phpbb_container->get('auth.provider_collection');
+
 	// Some things need to be done in the loop (if the query changes based
 	// on which user is currently being deleted)
 	$added_guest_posts = 0;
@@ -510,6 +513,38 @@ function user_delete($mode, $user_ids, $retain_username = true)
 			avatar_delete('user', $user_row);
 		}
 
+		// Unlink accounts
+		foreach ($provider_collection as $provider_name => $auth_provider)
+		{
+			$provider_data = $auth_provider->get_auth_link_data($user_id);
+
+			if ($provider_data !== null)
+			{
+				$link_data = array(
+					'user_id' => $user_id,
+					'link_method' => 'user_delete',
+				);
+
+				// BLOCK_VARS might contain hidden fields necessary for unlinking accounts
+				if (isset($provider_data['BLOCK_VARS']) && is_array($provider_data['BLOCK_VARS']))
+				{
+					foreach ($provider_data['BLOCK_VARS'] as $provider_service)
+					{
+						if (!array_key_exists('HIDDEN_FIELDS', $provider_service))
+						{
+							$provider_service['HIDDEN_FIELDS'] = array();
+						}
+
+						$auth_provider->unlink_account(array_merge($link_data, $provider_service['HIDDEN_FIELDS']));
+					}
+				}
+				else
+				{
+					$auth_provider->unlink_account($link_data);
+				}
+			}
+		}
+
 		// Decrement number of users if this user is active
 		if ($user_row['user_type'] != USER_INACTIVE && $user_row['user_type'] != USER_IGNORE)
 		{
--- a/includes/mcp/mcp_front.php
+++ b/includes/mcp/mcp_front.php
@@ -41,10 +41,27 @@ function mcp_front_view($id, $mode, $action)
 
 		if (!empty($forum_list))
 		{
-			$sql = 'SELECT COUNT(post_id) AS total
-				FROM ' . POSTS_TABLE . '
-				WHERE ' . $db->sql_in_set('forum_id', $forum_list) . '
-					AND ' . $db->sql_in_set('post_visibility', array(ITEM_UNAPPROVED, ITEM_REAPPROVE));
+			$sql_ary = array(
+				'SELECT' => 'COUNT(post_id) AS total',
+				'FROM' => array(
+						POSTS_TABLE => 'p',
+					),
+				'WHERE' => $db->sql_in_set('p.forum_id', $forum_list) . '
+					AND ' . $db->sql_in_set('p.post_visibility', array(ITEM_UNAPPROVED, ITEM_REAPPROVE))
+			);
+
+			/**
+			* Allow altering the query to get the number of unapproved posts
+			*
+			* @event core.mcp_front_queue_unapproved_total_before
+			* @var	int		sql_ary						Query to get the total number of unapproved posts
+			* @var	array	forum_list					List of forums to look for unapproved posts
+			* @since 3.1.5-RC1
+			*/
+			$vars = array('sql_ary', 'forum_list');
+			extract($phpbb_dispatcher->trigger_event('core.mcp_front_queue_unapproved_total_before', compact($vars)));
+
+			$sql = $db->sql_build_query('SELECT', $sql_ary);
 			$result = $db->sql_query($sql);
 			$total = (int) $db->sql_fetchfield('total');
 			$db->sql_freeresult($result);
@@ -157,6 +174,18 @@ function mcp_front_view($id, $mode, $action)
 					AND r.pm_id = 0
 					AND r.report_closed = 0
 					AND ' . $db->sql_in_set('p.forum_id', $forum_list);
+
+			/**
+			* Alter sql query to count the number of reported posts
+			*
+			* @event core.mcp_front_reports_count_query_before
+			* @var	int		sql				The query string used to get the number of reports that exist
+			* @var	array	forum_list		List of forums that contain the posts
+			* @since 3.1.5-RC1
+			*/
+			$vars = array('sql', 'forum_list');
+			extract($phpbb_dispatcher->trigger_event('core.mcp_front_reports_count_query_before', compact($vars)));
+
 			$result = $db->sql_query($sql);
 			$total = (int) $db->sql_fetchfield('total');
 			$db->sql_freeresult($result);
--- a/includes/mcp/mcp_main.php
+++ b/includes/mcp/mcp_main.php
@@ -1144,7 +1144,7 @@ function mcp_delete_post($post_ids, $is_soft = false, $soft_delete_reason = '',
 function mcp_fork_topic($topic_ids)
 {
 	global $auth, $user, $db, $template, $config;
-	global $phpEx, $phpbb_root_path;
+	global $phpEx, $phpbb_root_path, $phpbb_dispatcher;
 
 	if (!phpbb_check_ids($topic_ids, TOPICS_TABLE, 'topic_id', array('m_')))
 	{
@@ -1222,7 +1222,7 @@ function mcp_fork_topic($topic_ids)
 				}
 
 				$error = false;
-				$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user);
+				$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher);
 				$search_mode = 'post';
 
 				if ($error)
--- a/includes/mcp/mcp_post.php
+++ b/includes/mcp/mcp_post.php
@@ -26,6 +26,7 @@ function mcp_post_details($id, $mode, $action)
 {
 	global $phpEx, $phpbb_root_path, $config;
 	global $template, $db, $user, $auth, $cache;
+	global $phpbb_dispatcher;
 
 	$user->add_lang('posting');
 
@@ -106,6 +107,21 @@ function mcp_post_details($id, $mode, $action)
 			}
 
 		break;
+
+		default:
+
+			/**
+			* This event allows you to handle custom post moderation options
+			*
+			* @event core.mcp_post_additional_options
+			* @var	string	action		Post moderation action name
+			* @var	array	post_info	Information on the affected post
+			* @since 3.1.5-RC1
+			*/
+			$vars = array('action', 'post_info');
+			extract($phpbb_dispatcher->trigger_event('core.mcp_post_additional_options', compact($vars)));
+
+		break;
 	}
 
 	// Set some vars
@@ -197,7 +213,7 @@ function mcp_post_details($id, $mode, $action)
 		$l_deleted_by = '';
 	}
 
-	$template->assign_vars(array(
+	$mcp_post_template_data = array(
 		'U_MCP_ACTION'			=> "$url&amp;i=main&amp;quickmod=1&amp;mode=post_details", // Use this for mode paramaters
 		'U_POST_ACTION'			=> "$url&amp;i=$id&amp;mode=post_details", // Use this for action parameters
 		'U_APPROVE_ACTION'		=> append_sid("{$phpbb_root_path}mcp.$phpEx", "i=queue&amp;p=$post_id&amp;f={$post_info['forum_id']}"),
@@ -249,7 +265,32 @@ function mcp_post_details($id, $mode, $action)
 
 		'U_LOOKUP_IP'			=> ($auth->acl_get('m_info', $post_info['forum_id'])) ? "$url&amp;i=$id&amp;mode=$mode&amp;lookup={$post_info['poster_ip']}#ip" : '',
 		'U_WHOIS'				=> ($auth->acl_get('m_info', $post_info['forum_id'])) ? append_sid("{$phpbb_root_path}mcp.$phpEx", "i=$id&amp;mode=$mode&amp;action=whois&amp;p=$post_id&amp;ip={$post_info['poster_ip']}") : '',
-	));
+	);
+
+	$s_additional_opts = false;
+
+	/**
+	* Event to add/modify MCP post template data
+	*
+	* @event core.mcp_post_template_data
+	* @var	array	post_info					Array with the post information
+	* @var	array	mcp_post_template_data		Array with the MCP post template data
+	* @var	array	attachments					Array with the post attachments, if any
+	* @var	bool	s_additional_opts			Must be set to true in extension if additional options are presented in MCP post panel
+	* @since 3.1.5-RC1
+	*/
+	$vars = array(
+		'post_info',
+		'mcp_post_template_data',
+		'attachments',
+		's_additional_opts',
+	);
+	extract($phpbb_dispatcher->trigger_event('core.mcp_post_template_data', compact($vars)));
+
+	$template->assign_vars($mcp_post_template_data);
+	$template->assign_var('S_MCP_POST_ADDITIONAL_OPTS', $s_additional_opts);
+
+	unset($mcp_post_template_data);
 
 	// Get User Notes
 	$log_data = array();
@@ -497,7 +538,7 @@ function change_poster(&$post_info, $userdata)
 	{
 		// We do some additional checks in the module to ensure it can actually be utilised
 		$error = false;
-		$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user);
+		$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher);
 
 		if (!$error && method_exists($search, 'destroy_cache'))
 		{
--- a/includes/mcp/mcp_reports.php
+++ b/includes/mcp/mcp_reports.php
@@ -73,18 +73,66 @@ class mcp_reports
 
 				// closed reports are accessed by report id
 				$report_id = request_var('r', 0);
+				$sql_ary = array(
+					'SELECT'	=> 'r.post_id, r.user_id, r.report_id, r.report_closed, report_time, r.report_text, r.reported_post_text, r.reported_post_uid, r.reported_post_bitfield, r.reported_post_enable_magic_url, r.reported_post_enable_smilies, r.reported_post_enable_bbcode, rr.reason_title, rr.reason_description, u.username, u.username_clean, u.user_colour',
 
-				$sql = 'SELECT r.post_id, r.user_id, r.report_id, r.report_closed, report_time, r.report_text, r.reported_post_text, r.reported_post_uid, r.reported_post_bitfield, r.reported_post_enable_magic_url, r.reported_post_enable_smilies, r.reported_post_enable_bbcode, rr.reason_title, rr.reason_description, u.username, u.username_clean, u.user_colour
-					FROM ' . REPORTS_TABLE . ' r, ' . REPORTS_REASONS_TABLE . ' rr, ' . USERS_TABLE . ' u
-					WHERE ' . (($report_id) ? 'r.report_id = ' . $report_id : "r.post_id = $post_id") . '
+					'FROM'		=> array(
+						REPORTS_TABLE			=> 'r',
+						REPORTS_REASONS_TABLE	=> 'rr',
+						USERS_TABLE				=> 'u',
+					),
+
+					'WHERE'		=> (($report_id) ? 'r.report_id = ' . $report_id : "r.post_id = $post_id") . '
 						AND rr.reason_id = r.reason_id
 						AND r.user_id = u.user_id
-						AND r.pm_id = 0
-					ORDER BY report_closed ASC';
+						AND r.pm_id = 0',
+
+					'ORDER_BY'	=> 'report_closed ASC',
+				);
+
+				/**
+				* Allow changing the query to obtain the user-submitted report.
+				*
+				* @event core.mcp_reports_report_details_query_before
+				* @var	array	sql_ary			The array in the format of the query builder with the query
+				* @var	mixed	forum_id		The forum_id, the number in the f GET parameter
+				* @var	int		post_id			The post_id of the report being viewed (if 0, it is meaningless)
+				* @var	int		report_id		The report_id of the report being viewed
+				* @since 3.1.5-RC1
+				*/
+				$vars = array(
+					'sql_ary',
+					'forum_id',
+					'post_id',
+					'report_id',
+				);
+				extract($phpbb_dispatcher->trigger_event('core.mcp_reports_report_details_query_before', compact($vars)));
+
+				$sql = $db->sql_build_query('SELECT', $sql_ary);
 				$result = $db->sql_query_limit($sql, 1);
 				$report = $db->sql_fetchrow($result);
 				$db->sql_freeresult($result);
 
+				/**
+				* Allow changing the data obtained from the user-submitted report.
+				*
+				* @event core.mcp_reports_report_details_query_after
+				* @var	array	sql_ary		The array in the format of the query builder with the query that had been executted
+				* @var	mixed	forum_id	The forum_id, the number in the f GET parameter
+				* @var	int		post_id		The post_id of the report being viewed (if 0, it is meaningless)
+				* @var	int		report_id	The report_id of the report being viewed
+				* @var	int		report		The query's resulting row.
+				* @since 3.1.5-RC1
+				*/
+				$vars = array(
+					'sql_ary',
+					'forum_id',
+					'post_id',
+					'report_id',
+					'report',
+				);
+				extract($phpbb_dispatcher->trigger_event('core.mcp_reports_report_details_query_after', compact($vars)));
+
 				if (!$report)
 				{
 					trigger_error('NO_REPORT');
@@ -489,6 +537,7 @@ function close_report($report_id_list, $mode, $action, $pm = false)
 	{
 		$post_id_list[] = $row[$id_column];
 	}
+	$db->sql_freeresult($result);
 	$post_id_list = array_unique($post_id_list);
 
 	if ($pm)
--- a/includes/message_parser.php
+++ b/includes/message_parser.php
@@ -128,6 +128,9 @@ class bbcode_firstpass extends bbcode
 		// [quote] in second position.
 		// To parse multiline URL we enable dotall option setting only for URL text
 		// but not for link itself, thus [url][/url] is not affected.
+		//
+		// To perform custom validation in extension, use $this->validate_bbcode_by_extension()
+		// method which accepts variable number of parameters
 		$this->bbcodes = array(
 			'code'			=> array('bbcode_id' => 8,	'regexp' => array('#\[code(?:=([a-z]+))?\](.+\[/code\])#uise' => "\$this->bbcode_code('\$1', '\$2')")),
 			'quote'			=> array('bbcode_id' => 0,	'regexp' => array('#\[quote(?:=&quot;(.*?)&quot;)?\](.+)\[/quote\]#uise' => "\$this->bbcode_quote('\$0')")),
@@ -1875,4 +1878,36 @@ class parse_message extends bbcode_firstpass
 	{
 		$this->mimetype_guesser = $mimetype_guesser;
 	}
+
+	/**
+	* Function to perform custom bbcode validation by extensions
+	* can be used in bbcode_init() to assign regexp replacement
+	* Example: 'regexp' => array('#\[b\](.*?)\[/b\]#uise' => "\$this->validate_bbcode_by_extension('\$1')")
+	*
+	* Accepts variable number of parameters
+	*
+	* @return mixed Validation result
+	*/
+	public function validate_bbcode_by_extension()
+	{
+		global $phpbb_dispatcher;
+
+		$return = false;
+		$params_array = func_get_args();
+
+		/**
+		* Event to validate bbcode with the custom validating methods
+		* provided by extensions
+		*
+		* @event core.validate_bbcode_by_extension
+		* @var array	params_array	Array with the function parameters
+		* @var mixed	return			Validation result to return
+		*
+		* @since 3.1.5-RC1
+		*/
+		$vars = array('params_array', 'return');
+		extract($phpbb_dispatcher->trigger_event('core.validate_bbcode_by_extension', compact($vars)));
+
+		return $return;
+	}
 }
--- a/includes/ucp/ucp_pm_compose.php
+++ b/includes/ucp/ucp_pm_compose.php
@@ -55,7 +55,6 @@ function compose_pm($id, $mode, $action, $user_folders = array())
 
 	$address_list	= $request->variable('address_list', array('' => array(0 => '')));
 
-	$submit		= (isset($_POST['post'])) ? true : false;
 	$preview	= (isset($_POST['preview'])) ? true : false;
 	$save		= (isset($_POST['save'])) ? true : false;
 	$load		= (isset($_POST['load'])) ? true : false;
@@ -69,6 +68,7 @@ function compose_pm($id, $mode, $action, $user_folders = array())
 
 	$refresh	= isset($_POST['add_file']) || isset($_POST['delete_file']) || $save || $load
 		|| $remove_u || $remove_g || $add_to || $add_bcc;
+	$submit = $request->is_set_post('post') && !$refresh && !$preview;
 
 	$action		= ($delete && !$preview && !$refresh && $submit) ? 'delete' : $action;
 	$select_single = ($config['allow_mass_pm'] && $auth->acl_get('u_masspm')) ? false : true;
--- a/includes/ucp/ucp_prefs.php
+++ b/includes/ucp/ucp_prefs.php
@@ -69,7 +69,7 @@ class ucp_prefs
 				* @var	array	data		Array with current ucp options data
 				* @var	array	error		Array with list of errors
 				* @since 3.1.0-a1
-				* @changed 3.1.4-rc1 Added error variable to the event
+				* @changed 3.1.4-RC1 Added error variable to the event
 				*/
 				$vars = array('submit', 'data', 'error');
 				extract($phpbb_dispatcher->trigger_event('core.ucp_prefs_personal_data', compact($vars)));
--- a/language/en/acp/extensions.php
+++ b/language/en/acp/extensions.php
@@ -75,7 +75,7 @@ $lang = array_merge($lang, array(
 			<li>Upload the new files</li>
 			<li>Enable the extension</li>
 		</ol>',
-	'EXTENSION_REMOVE_HEADLINE'	=> 'Completly removing an extension from your board',
+	'EXTENSION_REMOVE_HEADLINE'	=> 'Completely removing an extension from your board',
 	'EXTENSION_REMOVE_EXPLAIN'	=> '<ol>
 			<li>Disable the extension</li>
 			<li>Delete the extension’s data</li>
--- a/language/en/captcha_recaptcha.php
+++ b/language/en/captcha_recaptcha.php
@@ -47,6 +47,6 @@ $lang = array_merge($lang, array(
 	'RECAPTCHA_PRIVATE'				=> 'Private reCaptcha key',
 	'RECAPTCHA_PRIVATE_EXPLAIN'		=> 'Your private reCaptcha key. Keys can be obtained on <a href="http://www.google.com/recaptcha">www.google.com/recaptcha</a>.',
 
-	'RECAPTCHA_EXPLAIN'				=> 'In an effort to prevent automatic submissions, we require that you enter both of the words displayed into the text field underneath.',
+	'RECAPTCHA_EXPLAIN'				=> 'In an effort to prevent automatic submissions, we require that you type the text displayed into the field underneath.',
 	'RECAPTCHA_SOCKET_ERROR'		=> 'There was a problem connecting to the RECAPTCHA service: could not open socket. Try again later.',
 ));
--- a/language/en/migrator.php
+++ b/language/en/migrator.php
@@ -52,6 +52,13 @@ $lang = array_merge($lang, array(
 	'MIGRATION_SCHEMA_DONE'				=> 'Installed Schema: %1$s; Time: %2$.2f seconds',
 	'MIGRATION_SCHEMA_RUNNING'			=> 'Installing Schema: %s.',
 
+	'MIGRATION_INVALID_DATA_MISSING_CONDITION'		=> 'A migration is invalid. An if statement helper is missing a condition.',
+	'MIGRATION_INVALID_DATA_MISSING_STEP'			=> 'A migration is invalid. An if statement helper is missing a valid call to a migration step.',
+	'MIGRATION_INVALID_DATA_CUSTOM_NOT_CALLABLE'	=> 'A migration is invalid. A custom callable function could not be called.',
+	'MIGRATION_INVALID_DATA_UNKNOWN_TYPE'			=> 'A migration is invalid. An unknown migration tool type was encountered.',
+	'MIGRATION_INVALID_DATA_UNDEFINED_TOOL'			=> 'A migration is invalid. An undefined migration tool was encountered.',
+	'MIGRATION_INVALID_DATA_UNDEFINED_METHOD'		=> 'A migration is invalid. An undefined migration tool method was encountered.',
+
 	'MODULE_ERROR'						=> 'An error occurred while creating a module: %s',
 	'MODULE_INFO_FILE_NOT_EXIST'		=> 'A required module info file is missing: %2$s',
 	'MODULE_NOT_EXIST'					=> 'A required module does not exist: %s',
--- a/phpbb/auth/provider/base.php
+++ b/phpbb/auth/provider/base.php
@@ -61,7 +61,7 @@ abstract class base implements \phpbb\auth\provider\provider_interface
 	/**
 	* {@inheritdoc}
 	*/
-	public function get_auth_link_data()
+	public function get_auth_link_data($user_id = 0)
 	{
 		return;
 	}
--- a/phpbb/auth/provider/ldap.php
+++ b/phpbb/auth/provider/ldap.php
@@ -289,7 +289,6 @@ class ldap extends \phpbb\auth\provider\base
 	/**
 	 * {@inheritdoc}
 	 */
-
 	public function acp()
 	{
 		// These are fields required in the config table
@@ -308,7 +307,7 @@ class ldap extends \phpbb\auth\provider\base
 			'TEMPLATE_VARS'	=> array(
 				'AUTH_LDAP_BASE_DN'		=> $new_config['ldap_base_dn'],
 				'AUTH_LDAP_EMAIL'		=> $new_config['ldap_email'],
-				'AUTH_LDAP_PASSORD'		=> $new_config['ldap_password'],
+				'AUTH_LDAP_PASSORD'		=> $new_config['ldap_password'] !== '' ? '********' : '',
 				'AUTH_LDAP_PORT'		=> $new_config['ldap_port'],
 				'AUTH_LDAP_SERVER'		=> $new_config['ldap_server'],
 				'AUTH_LDAP_UID'			=> $new_config['ldap_uid'],
--- a/phpbb/auth/provider/oauth/oauth.php
+++ b/phpbb/auth/provider/oauth/oauth.php
@@ -553,13 +553,13 @@ class oauth extends \phpbb\auth\provider\base
 	/**
 	* {@inheritdoc}
 	*/
-	public function get_auth_link_data()
+	public function get_auth_link_data($user_id = 0)
 	{
 		$block_vars = array();
 
 		// Get all external accounts tied to the current user
 		$data = array(
-			'user_id' => (int) $this->user->data['user_id'],
+			'user_id' => ($user_id <= 0) ? (int) $this->user->data['user_id'] : (int) $user_id,
 		);
 		$sql = 'SELECT oauth_provider_id, provider FROM ' . $this->auth_provider_oauth_token_account_assoc . '
 			WHERE ' . $this->db->sql_build_array('SELECT', $data);
@@ -616,10 +616,13 @@ class oauth extends \phpbb\auth\provider\base
 			return 'LOGIN_LINK_MISSING_DATA';
 		}
 
+		// Remove user specified in $link_data if possible
+		$user_id = isset($link_data['user_id']) ? $link_data['user_id'] : $this->user->data['user_id'];
+
 		// Remove the link
 		$sql = 'DELETE FROM ' . $this->auth_provider_oauth_token_account_assoc . "
 			WHERE provider = '" . $this->db->sql_escape($link_data['oauth_service']) . "'
-				AND user_id = " . (int) $this->user->data['user_id'];
+				AND user_id = " . (int) $user_id;
 		$this->db->sql_query($sql);
 
 		// Clear all tokens belonging to the user on this servce
--- a/phpbb/auth/provider/oauth/token_storage.php
+++ b/phpbb/auth/provider/oauth/token_storage.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\auth\provider\oauth;
 
-
 use OAuth\OAuth1\Token\StdOAuth1Token;
 use OAuth\Common\Token\TokenInterface;
 use OAuth\Common\Storage\TokenStorageInterface;
@@ -117,7 +116,8 @@ class token_storage implements TokenStorageInterface
 	{
 		$service = $this->get_service_name_for_db($service);
 
-		if ($this->cachedToken) {
+		if ($this->cachedToken)
+		{
 			return true;
 		}
 
@@ -232,7 +232,8 @@ class token_storage implements TokenStorageInterface
 	{
 		$service = $this->get_service_name_for_db($service);
 
-		if ($this->cachedToken instanceof TokenInterface) {
+		if ($this->cachedToken instanceof TokenInterface)
+		{
 			return $this->cachedToken;
 		}
 
--- a/phpbb/auth/provider/provider_interface.php
+++ b/phpbb/auth/provider/provider_interface.php
@@ -166,6 +166,10 @@ interface provider_interface
 	/**
 	* Returns an array of data necessary to build the ucp_auth_link page
 	*
+	* @param int $user_id User ID for whom the data should be retrieved.
+	*						defaults to 0, which is not a valid ID. The method
+	*						should fall back to the current user's ID in this
+	*						case.
 	* @return	array|null	If this function is not implemented on an auth
 	*						provider then it returns null. If it is implemented
 	*						it will return an array of up to four elements of
@@ -181,7 +185,7 @@ interface provider_interface
 	*							'VARS'				=> array(...),
 	*						)
 	*/
-	public function get_auth_link_data();
+	public function get_auth_link_data($user_id = 0);
 
 	/**
 	* Unlinks an external account from a phpBB account.
--- a/phpbb/controller/helper.php
+++ b/phpbb/controller/helper.php
@@ -13,6 +13,7 @@
 
 namespace phpbb\controller;
 
+use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Generator\UrlGenerator;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -212,12 +213,31 @@ class helper
 	public function message($message, array $parameters = array(), $title = 'INFORMATION', $code = 200)
 	{
 		array_unshift($parameters, $message);
+		$message_text = call_user_func_array(array($this->user, 'lang'), $parameters);
+		$message_title = $this->user->lang($title);
+
+		if ($this->request->is_ajax())
+		{
+			global $refresh_data;
+
+			return new JsonResponse(
+				array(
+					'MESSAGE_TITLE'		=> $message_title,
+					'MESSAGE_TEXT'		=> $message_text,
+					'S_USER_WARNING'	=> false,
+					'S_USER_NOTICE'		=> false,
+					'REFRESH_DATA'		=> (!empty($refresh_data)) ? $refresh_data : null
+				),
+				$code
+			);
+		}
+
 		$this->template->assign_vars(array(
-			'MESSAGE_TEXT'	=> call_user_func_array(array($this->user, 'lang'), $parameters),
-			'MESSAGE_TITLE'	=> $this->user->lang($title),
+			'MESSAGE_TEXT'	=> $message_text,
+			'MESSAGE_TITLE'	=> $message_title,
 		));
 
-		return $this->render('message_body.html', $this->user->lang($title), $code);
+		return $this->render('message_body.html', $message_title, $code);
 	}
 
 	/**
--- a/phpbb/db/migration/data/v310/style_update_p1.php
+++ b/phpbb/db/migration/data/v310/style_update_p1.php
@@ -136,7 +136,7 @@ class style_update_p1 extends \phpbb\db\migration\migration
 		if (!sizeof($valid_styles))
 		{
 			// No valid styles: remove everything and add prosilver
-			$this->sql_query('DELETE FROM ' . STYLES_TABLE, $errored, $error_ary);
+			$this->sql_query('DELETE FROM ' . STYLES_TABLE);
 
 			$sql_ary = array(
 				'style_name'		=> 'prosilver',
@@ -157,13 +157,13 @@ class style_update_p1 extends \phpbb\db\migration\migration
 			$this->sql_query($sql);
 
 			$sql = 'SELECT style_id
-				FROM ' . $table . "
+				FROM ' . STYLES_TABLE . "
 				WHERE style_name = 'prosilver'";
 			$result = $this->sql_query($sql);
 			$default_style = $this->db->sql_fetchfield($result);
 			$this->db->sql_freeresult($result);
 
-			set_config('default_style', $default_style);
+			$this->config->set('default_style', $default_style);
 
 			$sql = 'UPDATE ' . USERS_TABLE . ' SET user_style = 0';
 			$this->sql_query($sql);
--- /dev/null
+++ b/phpbb/db/migration/data/v31x/v315.php
@@ -0,0 +1,31 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\migration\data\v31x;
+
+class v315 extends \phpbb\db\migration\migration
+{
+	static public function depends_on()
+	{
+		return array(
+			'\phpbb\db\migration\data\v31x\v315rc1',
+		);
+	}
+
+	public function update_data()
+	{
+		return array(
+			array('config.update', array('version', '3.1.5')),
+		);
+	}
+}
--- /dev/null
+++ b/phpbb/db/migration/data/v31x/v315rc1.php
@@ -0,0 +1,31 @@
+<?php
+/**
+*
+* This file is part of the phpBB Forum Software package.
+*
+* @copyright (c) phpBB Limited <https://www.phpbb.com>
+* @license GNU General Public License, version 2 (GPL-2.0)
+*
+* For full copyright and license information, please see
+* the docs/CREDITS.txt file.
+*
+*/
+
+namespace phpbb\db\migration\data\v31x;
+
+class v315rc1 extends \phpbb\db\migration\migration
+{
+	static public function depends_on()
+	{
+		return array(
+			'\phpbb\db\migration\data\v31x\v314',
+		);
+	}
+
+	public function update_data()
+	{
+		return array(
+			array('config.update', array('version', '3.1.5-RC1')),
+		);
+	}
+}
--- a/phpbb/db/migration/tool/permission.php
+++ b/phpbb/db/migration/tool/permission.php
@@ -425,13 +425,27 @@ class permission implements \phpbb\db\migration\tool\tool_interface
 				$role_id = (int) $this->db->sql_fetchfield('auth_role_id');
 				if ($role_id)
 				{
-					$sql = 'SELECT role_name
+					$sql = 'SELECT role_name, role_type
 						FROM ' . ACL_ROLES_TABLE . '
 						WHERE role_id = ' . $role_id;
 					$this->db->sql_query($sql);
-					$role_name = $this->db->sql_fetchfield('role_name');
-
-					return $this->permission_set($role_name, $auth_option, 'role', $has_permission);
+					$role_data = $this->db->sql_fetchrow();
+					$role_name = $role_data['role_name'];
+					$role_type = $role_data['role_type'];
+
+					// Filter new auth options to match the role type: a_ | f_ | m_ | u_
+					// Set new auth options to the role only if options matching the role type were found
+					$auth_option = array_filter($auth_option,
+						function ($option) use ($role_type)
+						{
+							return strpos($option, $role_type) === 0;
+						}
+					);
+
+					if (sizeof($auth_option))
+					{
+						return $this->permission_set($role_name, $auth_option, 'role', $has_permission);
+					}
 				}
 
 				$sql = 'SELECT auth_option_id, auth_setting
--- a/phpbb/db/sql_insert_buffer.php
+++ b/phpbb/db/sql_insert_buffer.php
@@ -107,7 +107,7 @@ class sql_insert_buffer
 	* first building a huge rowset. Or at least sizeof($rows) should be kept
 	* small.
 	*
-	* @param array $rows 
+	* @param array $rows
 	*
 	* @return bool		True when some data was flushed to the database.
 	*					False otherwise.
--- a/phpbb/event/md_exporter.php
+++ b/phpbb/event/md_exporter.php
@@ -24,6 +24,12 @@ class md_exporter
 	/** @var string phpBB Root Path */
 	protected $root_path;
 
+	/** @var string The minimum version for the events to return */
+	protected $min_version;
+
+	/** @var string The maximum version for the events to return */
+	protected $max_version;
+
 	/** @var string */
 	protected $filter;
 
@@ -36,8 +42,10 @@ class md_exporter
 	/**
 	* @param string $phpbb_root_path
 	* @param mixed $extension	String 'vendor/ext' to filter, null for phpBB core
+	* @param string $min_version
+	* @param string $max_version
 	*/
-	public function __construct($phpbb_root_path, $extension = null)
+	public function __construct($phpbb_root_path, $extension = null, $min_version = null, $max_version = null)
 	{
 		$this->root_path = $phpbb_root_path;
 		$this->path = $this->root_path;
@@ -49,6 +57,8 @@ class md_exporter
 		$this->events = array();
 		$this->events_by_file = array();
 		$this->filter = $this->current_event = '';
+		$this->min_version = $min_version;
+		$this->max_version = $max_version;
 	}
 
 	/**
@@ -147,15 +157,64 @@ class md_exporter
 			}
 
 			list($file_details, $details) = explode("\n* Since: ", $details, 2);
-			list($since, $description) = explode("\n* Purpose: ", $details, 2);
+
+			$changed_versions = array();
+			if (strpos($details, "\n* Changed: ") !== false)
+			{
+				list($since, $details) = explode("\n* Changed: ", $details, 2);
+				while (strpos($details, "\n* Changed: ") !== false)
+				{
+					list($changed, $details) = explode("\n* Changed: ", $details, 2);
+					$changed_versions[] = $changed;
+				}
+				list($changed, $description) = explode("\n* Purpose: ", $details, 2);
+				$changed_versions[] = $changed;
+			}
+			else
+			{
+				list($since, $description) = explode("\n* Purpose: ", $details, 2);
+				$changed_versions = array();
+			}
 
 			$files = $this->validate_file_list($file_details);
 			$since = $this->validate_since($since);
+			$changes = array();
+			foreach ($changed_versions as $changed)
+			{
+				list($changed_version, $changed_description) = $this->validate_changed($changed);
+
+				if (isset($changes[$changed_version]))
+				{
+					throw new \LogicException("Duplicate change information found for event '{$this->current_event}'");
+				}
+
+				$changes[$changed_version] = $changed_description;
+			}
+			$description = trim($description, "\n") . "\n";
+
+			if (!$this->version_is_filtered($since))
+			{
+				$is_filtered = false;
+				foreach ($changes as $version => $null)
+				{
+					if ($this->version_is_filtered($version))
+					{
+						$is_filtered = true;
+						break;
+					}
+				}
+
+				if (!$is_filtered)
+				{
+					continue;
+				}
+			}
 
 			$this->events[$event_name] = array(
 				'event'			=> $this->current_event,
 				'files'			=> $files,
 				'since'			=> $since,
+				'changed'		=> $changes,
 				'description'	=> $description,
 			);
 		}
@@ -164,20 +223,48 @@ class md_exporter
 	}
 
 	/**
+	 * The version to check
+	 *
+	 * @param string $version
+	 * @return bool
+	 */
+	protected function version_is_filtered($version)
+	{
+		return (!$this->min_version || phpbb_version_compare($this->min_version, $version, '<='))
+		&& (!$this->max_version || phpbb_version_compare($this->max_version, $version, '>='));
+	}
+
+	/**
 	* Format the php events as a wiki table
+	*
+	* @param string $action
 	* @return string		Number of events found
 	*/
-	public function export_events_for_wiki()
+	public function export_events_for_wiki($action = '')
 	{
 		if ($this->filter === 'adm')
 		{
-			$wiki_page = '= ACP Template Events =' . "\n";
+			if ($action === 'diff')
+			{
+				$wiki_page = '=== ACP Template Events ===' . "\n";
+			}
+			else
+			{
+				$wiki_page = '= ACP Template Events =' . "\n";
+			}
 			$wiki_page .= '{| class="zebra sortable" cellspacing="0" cellpadding="5"' . "\n";
 			$wiki_page .= '! Identifier !! Placement !! Added in Release !! Explanation' . "\n";
 		}
 		else
 		{
-			$wiki_page = '= Template Events =' . "\n";
+			if ($action === 'diff')
+			{
+				$wiki_page = '=== Template Events ===' . "\n";
+			}
+			else
+			{
+				$wiki_page = '= Template Events =' . "\n";
+			}
 			$wiki_page .= '{| class="zebra sortable" cellspacing="0" cellpadding="5"' . "\n";
 			$wiki_page .= '! Identifier !! Prosilver Placement (If applicable) !! Subsilver Placement (If applicable) !! Added in Release !! Explanation' . "\n";
 		}
@@ -227,7 +314,7 @@ class md_exporter
 	*/
 	public function validate_since($since)
 	{
-		if (!preg_match('#^\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?$#', $since))
+		if (!$this->validate_version($since))
 		{
 			throw new \LogicException("Invalid since information found for event '{$this->current_event}'");
 		}
@@ -236,6 +323,44 @@ class md_exporter
 	}
 
 	/**
+	* Validate "Changed" Information
+	*
+	* @param string $changed
+	* @return string
+	* @throws \LogicException
+	*/
+	public function validate_changed($changed)
+	{
+		if (strpos($changed, ' ') !== false)
+		{
+			list($version, $description) = explode(' ', $changed, 2);
+		}
+		else
+		{
+			$version = $changed;
+			$description = '';
+		}
+
+		if (!$this->validate_version($version))
+		{
+			throw new \LogicException("Invalid changed information found for event '{$this->current_event}'");
+		}
+
+		return array($version, $description);
+	}
+
+	/**
+	* Validate "version" Information
+	*
+	* @param string $version
+	* @return bool True if valid, false otherwise
+	*/
+	public function validate_version($version)
+	{
+		return preg_match('#^\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?$#', $version);
+	}
+
+	/**
 	* Validate the files list
 	*
 	* @param string $file_details
--- a/phpbb/event/php_exporter.php
+++ b/phpbb/event/php_exporter.php
@@ -25,6 +25,12 @@ class php_exporter
 	/** @var string phpBB Root Path */
 	protected $root_path;
 
+	/** @var string The minimum version for the events to return */
+	protected $min_version;
+
+	/** @var string The maximum version for the events to return */
+	protected $max_version;
+
 	/** @var string */
 	protected $current_file;
 
@@ -43,14 +49,18 @@ class php_exporter
 	/**
 	* @param string $phpbb_root_path
 	* @param mixed $extension	String 'vendor/ext' to filter, null for phpBB core
+	* @param string $min_version
+	* @param string $max_version
 	*/
-	public function __construct($phpbb_root_path, $extension = null)
+	public function __construct($phpbb_root_path, $extension = null, $min_version = null, $max_version = null)
 	{
 		$this->root_path = $phpbb_root_path;
 		$this->path = $phpbb_root_path;
 		$this->events = $this->file_lines = array();
 		$this->current_file = $this->current_event = '';
 		$this->current_event_line = 0;
+		$this->min_version = $min_version;
+		$this->max_version = $max_version;
 
 		$this->path = $this->root_path;
 		if ($extension)
@@ -148,11 +158,20 @@ class php_exporter
 
 	/**
 	* Format the php events as a wiki table
+	*
+	* @param string $action
 	* @return string
 	*/
-	public function export_events_for_wiki()
+	public function export_events_for_wiki($action = '')
 	{
-		$wiki_page = '= PHP Events (Hook Locations) =' . "\n";
+		if ($action === 'diff')
+		{
+			$wiki_page = '=== PHP Events (Hook Locations) ===' . "\n";
+		}
+		else
+		{
+			$wiki_page = '= PHP Events (Hook Locations) =' . "\n";
+		}
 		$wiki_page .= '{| class="sortable zebra" cellspacing="0" cellpadding="5"' . "\n";
 		$wiki_page .= '! Identifier !! Placement !! Arguments !! Added in Release !! Explanation' . "\n";
 		foreach ($this->events as $event)
@@ -215,6 +234,34 @@ class php_exporter
 					$since_line_num = $this->find_since();
 					$since = $this->validate_since($this->file_lines[$since_line_num]);
 
+					$changed_line_nums = $this->find_changed('changed');
+					if (empty($changed_line_nums))
+					{
+						$changed_line_nums = $this->find_changed('change');
+					}
+					$changed_versions = array();
+					if (!empty($changed_line_nums))
+					{
+						foreach ($changed_line_nums as $changed_line_num)
+						{
+							$changed_versions[] = $this->validate_changed($this->file_lines[$changed_line_num]);
+						}
+					}
+
+					if (!$this->version_is_filtered($since))
+					{
+						$valid_version = false;
+						foreach ($changed_versions as $changed)
+						{
+							$valid_version = $valid_version || $this->version_is_filtered($changed);
+						}
+
+						if (!$valid_version)
+						{
+							continue;
+						}
+					}
+
 					// Find event description line
 					$description_line_num = $this->find_description();
 					$description = substr(trim($this->file_lines[$description_line_num]), strlen('* '));
@@ -243,6 +290,18 @@ class php_exporter
 	}
 
 	/**
+	 * The version to check
+	 *
+	 * @param string $version
+	 * @return bool
+	 */
+	protected function version_is_filtered($version)
+	{
+		return (!$this->min_version || phpbb_version_compare($this->min_version, $version, '<='))
+			&& (!$this->max_version || phpbb_version_compare($this->max_version, $version, '>='));
+	}
+
+	/**
 	* Find the name of the event inside the dispatch() line
 	*
 	* @param int $event_line
@@ -449,6 +508,33 @@ class php_exporter
 	}
 
 	/**
+	* Find the "@changed" Information lines
+	*
+	* @param string $tag_name Should be 'changed' or 'change'
+	* @return array Absolute line numbers
+	* @throws \LogicException
+	*/
+	public function find_changed($tag_name)
+	{
+		$lines = array();
+		$last_line = 0;
+		try
+		{
+			while ($line = $this->find_tag($tag_name, array('since'), $last_line))
+			{
+				$lines[] = $line;
+				$last_line = $line;
+			}
+		}
+		catch (\LogicException $e)
+		{
+			// Not changed? No problem!
+		}
+
+		return $lines;
+	}
+
+	/**
 	* Find the "@event" Information line
 	*
 	* @return int Absolute line number
@@ -464,13 +550,14 @@ class php_exporter
 	* @param string $find_tag		Name of the tag we are trying to find
 	* @param array $disallowed_tags		List of tags that must not appear between
 	*									the tag and the actual event
+	* @param int $skip_to_line		Skip lines until this one
 	* @return int Absolute line number
 	* @throws \LogicException
 	*/
-	public function find_tag($find_tag, $disallowed_tags)
+	public function find_tag($find_tag, $disallowed_tags, $skip_to_line = 0)
 	{
-		$find_tag_line = 0;
-		$found_comment_end = false;
+		$find_tag_line = $skip_to_line ? $this->current_event_line - $skip_to_line + 1 : 0;
+		$found_comment_end = ($skip_to_line) ? true : false;
 		while (strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t "), '* @' . $find_tag . ' ') !== 0)
 		{
 			if ($found_comment_end && ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t") === '/**')
@@ -561,6 +648,27 @@ class php_exporter
 	}
 
 	/**
+	* Validate "@changed" Information
+	*
+	* @param string $line
+	* @return string
+	* @throws \LogicException
+	*/
+	public function validate_changed($line)
+	{
+		$match = array();
+		$line = str_replace("\t", ' ', ltrim($line, "\t "));
+		preg_match('#^\* @change(d)? (\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?)( (?:.*))?$#', $line, $match);
+		if (!isset($match[2]))
+		{
+			throw new \LogicException("Invalid '@changed' information for event "
+				. "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'");
+		}
+
+		return $match[2];
+	}
+
+	/**
 	* Validate "@event" Information
 	*
 	* @param string $event_name
--- a/phpbb/log/log.php
+++ b/phpbb/log/log.php
@@ -521,15 +521,77 @@ class log implements \phpbb\log\log_interface
 			$sql_keywords = $this->generate_sql_keyword($keywords);
 		}
 
-		if ($count_logs)
-		{
-			$sql = 'SELECT COUNT(l.log_id) AS total_entries
-				FROM ' . $this->log_table . ' l, ' . USERS_TABLE . ' u
-				WHERE l.log_type = ' . (int) $log_type . '
+		$get_logs_sql_ary = array(
+			'SELECT' => 'l.*, u.username, u.username_clean, u.user_colour',
+			'FROM' => array(
+						$this->log_table => 'l',
+						USERS_TABLE => 'u',
+					),
+			'WHERE' => 'l.log_type = ' . (int) $log_type . "
 					AND l.user_id = u.user_id
-					AND l.log_time >= ' . (int) $log_time . "
 					$sql_keywords
-					$sql_additional";
+					$sql_additional",
+
+			'ORDER_BY' => $sort_by,
+		);
+
+		if($log_time)
+		{
+			$get_logs_sql_ary['WHERE'] = 'l.log_time >= ' . (int) $log_time . '
+					AND ' . $get_logs_sql_ary['WHERE'];
+		}
+
+		/**
+		* Modify the query to obtain the logs data
+		*
+		* @event core.get_logs_main_query_before
+		* @var	array	get_logs_sql_ary	The array in the format of the query builder with the query
+		*									to get the log count and the log list
+		* @var	string	mode				Mode of the entries we display
+		* @var	bool	count_logs			Do we count all matching entries?
+		* @var	int		limit				Limit the number of entries
+		* @var	int		offset				Offset when fetching the entries
+		* @var	mixed	forum_id			Limit entries to the forum_id,
+		*									can also be an array of forum_ids
+		* @var	int		topic_id			Limit entries to the topic_id
+		* @var	int		user_id				Limit entries to the user_id
+		* @var	int		log_time			Limit maximum age of log entries
+		* @var	string	sort_by				SQL order option
+		* @var	string	keywords			Will only return entries that have the
+		*									keywords in log_operation or log_data
+		* @var	string	profile_url			URL to the users profile
+		* @var	int		log_type			Limit logs to a certain type. If log_type
+		*									is false, no entries will be returned.
+		* @var	string	sql_additional		Additional conditions for the entries,
+		*									e.g.: 'AND l.forum_id = 1'
+		* @since 3.1.5-RC1
+		*/
+		$vars = array(
+			'get_logs_sql_ary',
+			'mode',
+			'count_logs',
+			'limit',
+			'offset',
+			'forum_id',
+			'topic_id',
+			'user_id',
+			'log_time',
+			'sort_by',
+			'keywords',
+			'profile_url',
+			'log_type',
+			'sql_additional',
+		);
+		extract($this->dispatcher->trigger_event('core.get_logs_main_query_before', compact($vars)));
+
+		if ($count_logs)
+		{
+			$count_logs_sql_ary = $get_logs_sql_ary;
+
+			$count_logs_sql_ary['SELECT'] = 'COUNT(l.log_id) AS total_entries';
+			unset($count_logs_sql_ary['ORDER_BY']);
+
+			$sql = $this->db->sql_build_query('SELECT', $count_logs_sql_ary);
 			$result = $this->db->sql_query($sql);
 			$this->entry_count = (int) $this->db->sql_fetchfield('total_entries');
 			$this->db->sql_freeresult($result);
@@ -548,14 +610,7 @@ class log implements \phpbb\log\log_interface
 			}
 		}
 
-		$sql = 'SELECT l.*, u.username, u.username_clean, u.user_colour
-			FROM ' . $this->log_table . ' l, ' . USERS_TABLE . ' u
-			WHERE l.log_type = ' . (int) $log_type . '
-				AND u.user_id = l.user_id
-				' . (($log_time) ? 'AND l.log_time >= ' . (int) $log_time : '') . "
-				$sql_keywords
-				$sql_additional
-			ORDER BY $sort_by";
+		$sql = $this->db->sql_build_query('SELECT', $get_logs_sql_ary);
 		$result = $this->db->sql_query_limit($sql, $limit, $this->last_page_offset);
 
 		$i = 0;
--- a/phpbb/notification/method/base.php
+++ b/phpbb/notification/method/base.php
@@ -60,7 +60,7 @@ abstract class base implements \phpbb\notification\method\method_interface
 
 	/**
 	* Notification Method Base Constructor
-	* 
+	*
 	* @param \phpbb\user_loader $user_loader
 	* @param \phpbb\db\driver\driver_interface $db
 	* @param \phpbb\cache\driver\driver_interface $cache
@@ -85,7 +85,7 @@ abstract class base implements \phpbb\notification\method\method_interface
 
 	/**
 	* Set notification manager (required)
-	* 
+	*
 	* @param \phpbb\notification\manager $notification_manager
 	*/
 	public function set_notification_manager(\phpbb\notification\manager $notification_manager)
--- a/phpbb/passwords/driver/helper.php
+++ b/phpbb/passwords/driver/helper.php
@@ -153,11 +153,23 @@ class helper
 	 */
 	public function string_compare($string_a, $string_b)
 	{
-		$difference = strlen($string_a) != strlen($string_b);
+		// Return if input variables are not strings or if length does not match
+		if (!is_string($string_a) || !is_string($string_b) || strlen($string_a) != strlen($string_b))
+		{
+			return false;
+		}
+
+		// Use hash_equals() if it's available
+		if (function_exists('hash_equals'))
+		{
+			return hash_equals($string_a, $string_b);
+		}
+
+		$difference = 0;
 
 		for ($i = 0; $i < strlen($string_a) && $i < strlen($string_b); $i++)
 		{
-			$difference |= $string_a[$i] != $string_b[$i];
+			$difference |= ord($string_a[$i]) ^ ord($string_b[$i]);
 		}
 
 		return $difference === 0;
--- a/phpbb/passwords/manager.php
+++ b/phpbb/passwords/manager.php
@@ -56,7 +56,7 @@ class manager
 	* @param array $hashing_algorithms Hashing driver
 	*			service collection
 	* @param \phpbb\passwords\helper $helper Passwords helper object
-	* @param string $defaults List of default driver types
+	* @param array $defaults List of default driver types
 	*/
 	public function __construct(\phpbb\config\config $config, $hashing_algorithms, helper $helper, $defaults)
 	{
--- a/phpbb/permissions.php
+++ b/phpbb/permissions.php
@@ -277,13 +277,14 @@ class permissions
 		'm_approve'		=> array('lang' => 'ACL_M_APPROVE', 'cat' => 'post_actions'),
 		'm_report'		=> array('lang' => 'ACL_M_REPORT', 'cat' => 'post_actions'),
 		'm_chgposter'	=> array('lang' => 'ACL_M_CHGPOSTER', 'cat' => 'post_actions'),
+		'm_info'		=> array('lang' => 'ACL_M_INFO', 'cat' => 'post_actions'),
+		'm_softdelete'	=> array('lang' => 'ACL_M_SOFTDELETE', 'cat' => 'post_actions'),
 
 		'm_move'	=> array('lang' => 'ACL_M_MOVE', 'cat' => 'topic_actions'),
 		'm_lock'	=> array('lang' => 'ACL_M_LOCK', 'cat' => 'topic_actions'),
 		'm_split'	=> array('lang' => 'ACL_M_SPLIT', 'cat' => 'topic_actions'),
 		'm_merge'	=> array('lang' => 'ACL_M_MERGE', 'cat' => 'topic_actions'),
 
-		'm_info'	=> array('lang' => 'ACL_M_INFO', 'cat' => 'misc'),
 		'm_warn'	=> array('lang' => 'ACL_M_WARN', 'cat' => 'misc'),
 		'm_ban'		=> array('lang' => 'ACL_M_BAN', 'cat' => 'misc'),
 
--- a/phpbb/plupload/plupload.php
+++ b/phpbb/plupload/plupload.php
@@ -267,8 +267,8 @@ class plupload
 		{
 			$resize = sprintf(
 				'resize: {width: %d, height: %d, quality: 100},',
-				(int) $this->config['img_max_height'],
-				(int) $this->config['img_max_width']
+				(int) $this->config['img_max_width'],
+				(int) $this->config['img_max_height']
 			);
 		}
 
--- a/phpbb/search/fulltext_mysql.php
+++ b/phpbb/search/fulltext_mysql.php
@@ -43,6 +43,12 @@ class fulltext_mysql extends \phpbb\search\base
 	protected $db;
 
 	/**
+	 * phpBB event dispatcher object
+	 * @var \phpbb\event\dispatcher_interface
+	 */
+	protected $phpbb_dispatcher;
+
+	/**
 	 * User object
 	 * @var \phpbb\user
 	 */
@@ -79,11 +85,13 @@ class fulltext_mysql extends \phpbb\search\base
 	 * @param \phpbb\config\config $config Config object
 	 * @param \phpbb\db\driver\driver_interface Database object
 	 * @param \phpbb\user $user User object
+	 * @param \phpbb\event\dispatcher_interface	$phpbb_dispatcher	Event dispatcher object
 	 */
-	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user)
+	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
 	{
 		$this->config = $config;
 		$this->db = $db;
+		$this->phpbb_dispatcher = $phpbb_dispatcher;
 		$this->user = $user;
 
 		$this->word_length = array('min' => $this->config['fulltext_mysql_min_word_len'], 'max' => $this->config['fulltext_mysql_max_word_len']);
@@ -447,6 +455,55 @@ class fulltext_mysql extends \phpbb\search\base
 			break;
 		}
 
+		$search_query = $this->search_query;
+
+		/**
+		* Allow changing the query used to search for posts using fulltext_mysql
+		*
+		* @event core.search_mysql_keywords_main_query_before
+		* @var	string	search_query		The parsed keywords used for this search
+		* @var	int		result_count		The previous result count for the format of the query.
+		*									Set to 0 to force a re-count
+		* @var	bool	join_topic			Weather or not TOPICS_TABLE should be CROSS JOIN'ED
+		* @var	array	author_ary			Array of user_id containing the users to filter the results to
+		* @var	string	author_name			An extra username to search on (!empty(author_ary) must be true, to be relevant)
+		* @var	array	ex_fid_ary			Which forums not to search on
+		* @var	int		topic_id			Limit the search to this topic_id only
+		* @var	string	sql_sort_table		Extra tables to include in the SQL query.
+		*									Used in conjunction with sql_sort_join
+		* @var	string	sql_sort_join		SQL conditions to join all the tables used together.
+		*									Used in conjunction with sql_sort_table
+		* @var	int		sort_days			Time, in days, of the oldest possible post to list
+		* @var	string	sql_match			Which columns to do the search on.
+		* @var	string	sql_match_where		Extra conditions to use to properly filter the matching process
+		* @var	string	sort_by_sql			The possible predefined sort types
+		* @var	string	sort_key			The sort type used from the possible sort types
+		* @var	string	sort_dir			"a" for ASC or "d" dor DESC for the sort order used
+		* @var	string	sql_sort			The result SQL when processing sort_by_sql + sort_key + sort_dir
+		* @var	int		start				How many posts to skip in the search results (used for pagination)
+		* @since 3.1.5-RC1
+		*/
+		$vars = array(
+			'search_query',
+			'result_count',
+			'join_topic',
+			'author_ary',
+			'author_name',
+			'ex_fid_ary',
+			'topic_id',
+			'sql_sort_table',
+			'sql_sort_join',
+			'sort_days',
+			'sql_match',
+			'sql_match_where',
+			'sort_by_sql',
+			'sort_key',
+			'sort_dir',
+			'sql_sort',
+			'start',
+		);
+		extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_keywords_main_query_before', compact($vars)));
+
 		$sql_select			= (!$result_count) ? 'SQL_CALC_FOUND_ROWS ' : '';
 		$sql_select			= ($type == 'posts') ? $sql_select . 'p.post_id' : 'DISTINCT ' . $sql_select . 't.topic_id';
 		$sql_from			= ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
@@ -620,6 +677,55 @@ class fulltext_mysql extends \phpbb\search\base
 
 		$m_approve_fid_sql = ' AND ' . $post_visibility;
 
+		/**
+		* Allow changing the query used to search for posts by author in fulltext_mysql
+		*
+		* @event core.search_mysql_author_query_before
+		* @var	int		result_count		The previous result count for the format of the query.
+		*									Set to 0 to force a re-count
+		* @var	string	sql_sort_table		CROSS JOIN'ed table to allow doing the sort chosen
+		* @var	string	sql_sort_join		Condition to define how to join the CROSS JOIN'ed table specifyed in sql_sort_table
+		* @var	array	author_ary			Array of user_id containing the users to filter the results to
+		* @var	string	author_name			An extra username to search on
+		* @var	string	sql_author			SQL WHERE condition for the post author ids
+		* @var	int		topic_id			Limit the search to this topic_id only
+		* @var	string	sql_topic_id		SQL of topic_id
+		* @var	string	sort_by_sql			The possible predefined sort types
+		* @var	string	sort_key			The sort type used from the possible sort types
+		* @var	string	sort_dir			"a" for ASC or "d" dor DESC for the sort order used
+		* @var	string	sql_sort			The result SQL when processing sort_by_sql + sort_key + sort_dir
+		* @var	string	sort_days			Time, in days, that the oldest post showing can have
+		* @var	string	sql_time			The SQL to search on the time specifyed by sort_days
+		* @var	bool	firstpost_only		Wether or not to search only on the first post of the topics
+		* @var	array	ex_fid_ary			Forum ids that must not be searched on
+		* @var	array	sql_fora			SQL query for ex_fid_ary
+		* @var	string	m_approve_fid_sql	WHERE clause condition on post_visibility restrictions
+		* @var	int		start				How many posts to skip in the search results (used for pagination)
+		* @since 3.1.5-RC1
+		*/
+		$vars = array(
+			'result_count',
+			'sql_sort_table',
+			'sql_sort_join',
+			'author_ary',
+			'author_name',
+			'sql_author',
+			'topic_id',
+			'sql_topic_id',
+			'sort_by_sql',
+			'sort_key',
+			'sort_dir',
+			'sql_sort',
+			'sort_days',
+			'sql_time',
+			'firstpost_only',
+			'ex_fid_ary',
+			'sql_fora',
+			'm_approve_fid_sql',
+			'start',
+		);
+		extract($this->phpbb_dispatcher->trigger_event('core.search_mysql_author_query_before', compact($vars)));
+
 		// If the cache was completely empty count the results
 		$calc_results = ($result_count) ? '' : 'SQL_CALC_FOUND_ROWS ';
 
--- a/phpbb/search/fulltext_native.php
+++ b/phpbb/search/fulltext_native.php
@@ -87,6 +87,12 @@ class fulltext_native extends \phpbb\search\base
 	protected $db;
 
 	/**
+	 * phpBB event dispatcher object
+	 * @var \phpbb\event\dispatcher_interface
+	 */
+	protected $phpbb_dispatcher;
+
+	/**
 	 * User object
 	 * @var \phpbb\user
 	 */
@@ -96,13 +102,15 @@ class fulltext_native extends \phpbb\search\base
 	* Initialises the fulltext_native search backend with min/max word length and makes sure the UTF-8 normalizer is loaded
 	*
 	* @param	boolean|string	&$error	is passed by reference and should either be set to false on success or an error message on failure
+	* @param	\phpbb\event\dispatcher_interface	$phpbb_dispatcher	Event dispatcher object
 	*/
-	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user)
+	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
 	{
 		$this->phpbb_root_path = $phpbb_root_path;
 		$this->php_ext = $phpEx;
 		$this->config = $config;
 		$this->db = $db;
+		$this->phpbb_dispatcher = $phpbb_dispatcher;
 		$this->user = $user;
 
 		$this->word_length = array('min' => $this->config['fulltext_native_min_chars'], 'max' => $this->config['fulltext_native_max_chars']);
@@ -712,6 +720,68 @@ class fulltext_native extends \phpbb\search\base
 
 		$sql_where[] = $post_visibility;
 
+		$search_query = $this->search_query;
+		$must_exclude_one_ids = $this->must_exclude_one_ids;
+		$must_not_contain_ids = $this->must_not_contain_ids;
+		$must_contain_ids = $this->must_contain_ids;
+
+		/**
+		* Allow changing the query used for counting for posts using fulltext_native
+		*
+		* @event core.search_native_keywords_count_query_before
+		* @var	string	search_query			The parsed keywords used for this search
+		* @var	array	must_not_contain_ids	Ids that cannot be taken into account for the results
+		* @var	array	must_exclude_one_ids	Ids that cannot be on the results
+		* @var	array	must_contain_ids		Ids that must be on the results
+		* @var	int		result_count			The previous result count for the format of the query
+		*										Set to 0 to force a re-count
+		* @var	bool	join_topic				Weather or not TOPICS_TABLE should be CROSS JOIN'ED
+		* @var	array	author_ary				Array of user_id containing the users to filter the results to
+		* @var	string	author_name				An extra username to search on (!empty(author_ary) must be true, to be relevant)
+		* @var	array	ex_fid_ary				Which forums not to search on
+		* @var	int		topic_id				Limit the search to this topic_id only
+		* @var	string	sql_sort_table			Extra tables to include in the SQL query.
+		*										Used in conjunction with sql_sort_join
+		* @var	string	sql_sort_join			SQL conditions to join all the tables used together.
+		*										Used in conjunction with sql_sort_table
+		* @var	int		sort_days				Time, in days, of the oldest possible post to list
+		* @var	string	sql_where				An array of the current WHERE clause conditions
+		* @var	string	sql_match				Which columns to do the search on
+		* @var	string	sql_match_where			Extra conditions to use to properly filter the matching process
+		* @var	string	group_by				Whether or not the SQL query requires a GROUP BY for the elements in the SELECT clause
+		* @var	string	sort_by_sql				The possible predefined sort types
+		* @var	string	sort_key				The sort type used from the possible sort types
+		* @var	string	sort_dir				"a" for ASC or "d" dor DESC for the sort order used
+		* @var	string	sql_sort				The result SQL when processing sort_by_sql + sort_key + sort_dir
+		* @var	int		start					How many posts to skip in the search results (used for pagination)
+		* @since 3.1.5-RC1
+		*/
+		$vars = array(
+			'search_query',
+			'must_not_contain_ids',
+			'must_exclude_one_ids',
+			'must_contain_ids',
+			'result_count',
+			'join_topic',
+			'author_ary',
+			'author_name',
+			'ex_fid_ary',
+			'topic_id',
+			'sql_sort_table',
+			'sql_sort_join',
+			'sort_days',
+			'sql_where',
+			'sql_match',
+			'sql_match_where',
+			'group_by',
+			'sort_by_sql',
+			'sort_key',
+			'sort_dir',
+			'sql_sort',
+			'start',
+		);
+		extract($this->phpbb_dispatcher->trigger_event('core.search_native_keywords_count_query_before', compact($vars)));
+
 		if ($topic_id)
 		{
 			$sql_where[] = 'p.topic_id = ' . $topic_id;
@@ -972,6 +1042,49 @@ class fulltext_native extends \phpbb\search\base
 		$select = ($type == 'posts') ? 'p.post_id' : 't.topic_id';
 		$is_mysql = false;
 
+		/**
+		* Allow changing the query used to search for posts by author in fulltext_native
+		*
+		* @event core.search_native_author_count_query_before
+		* @var	int		total_results		The previous result count for the format of the query.
+		*									Set to 0 to force a re-count
+		* @var	string	select				SQL SELECT clause for what to get
+		* @var	string	sql_sort_table		CROSS JOIN'ed table to allow doing the sort chosen
+		* @var	string	sql_sort_join		Condition to define how to join the CROSS JOIN'ed table specifyed in sql_sort_table
+		* @var	array	sql_author			SQL WHERE condition for the post author ids
+		* @var	int		topic_id			Limit the search to this topic_id only
+		* @var	string	sort_by_sql			The possible predefined sort types
+		* @var	string	sort_key			The sort type used from the possible sort types
+		* @var	string	sort_dir			"a" for ASC or "d" dor DESC for the sort order used
+		* @var	string	sql_sort			The result SQL when processing sort_by_sql + sort_key + sort_dir
+		* @var	string	sort_days			Time, in days, that the oldest post showing can have
+		* @var	string	sql_time			The SQL to search on the time specifyed by sort_days
+		* @var	bool	firstpost_only		Wether or not to search only on the first post of the topics
+		* @var	array	ex_fid_ary			Forum ids that must not be searched on
+		* @var	array	sql_fora			SQL query for ex_fid_ary
+		* @var	int		start				How many posts to skip in the search results (used for pagination)
+		* @since 3.1.5-RC1
+		*/
+		$vars = array(
+			'total_results',
+			'select',
+			'sql_sort_table',
+			'sql_sort_join',
+			'sql_author',
+			'topic_id',
+			'sort_by_sql',
+			'sort_key',
+			'sort_dir',
+			'sql_sort',
+			'sort_days',
+			'sql_time',
+			'firstpost_only',
+			'ex_fid_ary',
+			'sql_fora',
+			'start',
+		);
+		extract($this->phpbb_dispatcher->trigger_event('core.search_native_author_count_query_before', compact($vars)));
+
 		// If the cache was completely empty count the results
 		if (!$total_results)
 		{
@@ -1075,7 +1188,7 @@ class fulltext_native extends \phpbb\search\base
 			// Count rows for the executed queries. Replace $select within $sql with SQL_CALC_FOUND_ROWS, and run it.
 			$sql_calc = str_replace('SELECT ' . $select, 'SELECT DISTINCT SQL_CALC_FOUND_ROWS p.post_id', $sql);
 
-			$this->db->sql_query($sql_calc);
+			$result = $this->db->sql_query($sql_calc);
 			$this->db->sql_freeresult($result);
 
 			$sql_count = 'SELECT FOUND_ROWS() as total_results';
--- a/phpbb/search/fulltext_postgres.php
+++ b/phpbb/search/fulltext_postgres.php
@@ -56,6 +56,12 @@ class fulltext_postgres extends \phpbb\search\base
 	protected $db;
 
 	/**
+	 * phpBB event dispatcher object
+	 * @var \phpbb\event\dispatcher_interface
+	 */
+	protected $phpbb_dispatcher;
+
+	/**
 	 * User object
 	 * @var \phpbb\user
 	 */
@@ -92,11 +98,13 @@ class fulltext_postgres extends \phpbb\search\base
 	 * @param \phpbb\config\config $config Config object
 	 * @param \phpbb\db\driver\driver_interface Database object
 	 * @param \phpbb\user $user User object
+	 * @param \phpbb\event\dispatcher_interface	$phpbb_dispatcher	Event dispatcher object
 	 */
-	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user)
+	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
 	{
 		$this->config = $config;
 		$this->db = $db;
+		$this->phpbb_dispatcher = $phpbb_dispatcher;
 		$this->user = $user;
 
 		$this->word_length = array('min' => $this->config['fulltext_postgres_min_word_len'], 'max' => $this->config['fulltext_postgres_max_word_len']);
@@ -409,6 +417,55 @@ class fulltext_postgres extends \phpbb\search\base
 			break;
 		}
 
+		$tsearch_query = $this->tsearch_query;
+
+		/**
+		* Allow changing the query used to search for posts using fulltext_postgres
+		*
+		* @event core.search_postgres_keywords_main_query_before
+		* @var	string	tsearch_query		The parsed keywords used for this search
+		* @var	int		result_count		The previous result count for the format of the query.
+		*									Set to 0 to force a re-count
+		* @var	bool	join_topic			Weather or not TOPICS_TABLE should be CROSS JOIN'ED
+		* @var	array	author_ary			Array of user_id containing the users to filter the results to
+		* @var	string	author_name			An extra username to search on (!empty(author_ary) must be true, to be relevant)
+		* @var	array	ex_fid_ary			Which forums not to search on
+		* @var	int		topic_id			Limit the search to this topic_id only
+		* @var	string	sql_sort_table		Extra tables to include in the SQL query.
+		*									Used in conjunction with sql_sort_join
+		* @var	string	sql_sort_join		SQL conditions to join all the tables used together.
+		*									Used in conjunction with sql_sort_table
+		* @var	int		sort_days			Time, in days, of the oldest possible post to list
+		* @var	string	sql_match			Which columns to do the search on.
+		* @var	string	sql_match_where		Extra conditions to use to properly filter the matching process
+		* @var	string	sort_by_sql			The possible predefined sort types
+		* @var	string	sort_key			The sort type used from the possible sort types
+		* @var	string	sort_dir			"a" for ASC or "d" dor DESC for the sort order used
+		* @var	string	sql_sort			The result SQL when processing sort_by_sql + sort_key + sort_dir
+		* @var	int		start				How many posts to skip in the search results (used for pagination)
+		* @since 3.1.5-RC1
+		*/
+		$vars = array(
+			'tsearch_query',
+			'result_count',
+			'join_topic',
+			'author_ary',
+			'author_name',
+			'ex_fid_ary',
+			'topic_id',
+			'sql_sort_table',
+			'sql_sort_join',
+			'sort_days',
+			'sql_match',
+			'sql_match_where',
+			'sort_by_sql',
+			'sort_key',
+			'sort_dir',
+			'sql_sort',
+			'start',
+		);
+		extract($this->phpbb_dispatcher->trigger_event('core.search_postgres_keywords_main_query_before', compact($vars)));
+
 		$sql_select			= ($type == 'posts') ? 'p.post_id' : 'DISTINCT t.topic_id';
 		$sql_from			= ($join_topic) ? TOPICS_TABLE . ' t, ' : '';
 		$field				= ($type == 'posts') ? 'post_id' : 'topic_id';
@@ -595,6 +652,55 @@ class fulltext_postgres extends \phpbb\search\base
 
 		$m_approve_fid_sql = ' AND ' . $post_visibility;
 
+		/**
+		* Allow changing the query used to search for posts by author in fulltext_postgres
+		*
+		* @event core.search_postgres_author_count_query_before
+		* @var	int		result_count		The previous result count for the format of the query.
+		*									Set to 0 to force a re-count
+		* @var	string	sql_sort_table		CROSS JOIN'ed table to allow doing the sort chosen
+		* @var	string	sql_sort_join		Condition to define how to join the CROSS JOIN'ed table specifyed in sql_sort_table
+		* @var	array	author_ary			Array of user_id containing the users to filter the results to
+		* @var	string	author_name			An extra username to search on
+		* @var	string	sql_author			SQL WHERE condition for the post author ids
+		* @var	int		topic_id			Limit the search to this topic_id only
+		* @var	string	sql_topic_id		SQL of topic_id
+		* @var	string	sort_by_sql			The possible predefined sort types
+		* @var	string	sort_key			The sort type used from the possible sort types
+		* @var	string	sort_dir			"a" for ASC or "d" dor DESC for the sort order used
+		* @var	string	sql_sort			The result SQL when processing sort_by_sql + sort_key + sort_dir
+		* @var	string	sort_days			Time, in days, that the oldest post showing can have
+		* @var	string	sql_time			The SQL to search on the time specifyed by sort_days
+		* @var	bool	firstpost_only		Wether or not to search only on the first post of the topics
+		* @var	array	ex_fid_ary			Forum ids that must not be searched on
+		* @var	array	sql_fora			SQL query for ex_fid_ary
+		* @var	string	m_approve_fid_sql	WHERE clause condition on post_visibility restrictions
+		* @var	int		start				How many posts to skip in the search results (used for pagination)
+		* @since 3.1.5-RC1
+		*/
+		$vars = array(
+			'result_count',
+			'sql_sort_table',
+			'sql_sort_join',
+			'author_ary',
+			'author_name',
+			'sql_author',
+			'topic_id',
+			'sql_topic_id',
+			'sort_by_sql',
+			'sort_key',
+			'sort_dir',
+			'sql_sort',
+			'sort_days',
+			'sql_time',
+			'firstpost_only',
+			'ex_fid_ary',
+			'sql_fora',
+			'm_approve_fid_sql',
+			'start',
+		);
+		extract($this->phpbb_dispatcher->trigger_event('core.search_postgres_author_count_query_before', compact($vars)));
+
 		// Build the query for really selecting the post_ids
 		if ($type == 'posts')
 		{
--- a/phpbb/search/fulltext_sphinx.php
+++ b/phpbb/search/fulltext_sphinx.php
@@ -96,6 +96,12 @@ class fulltext_sphinx
 	protected $dbtype;
 
 	/**
+	 * phpBB event dispatcher object
+	 * @var \phpbb\event\dispatcher_interface
+	 */
+	protected $phpbb_dispatcher;
+
+	/**
 	 * User object
 	 * @var \phpbb\user
 	 */
@@ -125,12 +131,14 @@ class fulltext_sphinx
 	 * @param \phpbb\config\config $config Config object
 	 * @param \phpbb\db\driver\driver_interface Database object
 	 * @param \phpbb\user $user User object
+	 * @param \phpbb\event\dispatcher_interface	$phpbb_dispatcher	Event dispatcher object
 	 */
-	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user)
+	public function __construct(&$error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher)
 	{
 		$this->phpbb_root_path = $phpbb_root_path;
 		$this->php_ext = $phpEx;
 		$this->config = $config;
+		$this->phpbb_dispatcher = $phpbb_dispatcher;
 		$this->user = $user;
 		$this->db = $db;
 		$this->auth = $auth;
--- a/phpbb/template/twig/loader.php
+++ b/phpbb/template/twig/loader.php
@@ -97,7 +97,8 @@ class loader extends \Twig_Loader_Filesystem
 
 		// If this is in the cache we can skip the entire process below
 		//	as it should have already been validated
-		if (isset($this->cache[$name])) {
+		if (isset($this->cache[$name]))
+		{
 			return $this->cache[$name];
 		}
 
--- a/phpbb/template/twig/node/definenode.php
+++ b/phpbb/template/twig/node/definenode.php
@@ -14,7 +14,6 @@
 
 namespace phpbb\template\twig\node;
 
-
 class definenode extends \Twig_Node
 {
 	public function __construct($capture, \Twig_NodeInterface $name, \Twig_NodeInterface $value, $lineno, $tag = null)
@@ -31,7 +30,8 @@ class definenode extends \Twig_Node
 	{
 		$compiler->addDebugInfo($this);
 
-		if ($this->getAttribute('capture')) {
+		if ($this->getAttribute('capture'))
+		{
 			$compiler
 				->write("ob_start();\n")
 				->subcompile($this->getNode('value'))
--- a/phpbb/template/twig/node/event.php
+++ b/phpbb/template/twig/node/event.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\template\twig\node;
 
-
 class event extends \Twig_Node
 {
 	/**
--- a/phpbb/template/twig/node/expression/binary/equalequal.php
+++ b/phpbb/template/twig/node/expression/binary/equalequal.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\template\twig\node\expression\binary;
 
-
 class equalequal extends \Twig_Node_Expression_Binary
 {
 	public function operator(\Twig_Compiler $compiler)
--- a/phpbb/template/twig/node/expression/binary/notequalequal.php
+++ b/phpbb/template/twig/node/expression/binary/notequalequal.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\template\twig\node\expression\binary;
 
-
 class notequalequal extends \Twig_Node_Expression_Binary
 {
 	public function operator(\Twig_Compiler $compiler)
--- a/phpbb/template/twig/node/includenode.php
+++ b/phpbb/template/twig/node/includenode.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\template\twig\node;
 
-
 class includenode extends \Twig_Node_Include
 {
 	/**
--- a/phpbb/template/twig/node/includephp.php
+++ b/phpbb/template/twig/node/includephp.php
@@ -14,7 +14,6 @@
 
 namespace phpbb\template\twig\node;
 
-
 class includephp extends \Twig_Node
 {
 	/** @var \Twig_Environment */
@@ -47,7 +46,8 @@ class includephp extends \Twig_Node
 			return;
 		}
 
-		if ($this->getAttribute('ignore_missing')) {
+		if ($this->getAttribute('ignore_missing'))
+		{
 			$compiler
 				->write("try {\n")
 				->indent()
@@ -76,7 +76,8 @@ class includephp extends \Twig_Node
 			->write("}\n")
 		;
 
-		if ($this->getAttribute('ignore_missing')) {
+		if ($this->getAttribute('ignore_missing'))
+		{
 			$compiler
 				->outdent()
 				->write("} catch (\Twig_Error_Loader \$e) {\n")
--- a/phpbb/template/twig/node/php.php
+++ b/phpbb/template/twig/node/php.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\template\twig\node;
 
-
 class php extends \Twig_Node
 {
 	/** @var \Twig_Environment */
--- a/phpbb/template/twig/tokenparser/defineparser.php
+++ b/phpbb/template/twig/tokenparser/defineparser.php
@@ -14,7 +14,6 @@
 
 namespace phpbb\template\twig\tokenparser;
 
-
 class defineparser extends \Twig_TokenParser
 {
 	/**
@@ -33,7 +32,8 @@ class defineparser extends \Twig_TokenParser
 		$name = $this->parser->getExpressionParser()->parseExpression();
 
 		$capture = false;
-		if ($stream->test(\Twig_Token::OPERATOR_TYPE, '=')) {
+		if ($stream->test(\Twig_Token::OPERATOR_TYPE, '='))
+		{
 			$stream->next();
 			$value = $this->parser->getExpressionParser()->parseExpression();
 
@@ -45,7 +45,9 @@ class defineparser extends \Twig_TokenParser
 			}
 
 			$stream->expect(\Twig_Token::BLOCK_END_TYPE);
-		} else {
+		}
+		else
+		{
 			$capture = true;
 
 			$stream->expect(\Twig_Token::BLOCK_END_TYPE);
--- a/phpbb/template/twig/tokenparser/event.php
+++ b/phpbb/template/twig/tokenparser/event.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\template\twig\tokenparser;
 
-
 class event extends \Twig_TokenParser
 {
 	/**
--- a/phpbb/template/twig/tokenparser/includejs.php
+++ b/phpbb/template/twig/tokenparser/includejs.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\template\twig\tokenparser;
 
-
 class includejs extends \Twig_TokenParser
 {
 	/**
--- a/phpbb/template/twig/tokenparser/includeparser.php
+++ b/phpbb/template/twig/tokenparser/includeparser.php
@@ -14,7 +14,6 @@
 
 namespace phpbb\template\twig\tokenparser;
 
-
 class includeparser extends \Twig_TokenParser_Include
 {
 	/**
--- a/phpbb/template/twig/tokenparser/includephp.php
+++ b/phpbb/template/twig/tokenparser/includephp.php
@@ -14,7 +14,6 @@
 
 namespace phpbb\template\twig\tokenparser;
 
-
 class includephp extends \Twig_TokenParser
 {
 	/**
@@ -31,7 +30,8 @@ class includephp extends \Twig_TokenParser
 		$stream = $this->parser->getStream();
 
 		$ignoreMissing = false;
-		if ($stream->test(\Twig_Token::NAME_TYPE, 'ignore')) {
+		if ($stream->test(\Twig_Token::NAME_TYPE, 'ignore'))
+		{
 			$stream->next();
 			$stream->expect(\Twig_Token::NAME_TYPE, 'missing');
 
--- a/phpbb/template/twig/tokenparser/php.php
+++ b/phpbb/template/twig/tokenparser/php.php
@@ -13,7 +13,6 @@
 
 namespace phpbb\template\twig\tokenparser;
 
-
 class php extends \Twig_TokenParser
 {
 	/**
--- a/phpbb/user.php
+++ b/phpbb/user.php
@@ -637,7 +637,7 @@ class user extends \phpbb\session
 				$lang_path = $this->lang_path;
 			}
 
-			if (strpos($phpbb_root_path . $filename, $lang_path . $this->lang_name . '/') === 0)
+			if (strpos($phpbb_root_path . $filename, $lang_path) === 0)
 			{
 				$language_filename = $phpbb_root_path . $filename;
 			}
--- a/posting.php
+++ b/posting.php
@@ -35,7 +35,6 @@ $forum_id	= request_var('f', 0);
 $draft_id	= request_var('d', 0);
 $lastclick	= request_var('lastclick', 0);
 
-$submit		= (isset($_POST['post'])) ? true : false;
 $preview	= (isset($_POST['preview'])) ? true : false;
 $save		= (isset($_POST['save'])) ? true : false;
 $load		= (isset($_POST['load'])) ? true : false;
@@ -43,6 +42,7 @@ $confirm	= $request->is_set_post('confirm');
 $cancel		= (isset($_POST['cancel']) && !isset($_POST['save'])) ? true : false;
 
 $refresh	= (isset($_POST['add_file']) || isset($_POST['delete_file']) || isset($_POST['cancel_unglobalise']) || $save || $load || $preview);
+$submit = $request->is_set_post('post') && !$refresh && !$preview;
 $mode		= request_var('mode', '');
 
 // If the user is not allowed to delete the post, we try to soft delete it, so we overwrite the mode here.
@@ -1241,6 +1241,7 @@ if ($submit || $preview || $refresh)
 	*
 	* @event core.posting_modify_submission_errors
 	* @var	array	post_data	Array with post data
+	* @var	array	poll		Array with poll data from post (must be used instead of the post_data equivalent)
 	* @var	string	mode		What action to take if the form is submitted
 	*				post|reply|quote|edit|delete|bump|smilies|popup
 	* @var	string	page_title	Title of the mode page
@@ -1251,9 +1252,11 @@ if ($submit || $preview || $refresh)
 	* @var	array	error		Any error strings; a non-empty array aborts form submission.
 	*				NOTE: Should be actual language strings, NOT language keys.
 	* @since 3.1.0-RC5
+	* @change 3.1.5-RC1 Added poll array to the event
 	*/
 	$vars = array(
 		'post_data',
+		'poll',
 		'mode',
 		'page_title',
 		'post_id',
@@ -1791,6 +1794,30 @@ $page_data = array(
 	'S_IN_POSTING'			=> true,
 );
 
+// Build custom bbcodes array
+display_custom_bbcodes();
+
+// Poll entry
+if (($mode == 'post' || ($mode == 'edit' && $post_id == $post_data['topic_first_post_id']/* && (!$post_data['poll_last_vote'] || $auth->acl_get('m_edit', $forum_id))*/))
+	&& $auth->acl_get('f_poll', $forum_id))
+{
+	$page_data = array_merge($page_data, array(
+		'S_SHOW_POLL_BOX'		=> true,
+		'S_POLL_VOTE_CHANGE'	=> ($auth->acl_get('f_votechg', $forum_id) && $auth->acl_get('f_vote', $forum_id)),
+		'S_POLL_DELETE'			=> ($mode == 'edit' && sizeof($post_data['poll_options']) && ((!$post_data['poll_last_vote'] && $post_data['poster_id'] == $user->data['user_id'] && $auth->acl_get('f_delete', $forum_id)) || $auth->acl_get('m_delete', $forum_id))),
+		'S_POLL_DELETE_CHECKED'	=> (!empty($poll_delete)) ? true : false,
+
+		'L_POLL_OPTIONS_EXPLAIN'	=> $user->lang('POLL_OPTIONS_' . (($mode == 'edit') ? 'EDIT_' : '') . 'EXPLAIN', (int) $config['max_poll_options']),
+
+		'VOTE_CHANGE_CHECKED'	=> (!empty($post_data['poll_vote_change'])) ? ' checked="checked"' : '',
+		'POLL_TITLE'			=> (isset($post_data['poll_title'])) ? $post_data['poll_title'] : '',
+		'POLL_OPTIONS'			=> (!empty($post_data['poll_options'])) ? implode("\n", $post_data['poll_options']) : '',
+		'POLL_MAX_OPTIONS'		=> (isset($post_data['poll_max_options'])) ? (int) $post_data['poll_max_options'] : 1,
+		'POLL_LENGTH'			=> $post_data['poll_length'],
+		)
+	);
+}
+
 /**
 * This event allows you to modify template variables for the posting screen
 *
@@ -1829,6 +1856,7 @@ $page_data = array(
 *		post_id, topic_id, forum_id, submit, preview, save, load,
 *		delete, cancel, refresh, error, page_data, message_parser
 * @change 3.1.2-RC1 Removed 'delete' var as it does not exist
+* @change 3.1.5-RC1 Added poll variables to the page_data array
 */
 $vars = array(
 	'post_data',
@@ -1857,29 +1885,6 @@ extract($phpbb_dispatcher->trigger_event('core.posting_modify_template_vars', co
 // Start assigning vars for main posting page ...
 $template->assign_vars($page_data);
 
-// Build custom bbcodes array
-display_custom_bbcodes();
-
-// Poll entry
-if (($mode == 'post' || ($mode == 'edit' && $post_id == $post_data['topic_first_post_id']/* && (!$post_data['poll_last_vote'] || $auth->acl_get('m_edit', $forum_id))*/))
-	&& $auth->acl_get('f_poll', $forum_id))
-{
-	$template->assign_vars(array(
-		'S_SHOW_POLL_BOX'		=> true,
-		'S_POLL_VOTE_CHANGE'	=> ($auth->acl_get('f_votechg', $forum_id) && $auth->acl_get('f_vote', $forum_id)),
-		'S_POLL_DELETE'			=> ($mode == 'edit' && sizeof($post_data['poll_options']) && ((!$post_data['poll_last_vote'] && $post_data['poster_id'] == $user->data['user_id'] && $auth->acl_get('f_delete', $forum_id)) || $auth->acl_get('m_delete', $forum_id))),
-		'S_POLL_DELETE_CHECKED'	=> (!empty($poll_delete)) ? true : false,
-
-		'L_POLL_OPTIONS_EXPLAIN'	=> $user->lang('POLL_OPTIONS_' . (($mode == 'edit') ? 'EDIT_' : '') . 'EXPLAIN', (int) $config['max_poll_options']),
-
-		'VOTE_CHANGE_CHECKED'	=> (!empty($post_data['poll_vote_change'])) ? ' checked="checked"' : '',
-		'POLL_TITLE'			=> (isset($post_data['poll_title'])) ? $post_data['poll_title'] : '',
-		'POLL_OPTIONS'			=> (!empty($post_data['poll_options'])) ? implode("\n", $post_data['poll_options']) : '',
-		'POLL_MAX_OPTIONS'		=> (isset($post_data['poll_max_options'])) ? (int) $post_data['poll_max_options'] : 1,
-		'POLL_LENGTH'			=> $post_data['poll_length'])
-	);
-}
-
 // Show attachment box for adding attachments if true
 $allowed = ($auth->acl_get('f_attach', $forum_id) && $auth->acl_get('u_attach') && $config['allow_attachments'] && $form_enctype);
 
--- a/search.php
+++ b/search.php
@@ -274,7 +274,7 @@ if ($keywords || $author || $author_id || $search_id || $submit)
 	}
 	// We do some additional checks in the module to ensure it can actually be utilised
 	$error = false;
-	$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user);
+	$search = new $search_type($error, $phpbb_root_path, $phpEx, $auth, $config, $db, $user, $phpbb_dispatcher);
 
 	if ($error)
 	{
--- a/styles/prosilver/style.cfg
+++ b/styles/prosilver/style.cfg
@@ -21,8 +21,8 @@
 # General Information about this style
 name = prosilver
 copyright = © phpBB Limited, 2007
-style_version = 3.1.4
-phpbb_version = 3.1.4
+style_version = 3.1.5
+phpbb_version = 3.1.5
 
 # Defining a different template bitfield
 # template_bitfield = lNg=
--- a/styles/prosilver/template/ajax.js
+++ b/styles/prosilver/template/ajax.js
@@ -9,9 +9,9 @@ phpbb.addAjaxCallback('mark_forums_read', function(res) {
 	var readTitle = res.NO_UNREAD_POSTS;
 	var unreadTitle = res.UNREAD_POSTS;
 	var iconsArray = {
-		'forum_unread': 'forum_read',
-		'forum_unread_subforum': 'forum_read_subforum',
-		'forum_unread_locked': 'forum_read_locked'
+		forum_unread: 'forum_read',
+		forum_unread_subforum: 'forum_read_subforum',
+		forum_unread_locked: 'forum_read_locked'
 	};
 
 	$('li.row').find('dl[class*="forum_unread"]').each(function() {
@@ -39,20 +39,20 @@ phpbb.addAjaxCallback('mark_forums_read', function(res) {
 	phpbb.closeDarkenWrapper(3000);
 });
 
-/** 
+/**
 * This callback will mark all topic icons read
 *
-* @param update_topic_links bool Whether "Mark topics read" links should be
-*     updated. Defaults to true.
+* @param {bool} [update_topic_links=true] Whether "Mark topics read" links
+* 	should be updated. Defaults to true.
 */
 phpbb.addAjaxCallback('mark_topics_read', function(res, updateTopicLinks) {
 	var readTitle = res.NO_UNREAD_POSTS;
 	var unreadTitle = res.UNREAD_POSTS;
 	var iconsArray = {
-		'global_unread': 'global_read',
-		'announce_unread': 'announce_read',
-		'sticky_unread': 'sticky_read',
-		'topic_unread': 'topic_read'
+		global_unread: 'global_read',
+		announce_unread: 'announce_read',
+		sticky_unread: 'sticky_read',
+		topic_unread: 'topic_read'
 	};
 	var iconsState = ['', '_hot', '_hot_mine', '_locked', '_locked_mine', '_mine'];
 	var unreadClassSelectors;
@@ -138,9 +138,9 @@ phpbb.markNotifications = function($popup, unreadCount) {
 	}
 
 	// Update page title
-	$('title').text(
-		(unreadCount ? '(' + unreadCount + ')' : '') + $('title').text().replace(/(\(([0-9])\))/, '')
-	);
+	var $title = $('title');
+	var originalTitle = $title.text().replace(/(\((\d+)\))/, '');
+	$title.text((unreadCount ? '(' + unreadCount + ')' : '') + originalTitle);
 };
 
 // This callback finds the post from the delete link, and removes it.
@@ -205,7 +205,7 @@ phpbb.addAjaxCallback('vote_poll', function(res) {
 
 		// Set min-height to prevent the page from jumping when the content changes
 		var updatePanelHeight = function (height) {
-			var height = (typeof height === 'undefined') ? panel.find('.inner').outerHeight() : height;
+			height = (typeof height === 'undefined') ? panel.find('.inner').outerHeight() : height;
 			panel.css('min-height', height);
 		};
 		updatePanelHeight();
@@ -223,7 +223,7 @@ phpbb.addAjaxCallback('vote_poll', function(res) {
 			// If the user can still vote, simply slide down the results
 			poll.find('.resultbar, .poll_option_percent, .poll_total_votes').show(500);
 		}
-		
+
 		// Get the votes count of the highest poll option
 		poll.find('[data-poll-option-id]').each(function() {
 			var option = $(this);
@@ -252,7 +252,7 @@ phpbb.addAjaxCallback('vote_poll', function(res) {
 			var newBarClass = (percent === 100) ? 'pollbar5' : 'pollbar' + (Math.floor(percent / 20) + 1);
 
 			setTimeout(function () {
-				bar.animate({width: percentRel + '%'}, 500)
+				bar.animate({ width: percentRel + '%' }, 500)
 					.removeClass('pollbar1 pollbar2 pollbar3 pollbar4 pollbar5')
 					.addClass(newBarClass)
 					.html(res.vote_counts[optionId]);
@@ -287,10 +287,10 @@ phpbb.addAjaxCallback('vote_poll', function(res) {
 			var panelHeight = panel.height();
 			var innerHeight = panel.find('.inner').outerHeight();
 
-			if (panelHeight != innerHeight) {
-				panel.css({'min-height': '', 'height': panelHeight})
-					.animate({height: innerHeight}, time, function () {
-						panel.css({'min-height': innerHeight, 'height': ''});
+			if (panelHeight !== innerHeight) {
+				panel.css({ minHeight: '', height: panelHeight })
+					.animate({ height: innerHeight }, time, function () {
+						panel.css({ minHeight: innerHeight, height: '' });
 					});
 			}
 		};
@@ -377,8 +377,9 @@ $('#member_search').click(function () {
 * Automatically resize textarea
 */
 $(function() {
-	phpbb.resizeTextArea($('textarea:not(#message-box textarea, .no-auto-resize)'), {minHeight: 75, maxHeight: 250});
-	phpbb.resizeTextArea($('#message-box textarea'));
+	var $textarea = $('textarea:not(#message-box textarea, .no-auto-resize)');
+	phpbb.resizeTextArea($textarea, { minHeight: 75, maxHeight: 250 });
+	phpbb.resizeTextArea($('textarea', '#message-box'));
 });
 
 
--- a/styles/prosilver/template/forum_fn.js
+++ b/styles/prosilver/template/forum_fn.js
@@ -1,3 +1,5 @@
+/* global phpbb */
+
 /**
 * phpBB3 forum functions
 */
@@ -37,7 +39,7 @@ function pageJump(item) {
 		baseUrl = item.attr('data-base-url'),
 		startName = item.attr('data-start-name');
 
-	if (page !== null && !isNaN(page) && page == Math.floor(page) && page > 0) {
+	if (page !== null && !isNaN(page) && page === Math.floor(page) && page > 0) {
 		if (baseUrl.indexOf('?') === -1) {
 			document.location.href = baseUrl + '?' + startName + '=' + ((page - 1) * perPage);
 		} else {
@@ -124,7 +126,7 @@ function activateSubPanel(p, panels) {
 
 	var i, showPanel;
 
-	if (typeof(p) === 'string') {
+	if (typeof p === 'string') {
 		showPanel = p;
 	}
 	$('input[name="show_panel"]').val(showPanel);
@@ -277,8 +279,7 @@ jQuery(function($) {
 /**
 * Functions for user search popup
 */
-function insertUser(formId, value)
-{
+function insertUser(formId, value) {
 	'use strict';
 
 	var $form = jQuery(formId),
@@ -286,7 +287,7 @@ function insertUser(formId, value)
 		fieldName = $form.attr('data-field-name'),
 		item = opener.document.forms[formName][fieldName];
 
-	if (item.value.length && item.type == 'textarea') {
+	if (item.value.length && item.type === 'textarea') {
 		value = item.value + '\n' + value;
 	}
 
@@ -319,7 +320,7 @@ function parseDocument($container) {
 	'use strict';
 
 	var test = document.createElement('div'),
-		oldBrowser = (typeof test.style.borderRadius == 'undefined'),
+		oldBrowser = (typeof test.style.borderRadius === 'undefined'),
 		$body = $('body');
 
 	/**
@@ -359,7 +360,10 @@ function parseDocument($container) {
 	*/
 	if (oldBrowser) {
 		// Fix .linklist.bulletin lists
-		$container.find('ul.linklist.bulletin > li:first-child, ul.linklist.bulletin > li.rightside:last-child').addClass('no-bulletin');
+		$container
+			.find('ul.linklist.bulletin > li')
+			.filter(':first-child, .rightside:last-child')
+			.addClass('no-bulletin');
 	}
 
 	/**
@@ -420,12 +424,12 @@ function parseDocument($container) {
 				width;
 
 			// Test max-width set in code for .navlinks above
-			width = parseInt($this.css('max-width'));
+			width = parseInt($this.css('max-width'), 10);
 			if (!width) {
- 				width = $body.width();
+				width = $body.width();
 			}
 
-			maxHeight = parseInt($this.css('line-height'));
+			maxHeight = parseInt($this.css('line-height'), 10);
 			$links.each(function() {
 				if ($(this).height() > 0) {
 					maxHeight = Math.max(maxHeight, $(this).outerHeight(true));
@@ -452,8 +456,8 @@ function parseDocument($container) {
 				return;
 			}
 
-			for (var i = 0; i < classesLength; i ++) {
-				for (var j = length - 1; j >= 0; j --) {
+			for (var i = 0; i < classesLength; i++) {
+				for (var j = length - 1; j >= 0; j--) {
 					$links.eq(j).addClass('wrapped ' + classes[i]);
 					if ($this.height() <= maxHeight) {
 						return;
@@ -470,7 +474,9 @@ function parseDocument($container) {
 	/**
 	* Responsive link lists
 	*/
-	$container.find('.linklist:not(.navlinks, [data-skip-responsive]), .postbody .post-buttons:not([data-skip-responsive])').each(function() {
+	var selector = '.linklist:not(.navlinks, [data-skip-responsive]),' +
+		'.postbody .post-buttons:not([data-skip-responsive])';
+	$container.find(selector).each(function() {
 		var $this = $(this),
 			filterSkip = '.breadcrumbs, [data-skip-responsive]',
 			filterLast = '.edit-icon, .quote-icon, [data-last-responsive]',
@@ -478,7 +484,7 @@ function parseDocument($container) {
 			$linksNotSkip = $linksAll.not(filterSkip), // All items that can potentially be hidden
 			$linksFirst = $linksNotSkip.not(filterLast), // The items that will be hidden first
 			$linksLast = $linksNotSkip.filter(filterLast), // The items that will be hidden last
-			persistent = $this.attr('id') == 'nav-main', // Does this list already have a menu (such as quick-links)?
+			persistent = $this.attr('id') === 'nav-main', // Does this list already have a menu (such as quick-links)?
 			html = '<li class="responsive-menu hidden"><a href="javascript:void(0);" class="responsive-menu-link">&nbsp;</a><div class="dropdown hidden"><div class="pointer"><div class="pointer-inner" /></div><ul class="dropdown-contents" /></div></li>',
 			slack = 3; // Vertical slack space (in pixels). Determines how sensitive the script is in determining whether a line-break has occured.
 
@@ -675,7 +681,7 @@ function parseDocument($container) {
 				$children = column.children(),
 				html = column.html();
 
-			if ($children.length == 1 && $children.text() == column.text()) {
+			if ($children.length === 1 && $children.text() === column.text()) {
 				html = $children.html();
 			}
 
@@ -717,8 +723,7 @@ function parseDocument($container) {
 			if (!$block.length) {
 				$this.find('dt > .list-inner').append('<div class="responsive-show" style="display:none;" />');
 				$block = $this.find('dt .responsive-show:last-child');
-			}
-			else {
+			} else {
 				first = ($.trim($block.text()).length === 0);
 			}
 
@@ -728,7 +733,7 @@ function parseDocument($container) {
 					children = column.children(),
 					html = column.html();
 
-				if (children.length == 1 && children.text() == column.text()) {
+				if (children.length === 1 && children.text() === column.text()) {
 					html = children.html();
 				}
 
@@ -757,7 +762,7 @@ function parseDocument($container) {
 		// Find each header
 		$th.each(function(column) {
 			var cell = $(this),
-				colspan = parseInt(cell.attr('colspan')),
+				colspan = parseInt(cell.attr('colspan'), 10),
 				dfn = cell.attr('data-dfn'),
 				text = dfn ? dfn : cell.text();
 
@@ -788,14 +793,14 @@ function parseDocument($container) {
 				cells = row.children('td'),
 				column = 0;
 
-			if (cells.length == 1) {
+			if (cells.length === 1) {
 				row.addClass('big-column');
 				return;
 			}
 
 			cells.each(function() {
 				var cell = $(this),
-					colspan = parseInt(cell.attr('colspan')),
+					colspan = parseInt(cell.attr('colspan'), 10),
 					text = $.trim(cell.text());
 
 				if (headersLength <= column) {
@@ -871,19 +876,26 @@ function parseDocument($container) {
 				total = $availableTabs.length,
 				i, $tab;
 
-			for (i = total - 1; i >= 0; i --) {
+			for (i = total - 1; i >= 0; i--) {
 				$tab = $availableTabs.eq(i);
 				$menu.prepend($tab.clone(true).removeClass('tab'));
 				$tab.hide();
 				if ($this.height() <= maxHeight) {
-					$menu.find('a').click(function() { check(true); });
+					$menu.find('a').click(function() {
+						check(true);
+					});
 					return;
 				}
 			}
-			$menu.find('a').click(function() { check(true); });
+			$menu.find('a').click(function() {
+				check(true);
+			});
 		}
 
-		phpbb.registerDropdown($item.find('a.responsive-tab-link'), $item.find('.dropdown'), {visibleClass: 'activetab'});
+		var $tabLink = $item.find('a.responsive-tab-link');
+		phpbb.registerDropdown($tabLink, $item.find('.dropdown'), {
+			visibleClass: 'activetab'
+		});
 
 		check(true);
 		$(window).resize(check);
--- a/styles/prosilver/template/forumlist_body.html
+++ b/styles/prosilver/template/forumlist_body.html
@@ -13,12 +13,14 @@
 			<div class="inner">
 			<ul class="topiclist">
 				<li class="header">
+					<!-- EVENT forumlist_body_category_header_row_prepend -->
 					<dl class="icon">
 						<dt><div class="list-inner"><!-- IF forumrow.S_IS_CAT --><a href="{forumrow.U_VIEWFORUM}">{forumrow.FORUM_NAME}</a><!-- ELSE -->{L_FORUM}<!-- ENDIF --></div></dt>
 						<dd class="topics">{L_TOPICS}</dd>
 						<dd class="posts">{L_POSTS}</dd>
 						<dd class="lastpost"><span>{L_LAST_POST}</span></dd>
 					</dl>
+					<!-- EVENT forumlist_body_category_header_row_append -->
 				</li>
 			</ul>
 			<ul class="topiclist forums">
--- a/styles/prosilver/template/index_body.html
+++ b/styles/prosilver/template/index_body.html
@@ -19,15 +19,15 @@
 	<form method="post" action="{S_LOGIN_ACTION}" class="headerspace">
 	<h3><a href="{U_LOGIN_LOGOUT}">{L_LOGIN_LOGOUT}</a><!-- IF S_REGISTER_ENABLED -->&nbsp; &bull; &nbsp;<a href="{U_REGISTER}">{L_REGISTER}</a><!-- ENDIF --></h3>
 		<fieldset class="quick-login">
-			<label for="username"><span>{L_USERNAME}{L_COLON}</span> <input type="text" name="username" id="username" size="10" class="inputbox" title="{L_USERNAME}" /></label>
-			<label for="password"><span>{L_PASSWORD}{L_COLON}</span> <input type="password" name="password" id="password" size="10" class="inputbox" title="{L_PASSWORD}" /></label>
+			<label for="username"><span>{L_USERNAME}{L_COLON}</span> <input type="text" tabindex="1" name="username" id="username" size="10" class="inputbox" title="{L_USERNAME}" /></label>
+			<label for="password"><span>{L_PASSWORD}{L_COLON}</span> <input type="password" tabindex="2" name="password" id="password" size="10" class="inputbox" title="{L_PASSWORD}" autocomplete="off" /></label>
 			<!-- IF U_SEND_PASSWORD -->
 				<a href="{U_SEND_PASSWORD}">{L_FORGOT_PASS}</a>
 			<!-- ENDIF -->
 			<!-- IF S_AUTOLOGIN_ENABLED -->
-				<span class="responsive-hide">|</span> <label for="autologin">{L_LOG_ME_IN} <input type="checkbox" name="autologin" id="autologin" /></label>
+				<span class="responsive-hide">|</span> <label for="autologin">{L_LOG_ME_IN} <input type="checkbox" tabindex="4" name="autologin" id="autologin" /></label>
 			<!-- ENDIF -->
-			<input type="submit" name="login" value="{L_LOGIN}" class="button2" />
+			<input type="submit" tabindex="5" name="login" value="{L_LOGIN}" class="button2" />
 			{S_LOGIN_REDIRECT}
 		</fieldset>
 	</form>
--- a/styles/prosilver/template/login_body.html
+++ b/styles/prosilver/template/login_body.html
@@ -15,7 +15,7 @@
 		</dl>
 		<dl>
 			<dt><label for="{PASSWORD_CREDENTIAL}">{L_PASSWORD}{L_COLON}</label></dt>
-			<dd><input type="password" tabindex="2" id="{PASSWORD_CREDENTIAL}" name="{PASSWORD_CREDENTIAL}" size="25" class="inputbox autowidth" /></dd>
+			<dd><input type="password" tabindex="2" id="{PASSWORD_CREDENTIAL}" name="{PASSWORD_CREDENTIAL}" size="25" class="inputbox autowidth" autocomplete="off" /></dd>
 			<!-- IF S_DISPLAY_FULL_LOGIN and (U_SEND_PASSWORD or U_RESEND_ACTIVATION) -->
 				<!-- IF U_SEND_PASSWORD --><dd><a href="{U_SEND_PASSWORD}">{L_FORGOT_PASS}</a></dd><!-- ENDIF -->
 				<!-- IF U_RESEND_ACTIVATION --><dd><a href="{U_RESEND_ACTIVATION}">{L_RESEND_ACTIVATION}</a></dd><!-- ENDIF -->
--- a/styles/prosilver/template/login_forum.html
+++ b/styles/prosilver/template/login_forum.html
@@ -22,7 +22,7 @@
 
 			<dl>
 				<dt><label for="password">{L_PASSWORD}{L_COLON}</label></dt>
-				<dd><input type="password" tabindex="1" id="password" name="password" size="25" class="inputbox narrow" /></dd>
+				<dd><input type="password" tabindex="1" id="password" name="password" size="25" class="inputbox narrow" autocomplete="off" /></dd>
 			</dl>
 			{S_LOGIN_REDIRECT}
 			<dl>
--- a/styles/prosilver/template/mcp_post.html
+++ b/styles/prosilver/template/mcp_post.html
@@ -144,7 +144,7 @@
 	</div>
 </div>
 
-<!-- IF S_CAN_LOCK_POST or S_CAN_DELETE_POST or S_CAN_CHGPOSTER -->
+<!-- IF S_CAN_LOCK_POST or S_CAN_DELETE_POST or S_CAN_CHGPOSTER or S_MCP_POST_ADDITIONAL_OPTS -->
 	<div class="panel">
 		<div class="inner">
 
@@ -168,6 +168,8 @@
 			</form>
 		<!-- ENDIF -->
 
+		<!-- EVENT mcp_post_additional_options -->
+
 		<!-- IF S_CAN_LOCK_POST or S_CAN_DELETE_POST -->
 			<form method="post" id="mcp" action="{U_MCP_ACTION}">
 
--- a/styles/prosilver/template/overall_footer.html
+++ b/styles/prosilver/template/overall_footer.html
@@ -18,7 +18,6 @@
 	<div id="darkenwrapper" data-ajax-error-title="{L_AJAX_ERROR_TITLE}" data-ajax-error-text="{L_AJAX_ERROR_TEXT}" data-ajax-error-text-abort="{L_AJAX_ERROR_TEXT_ABORT}" data-ajax-error-text-timeout="{L_AJAX_ERROR_TEXT_TIMEOUT}" data-ajax-error-text-parsererror="{L_AJAX_ERROR_TEXT_PARSERERROR}">
 		<div id="darken">&nbsp;</div>
 	</div>
-	<div id="loading_indicator"></div>
 
 	<div id="phpbb_alert" class="phpbb_alert" data-l-err="{L_ERROR}" data-l-timeout-processing-req="{L_TIMEOUT_PROCESSING_REQ}">
 		<a href="#" class="alert_close"></a>
--- a/styles/prosilver/template/overall_header.html
+++ b/styles/prosilver/template/overall_header.html
@@ -7,17 +7,17 @@
 <title><!-- IF UNREAD_NOTIFICATIONS_COUNT -->({UNREAD_NOTIFICATIONS_COUNT}) <!-- ENDIF --><!-- IF not S_VIEWTOPIC and not S_VIEWFORUM -->{SITENAME} - <!-- ENDIF --><!-- IF S_IN_MCP -->{L_MCP} - <!-- ELSEIF S_IN_UCP -->{L_UCP} - <!-- ENDIF -->{PAGE_TITLE}<!-- IF S_VIEWTOPIC or S_VIEWFORUM --> - {SITENAME}<!-- ENDIF --></title>
 
 <!-- IF S_ENABLE_FEEDS -->
-	<!-- IF S_ENABLE_FEEDS_OVERALL --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {SITENAME}" href="{U_FEED}" /><!-- ENDIF -->
-	<!-- IF S_ENABLE_FEEDS_NEWS --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_FEED_NEWS}" href="{U_FEED}?mode=news" /><!-- ENDIF -->
-	<!-- IF S_ENABLE_FEEDS_FORUMS --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_ALL_FORUMS}" href="{U_FEED}?mode=forums" /><!-- ENDIF -->
-	<!-- IF S_ENABLE_FEEDS_TOPICS --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_FEED_TOPICS_NEW}" href="{U_FEED}?mode=topics" /><!-- ENDIF -->
-	<!-- IF S_ENABLE_FEEDS_TOPICS_ACTIVE --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_FEED_TOPICS_ACTIVE}" href="{U_FEED}?mode=topics_active" /><!-- ENDIF -->
-	<!-- IF S_ENABLE_FEEDS_FORUM and S_FORUM_ID --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_FORUM} - {FORUM_NAME}" href="{U_FEED}?f={S_FORUM_ID}" /><!-- ENDIF -->
-	<!-- IF S_ENABLE_FEEDS_TOPIC and S_TOPIC_ID --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_TOPIC} - {TOPIC_TITLE}" href="{U_FEED}?f={S_FORUM_ID}&amp;t={S_TOPIC_ID}" /><!-- ENDIF -->
+	<!-- IF S_ENABLE_FEEDS_OVERALL --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {SITENAME}" href="{U_FEED}"><!-- ENDIF -->
+	<!-- IF S_ENABLE_FEEDS_NEWS --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_FEED_NEWS}" href="{U_FEED}?mode=news"><!-- ENDIF -->
+	<!-- IF S_ENABLE_FEEDS_FORUMS --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_ALL_FORUMS}" href="{U_FEED}?mode=forums"><!-- ENDIF -->
+	<!-- IF S_ENABLE_FEEDS_TOPICS --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_FEED_TOPICS_NEW}" href="{U_FEED}?mode=topics"><!-- ENDIF -->
+	<!-- IF S_ENABLE_FEEDS_TOPICS_ACTIVE --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_FEED_TOPICS_ACTIVE}" href="{U_FEED}?mode=topics_active"><!-- ENDIF -->
+	<!-- IF S_ENABLE_FEEDS_FORUM and S_FORUM_ID --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_FORUM} - {FORUM_NAME}" href="{U_FEED}?f={S_FORUM_ID}"><!-- ENDIF -->
+	<!-- IF S_ENABLE_FEEDS_TOPIC and S_TOPIC_ID --><link rel="alternate" type="application/atom+xml" title="{L_FEED} - {L_TOPIC} - {TOPIC_TITLE}" href="{U_FEED}?f={S_FORUM_ID}&amp;t={S_TOPIC_ID}"><!-- ENDIF -->
 <!-- ENDIF -->
 
 <!-- IF U_CANONICAL -->
-	<link rel="canonical" href="{U_CANONICAL}" />
+	<link rel="canonical" href="{U_CANONICAL}">
 <!-- ENDIF -->
 
 <!--
@@ -27,22 +27,36 @@
 	Modified by:
 -->
 
-<link href="{T_THEME_PATH}/print.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="print" title="printonly" />
-<!-- IF S_ALLOW_CDN --><link href="//fonts.googleapis.com/css?family=Open+Sans:600&amp;subset=latin,cyrillic-ext,latin-ext,cyrillic,greek-ext,greek,vietnamese" rel="stylesheet" type="text/css" media="screen, projection" /><!-- ENDIF -->
-<link href="{T_STYLESHEET_LINK}" rel="stylesheet" type="text/css" media="screen, projection" />
-<link href="{T_STYLESHEET_LANG_LINK}" rel="stylesheet" type="text/css" media="screen, projection" />
-<link href="{T_THEME_PATH}/responsive.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="only screen and (max-width: 700px), only screen and (max-device-width: 700px)" />
+<!-- IF S_ALLOW_CDN -->
+<script>
+	WebFontConfig = {
+		google: {
+			families: ['Open Sans:n6']
+		}
+	};
+
+	(function(d) {
+		var wf = d.createElement('script'), s = d.scripts[0];
+		wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js';
+		wf.async = true;
+		s.parentNode.insertBefore(wf, s);
+	})(document);
+</script>
+<!-- ENDIF -->
+<link href="{T_STYLESHEET_LINK}" rel="stylesheet">
+<link href="{T_STYLESHEET_LANG_LINK}" rel="stylesheet">
+<link href="{T_THEME_PATH}/responsive.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" media="all and (max-width: 700px), all and (max-device-width: 700px)">
 
 <!-- IF S_CONTENT_DIRECTION eq 'rtl' -->
-	<link href="{T_THEME_PATH}/bidi.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" />
+	<link href="{T_THEME_PATH}/bidi.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet">
 <!-- ENDIF -->
 
 <!-- IF S_PLUPLOAD -->
-	<link href="{T_THEME_PATH}/plupload.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" />
+	<link href="{T_THEME_PATH}/plupload.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet">
 <!-- ENDIF -->
 
 <!--[if lte IE 9]>
-	<link href="{T_THEME_PATH}/tweaks.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" />
+	<link href="{T_THEME_PATH}/tweaks.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet">
 <![endif]-->
 
 <!-- EVENT overall_header_head_append -->
--- a/styles/prosilver/template/posting_poll_body.html
+++ b/styles/prosilver/template/posting_poll_body.html
@@ -6,13 +6,14 @@
 	<!-- ENDIF -->
 
 	<fieldset class="fields2">
+	<!-- IF S_POLL_DELETE -->
+		<dl>
+			<dt><label for="poll_delete">{L_POLL_DELETE}{L_COLON}</label></dt>
+			<dd><label for="poll_delete"><input type="checkbox" name="poll_delete" id="poll_delete"<!-- IF S_POLL_DELETE_CHECKED --> checked="checked"<!-- ENDIF --> /> </label></dd>
+		</dl>
+	<!-- ENDIF -->
+
 	<!-- IF S_SHOW_POLL_BOX -->
-		<!-- IF S_POLL_DELETE -->
-			<dl>
-				<dt><label for="poll_delete">{L_POLL_DELETE}{L_COLON}</label></dt>
-				<dd><label for="poll_delete"><input type="checkbox" name="poll_delete" id="poll_delete"<!-- IF S_POLL_DELETE_CHECKED --> checked="checked"<!-- ENDIF --> /> </label></dd>
-			</dl>
-		<!-- ENDIF -->
 		<dl>
 			<dt><label for="poll_title">{L_POLL_QUESTION}{L_COLON}</label></dt>
 			<dd><input type="text" name="poll_title" id="poll_title" maxlength="255" value="{POLL_TITLE}" class="inputbox" /></dd>
@@ -44,14 +45,8 @@
 			</dl>
 		<!-- ENDIF -->
 	<!-- ENDIF -->
-	<!-- EVENT posting_poll_body_options_after -->
 
-	<!-- IF S_POLL_DELETE -->
-		<dl class="fields1">
-			<dt><label for="poll_delete">{L_POLL_DELETE}{L_COLON}</label></dt>
-			<dd><label for="poll_delete"><input type="checkbox" name="poll_delete" id="poll_delete"<!-- IF S_POLL_DELETE_CHECKED --> checked="checked"<!-- ENDIF --> /> </label></dd>
-		</dl>
-	<!-- ENDIF -->
+	<!-- EVENT posting_poll_body_options_after -->
 	</fieldset>
 
 	</div>
--- a/styles/prosilver/template/search_body.html
+++ b/styles/prosilver/template/search_body.html
@@ -2,6 +2,7 @@
 
 <h2 class="solo">{L_SEARCH}</h2>
 
+<!-- EVENT search_body_form_before -->
 <form method="get" action="{S_SEARCH_ACTION}" data-focus="keywords">
 
 <div class="panel">
--- a/styles/prosilver/template/simple_header.html
+++ b/styles/prosilver/template/simple_header.html
@@ -6,18 +6,32 @@
 {META}
 <title>{SITENAME} &bull; <!-- IF S_IN_MCP -->{L_MCP} &bull; <!-- ELSEIF S_IN_UCP -->{L_UCP} &bull; <!-- ENDIF -->{PAGE_TITLE}</title>
 
-<link href="{T_THEME_PATH}/print.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="print" title="printonly" />
-<!-- IF S_ALLOW_CDN --><link href="//fonts.googleapis.com/css?family=Open+Sans:600&amp;subset=latin,cyrillic-ext,latin-ext,cyrillic,greek-ext,greek,vietnamese" rel="stylesheet" type="text/css" media="screen, projection" /><!-- ENDIF -->
-<link href="{T_STYLESHEET_LINK}" rel="stylesheet" type="text/css" media="screen, projection" />
-<link href="{T_STYLESHEET_LANG_LINK}" rel="stylesheet" type="text/css" media="screen, projection" />
-<link href="{T_THEME_PATH}/responsive.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="only screen and (max-width: 700px), only screen and (max-device-width: 700px)" />
+<!-- IF S_ALLOW_CDN -->
+<script>
+	WebFontConfig = {
+		google: {
+			families: ['Open Sans:n6']
+		}
+	};
+
+	(function(d) {
+		var wf = d.createElement('script'), s = d.scripts[0];
+		wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js';
+		wf.async = true;
+		s.parentNode.insertBefore(wf, s);
+	})(document);
+</script>
+<!-- ENDIF -->
+<link href="{T_STYLESHEET_LINK}" rel="stylesheet">
+<link href="{T_STYLESHEET_LANG_LINK}" rel="stylesheet">
+<link href="{T_THEME_PATH}/responsive.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" media="only screen and (max-width: 700px), only screen and (max-device-width: 700px)">
 
 <!-- IF S_CONTENT_DIRECTION eq 'rtl' -->
-	<link href="{T_THEME_PATH}/bidi.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" />
+	<link href="{T_THEME_PATH}/bidi.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet">
 <!-- ENDIF -->
 
 <!--[if lte IE 8]>
-	<link href="{T_THEME_PATH}/tweaks.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet" type="text/css" media="screen, projection" />
+	<link href="{T_THEME_PATH}/tweaks.css?assets_version={T_ASSETS_VERSION}" rel="stylesheet">
 <![endif]-->
 
 <!-- DEFINE $POPUP = 1 -->
--- a/styles/prosilver/template/ucp_login_link.html
+++ b/styles/prosilver/template/ucp_login_link.html
@@ -36,7 +36,7 @@
 				</dl>
 				<dl>
 					<dt><label for="{PASSWORD_CREDENTIAL}">{L_PASSWORD}{L_COLON}</label></dt>
-					<dd><input type="password" tabindex="3" id="{PASSWORD_CREDENTIAL}" name="{PASSWORD_CREDENTIAL}" size="25" class="inputbox autowidth" /></dd>
+					<dd><input type="password" tabindex="3" id="{PASSWORD_CREDENTIAL}" name="{PASSWORD_CREDENTIAL}" size="25" class="inputbox autowidth" autocomplete="off" /></dd>
 				</dl>
 				<!-- IF CAPTCHA_TEMPLATE and S_CONFIRM_CODE -->
 					<!-- DEFINE $CAPTCHA_TAB_INDEX = 4 -->
--- a/styles/prosilver/template/ucp_pm_viewmessage_print.html
+++ b/styles/prosilver/template/ucp_pm_viewmessage_print.html
@@ -6,7 +6,7 @@
 {META}
 <title>{SITENAME} &bull; {PAGE_TITLE}</title>
 
-<link href="{T_THEME_PATH}/print.css" rel="stylesheet" type="text/css" />
+<link href="{T_THEME_PATH}/print.css" rel="stylesheet">
 <!-- EVENT ucp_pm_viewmessage_print_head_append -->
 </head>
 <body id="phpbb">
--- a/styles/prosilver/template/ucp_profile_reg_details.html
+++ b/styles/prosilver/template/ucp_profile_reg_details.html
@@ -19,16 +19,16 @@
 	</dl>
 	<dl>
 		<dt><label for="email">{L_EMAIL_ADDRESS}{L_COLON}</label></dt>
-		<dd><!-- IF S_CHANGE_EMAIL --><input type="email" name="email" id="email" maxlength="100" value="{EMAIL}" class="inputbox" title="{L_EMAIL_ADDRESS}" /><!-- ELSE --><strong>{EMAIL}</strong><!-- ENDIF --></dd>
+		<dd><!-- IF S_CHANGE_EMAIL --><input type="email" name="email" id="email" maxlength="100" value="{EMAIL}" class="inputbox" title="{L_EMAIL_ADDRESS}" autocomplete="off" /><!-- ELSE --><strong>{EMAIL}</strong><!-- ENDIF --></dd>
 	</dl>
 	<!-- IF S_CHANGE_PASSWORD -->
 		<dl>
 			<dt><label for="new_password">{L_NEW_PASSWORD}{L_COLON}</label><br /><span>{L_CHANGE_PASSWORD_EXPLAIN}</span></dt>
-			<dd><input type="password" name="new_password" id="new_password" maxlength="255" value="{NEW_PASSWORD}" class="inputbox" title="{L_CHANGE_PASSWORD}" /></dd>
+			<dd><input type="password" name="new_password" id="new_password" maxlength="255" value="{NEW_PASSWORD}" class="inputbox" title="{L_CHANGE_PASSWORD}" autocomplete="off" /></dd>
 		</dl>
 		<dl>
 			<dt><label for="password_confirm">{L_CONFIRM_PASSWORD}{L_COLON}</label><br /><span>{L_CONFIRM_PASSWORD_EXPLAIN}</span></dt>
-			<dd><input type="password" name="password_confirm" id="password_confirm" maxlength="255" value="{PASSWORD_CONFIRM}" class="inputbox" title="{L_CONFIRM_PASSWORD}" /></dd>
+			<dd><input type="password" name="password_confirm" id="password_confirm" maxlength="255" value="{PASSWORD_CONFIRM}" class="inputbox" title="{L_CONFIRM_PASSWORD}" autocomplete="off" /></dd>
 		</dl>
 	<!-- ENDIF -->
 	<!-- EVENT ucp_profile_register_details_after -->	
@@ -42,7 +42,7 @@
 	<fieldset>
 	<dl>
 		<dt><label for="cur_password">{L_CURRENT_PASSWORD}{L_COLON}</label><br /><span><!-- IF S_CHANGE_PASSWORD -->{L_CURRENT_CHANGE_PASSWORD_EXPLAIN}<!-- ELSE -->{L_CURRENT_PASSWORD_EXPLAIN}<!-- ENDIF --></span></dt>
-		<dd><input type="password" name="cur_password" id="cur_password" maxlength="255" value="{CUR_PASSWORD}" class="inputbox" title="{L_CURRENT_PASSWORD}" /></dd>
+		<dd><input type="password" name="cur_password" id="cur_password" maxlength="255" value="{CUR_PASSWORD}" class="inputbox" title="{L_CURRENT_PASSWORD}" autocomplete="off" /></dd>
 	</dl>
 	</fieldset>
 
--- a/styles/prosilver/template/ucp_register.html
+++ b/styles/prosilver/template/ucp_register.html
@@ -33,15 +33,15 @@
 	</dl>
 	<dl>
 		<dt><label for="email">{L_EMAIL_ADDRESS}{L_COLON}</label></dt>
-		<dd><input type="email" tabindex="2" name="email" id="email" size="25" maxlength="100" value="{EMAIL}" class="inputbox autowidth" title="{L_EMAIL_ADDRESS}" /></dd>
+		<dd><input type="email" tabindex="2" name="email" id="email" size="25" maxlength="100" value="{EMAIL}" class="inputbox autowidth" title="{L_EMAIL_ADDRESS}" autocomplete="off" /></dd>
 	</dl>
 	<dl>
 		<dt><label for="new_password">{L_PASSWORD}{L_COLON}</label><br /><span>{L_PASSWORD_EXPLAIN}</span></dt>
-		<dd><input type="password" tabindex="4" name="new_password" id="new_password" size="25" value="{PASSWORD}" class="inputbox autowidth" title="{L_NEW_PASSWORD}" /></dd>
+		<dd><input type="password" tabindex="4" name="new_password" id="new_password" size="25" value="{PASSWORD}" class="inputbox autowidth" title="{L_NEW_PASSWORD}" autocomplete="off" /></dd>
 	</dl>
 	<dl>
 		<dt><label for="password_confirm">{L_CONFIRM_PASSWORD}{L_COLON}</label></dt>
-		<dd><input type="password"  tabindex="5" name="password_confirm" id="password_confirm" size="25" value="{PASSWORD_CONFIRM}" class="inputbox autowidth" title="{L_CONFIRM_PASSWORD}" /></dd>
+		<dd><input type="password"  tabindex="5" name="password_confirm" id="password_confirm" size="25" value="{PASSWORD_CONFIRM}" class="inputbox autowidth" title="{L_CONFIRM_PASSWORD}" autocomplete="off" /></dd>
 	</dl>
 
 	<!-- EVENT ucp_register_credentials_after -->
--- a/styles/prosilver/template/viewforum_body.html
+++ b/styles/prosilver/template/viewforum_body.html
@@ -1,7 +1,7 @@
 <!-- INCLUDE overall_header.html -->
-
+<!-- EVENT viewforum_forum_title_before -->
 <h2 class="forum-title"><!-- EVENT viewforum_forum_name_prepend --><a href="{U_VIEW_FORUM}">{FORUM_NAME}</a><!-- EVENT viewforum_forum_name_append --></h2>
-
+<!-- EVENT viewforum_forum_title_after -->
 <!-- IF FORUM_DESC or MODERATORS or U_MCP -->
 <div>
 	<!-- NOTE: remove the style="display: none" when you want to have the forum description on the forum body -->
@@ -100,7 +100,7 @@
 				</dl>
 				<dl>
 					<dt><label for="password">{L_PASSWORD}{L_COLON}</label></dt>
-					<dd><input type="password" tabindex="2" id="password" name="password" size="25" class="inputbox autowidth" /></dd>
+					<dd><input type="password" tabindex="2" id="password" name="password" size="25" class="inputbox autowidth" autocomplete="off" /></dd>
 					<!-- IF S_AUTOLOGIN_ENABLED --><dd><label for="autologin"><input type="checkbox" name="autologin" id="autologin" tabindex="3" /> {L_LOG_ME_IN}</label></dd><!-- ENDIF -->
 					<dd><label for="viewonline"><input type="checkbox" name="viewonline" id="viewonline" tabindex="4" /> {L_HIDE_ME}</label></dd>
 				</dl>
--- a/styles/prosilver/template/viewtopic_body.html
+++ b/styles/prosilver/template/viewtopic_body.html
@@ -210,6 +210,7 @@
 
 			<h3 <!-- IF postrow.S_FIRST_ROW -->class="first"<!-- ENDIF -->><!-- IF postrow.POST_ICON_IMG --><img src="{T_ICONS_PATH}{postrow.POST_ICON_IMG}" width="{postrow.POST_ICON_IMG_WIDTH}" height="{postrow.POST_ICON_IMG_HEIGHT}" alt="" /> <!-- ENDIF --><a href="#p{postrow.POST_ID}">{postrow.POST_SUBJECT}</a></h3>
 
+		<!-- EVENT viewtopic_body_post_buttons_list_before -->
 		<!-- IF not S_IS_BOT -->
 			<!-- IF postrow.U_EDIT or postrow.U_DELETE or postrow.U_REPORT or postrow.U_WARN or postrow.U_INFO or postrow.U_QUOTE -->
 				<ul class="post-buttons">
@@ -248,6 +249,7 @@
 				</ul>
 			<!-- ENDIF -->
 		<!-- ENDIF -->
+		<!-- EVENT viewtopic_body_post_buttons_list_after -->
 
 			<!-- EVENT viewtopic_body_postrow_post_details_before -->
 			<p class="author"><!-- IF S_IS_BOT -->{postrow.MINI_POST_IMG}<!-- ELSE --><a href="{postrow.U_MINI_POST}">{postrow.MINI_POST_IMG}</a><!-- ENDIF --><span class="responsive-hide">{L_POST_BY_AUTHOR} <strong>{postrow.POST_AUTHOR_FULL}</strong> &raquo; </span>{postrow.POST_DATE} </p>
--- a/styles/prosilver/template/viewtopic_print.html
+++ b/styles/prosilver/template/viewtopic_print.html
@@ -6,7 +6,7 @@
 {META}
 <title>{SITENAME} &bull; {PAGE_TITLE}</title>
 
-<link href="{T_THEME_PATH}/print.css" rel="stylesheet" type="text/css" />
+<link href="{T_THEME_PATH}/print.css" rel="stylesheet">
 <!-- EVENT viewtopic_print_head_append -->
 </head>
 <body id="phpbb">
--- a/styles/prosilver/theme/colours.css
+++ b/styles/prosilver/theme/colours.css
@@ -977,6 +977,14 @@ fieldset.quick-login input.inputbox {
 	color: #333333;
 }
 
+#message-box textarea.drag-n-drop {
+	outline-color: rgba(102, 102, 102, 0.5);
+}
+
+#message-box textarea.drag-n-drop-highlight {
+	outline-color: rgba(17, 163, 234, 0.5);
+}
+
 /* Input field styles
 ---------------------------------------- */
 .inputbox {
--- a/styles/prosilver/theme/common.css
+++ b/styles/prosilver/theme/common.css
@@ -59,6 +59,7 @@ body {
 	margin: 0;
 	padding: 12px 0;
 	word-wrap: break-word;
+	-webkit-print-color-adjust: exact;
 }
 
 h1 {
--- a/styles/prosilver/theme/forms.css
+++ b/styles/prosilver/theme/forms.css
@@ -243,6 +243,13 @@ fieldset.submit-buttons input {
 	max-width: 100%;
 	font-size: 1.2em;
 	resize: vertical;
+	outline: 3px dashed transparent;
+	outline-offset: -4px;
+	-webkit-transition: all .5s ease;
+	-moz-transition: all .5s ease;
+	-ms-transition: all .5s ease;
+	-o-transition: all .5s ease;
+	transition: all .5s ease;
 }
 
 /* Emoticons panel */
--- a/vendor/autoload.php
+++ b/vendor/autoload.php
@@ -4,4 +4,4 @@
 
 require_once __DIR__ . '/composer' . '/autoload_real.php';
 
-return ComposerAutoloaderInit12dc4d654e9dc6a3dce6dc4a62ac980f::getLoader();
+return ComposerAutoloaderInit6cde395dbad8cbdae8a9172d56203500::getLoader();
--- a/vendor/composer/autoload_real.php
+++ b/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@
 
 // autoload_real.php @generated by Composer
 
-class ComposerAutoloaderInit12dc4d654e9dc6a3dce6dc4a62ac980f
+class ComposerAutoloaderInit6cde395dbad8cbdae8a9172d56203500
 {
     private static $loader;
 
@@ -19,9 +19,9 @@ class ComposerAutoloaderInit12dc4d654e9dc6a3dce6dc4a62ac980f
             return self::$loader;
         }
 
-        spl_autoload_register(array('ComposerAutoloaderInit12dc4d654e9dc6a3dce6dc4a62ac980f', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInit6cde395dbad8cbdae8a9172d56203500', 'loadClassLoader'), true, true);
         self::$loader = $loader = new \Composer\Autoload\ClassLoader();
-        spl_autoload_unregister(array('ComposerAutoloaderInit12dc4d654e9dc6a3dce6dc4a62ac980f', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInit6cde395dbad8cbdae8a9172d56203500', 'loadClassLoader'));
 
         $map = require __DIR__ . '/autoload_namespaces.php';
         foreach ($map as $namespace => $path) {
@@ -44,7 +44,7 @@ class ComposerAutoloaderInit12dc4d654e9dc6a3dce6dc4a62ac980f
     }
 }
 
-function composerRequire12dc4d654e9dc6a3dce6dc4a62ac980f($file)
+function composerRequire6cde395dbad8cbdae8a9172d56203500($file)
 {
     require $file;
 }
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -671,12 +671,12 @@
         "version_normalized": "1.13.2.0",
         "source": {
             "type": "git",
-            "url": "https://github.com/fabpot/Twig.git",
+            "url": "https://github.com/twigphp/Twig.git",
             "reference": "6d6a1009427d1f398c9d40904147bf9f723d5755"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/fabpot/Twig/zipball/6d6a1009427d1f398c9d40904147bf9f723d5755",
+            "url": "https://api.github.com/repos/twigphp/Twig/zipball/6d6a1009427d1f398c9d40904147bf9f723d5755",
             "reference": "6d6a1009427d1f398c9d40904147bf9f723d5755",
             "shasum": ""
         },
@@ -703,14 +703,11 @@
         "authors": [
             {
                 "name": "Fabien Potencier",
-                "email": "fabien@symfony.com",
-                "homepage": "http://fabien.potencier.org",
-                "role": "Lead Developer"
+                "email": "fabien@symfony.com"
             },
             {
                 "name": "Armin Ronacher",
-                "email": "armin.ronacher@active-4.com",
-                "role": "Project Founder"
+                "email": "armin.ronacher@active-4.com"
             }
         ],
         "description": "Twig, the flexible, fast, and secure template language for PHP",
--- a/viewonline.php
+++ b/viewonline.php
@@ -86,10 +86,26 @@ if ($mode == 'whois' && $auth->acl_get('a_') && $session_id)
 }
 
 // Forum info
-$sql = 'SELECT forum_id, forum_name, parent_id, forum_type, left_id, right_id
-	FROM ' . FORUMS_TABLE . '
-	ORDER BY left_id ASC';
-$result = $db->sql_query($sql, 600);
+$sql_ary = array(
+	'SELECT'	=> 'f.forum_id, f.forum_name, f.parent_id, f.forum_type, f.left_id, f.right_id',
+	'FROM'		=> array(
+		FORUMS_TABLE	=> 'f',
+	),
+	'ORDER_BY'	=> 'f.left_id ASC',
+);
+
+/**
+* Modify the forum data SQL query for getting additional fields if needed
+*
+* @event core.viewonline_modify_forum_data_sql
+* @var	array	sql_ary			The SQL array
+* @since 3.1.5-RC1
+*/
+$vars = array('sql_ary');
+extract($phpbb_dispatcher->trigger_event('core.viewonline_modify_forum_data_sql', compact($vars)));
+
+$result = $db->sql_query($db->sql_build_query('SELECT', $sql_ary), 600);
+unset($sql_ary);
 
 $forum_data = array();
 while ($row = $db->sql_fetchrow($result))
--- a/viewtopic.php
+++ b/viewtopic.php
@@ -804,6 +804,36 @@ if (!empty($topic_data['poll_start']))
 		($auth->acl_get('f_votechg', $forum_id) && $topic_data['poll_vote_change']))) ? true : false;
 	$s_display_results = (!$s_can_vote || ($s_can_vote && sizeof($cur_voted_id)) || $view == 'viewpoll') ? true : false;
 
+	/**
+	* Event to manipulate the poll data
+	*
+	* @event core.viewtopic_modify_poll_data
+	* @var	array	cur_voted_id				Array with options' IDs current user has voted for
+	* @var	int		forum_id					The topic's forum id
+	* @var	array	poll_info					Array with the poll information
+	* @var	bool	s_can_vote					Flag indicating if a user can vote
+	* @var	bool	s_display_results			Flag indicating if results or poll options should be displayed
+	* @var	int		topic_id					The id of the topic the user tries to access
+	* @var	array	topic_data					All the information from the topic and forum tables for this topic
+	* @var	string	viewtopic_url				URL to the topic page
+	* @var	array	vote_counts					Array with the vote counts for every poll option
+	* @var	array	voted_id					Array with updated options' IDs current user is voting for
+	* @since 3.1.5-RC1
+	*/
+	$vars = array(
+		'cur_voted_id',
+		'forum_id',
+		'poll_info',
+		's_can_vote',
+		's_display_results',
+		'topic_id',
+		'topic_data',
+		'viewtopic_url',
+		'vote_counts',
+		'voted_id',
+	);
+	extract($phpbb_dispatcher->trigger_event('core.viewtopic_modify_poll_data', compact($vars)));
+
 	if ($update && $s_can_vote)
 	{
 
@@ -937,6 +967,7 @@ if (!empty($topic_data['poll_start']))
 
 	$topic_data['poll_title'] = generate_text_for_display($topic_data['poll_title'], $poll_info[0]['bbcode_uid'], $poll_info[0]['bbcode_bitfield'], $parse_flags, true);
 
+	$poll_template_data = $poll_options_template_data = array();
 	foreach ($poll_info as $poll_option)
 	{
 		$option_pct = ($poll_total > 0) ? $poll_option['poll_option_total'] / $poll_total : 0;
@@ -945,7 +976,7 @@ if (!empty($topic_data['poll_start']))
 		$option_pct_rel_txt = sprintf("%.1d%%", round($option_pct_rel * 100));
 		$option_most_votes = ($poll_option['poll_option_total'] > 0 && $poll_option['poll_option_total'] == $poll_most) ? true : false;
 
-		$template->assign_block_vars('poll_option', array(
+		$poll_options_template_data[] = array(
 			'POLL_OPTION_ID' 			=> $poll_option['poll_option_id'],
 			'POLL_OPTION_CAPTION' 		=> $poll_option['poll_option_text'],
 			'POLL_OPTION_RESULT' 		=> $poll_option['poll_option_total'],
@@ -955,12 +986,12 @@ if (!empty($topic_data['poll_start']))
 			'POLL_OPTION_WIDTH'     	=> round($option_pct * 250),
 			'POLL_OPTION_VOTED'			=> (in_array($poll_option['poll_option_id'], $cur_voted_id)) ? true : false,
 			'POLL_OPTION_MOST_VOTES'	=> $option_most_votes,
-		));
+		);
 	}
 
 	$poll_end = $topic_data['poll_length'] + $topic_data['poll_start'];
 
-	$template->assign_vars(array(
+	$poll_template_data = array(
 		'POLL_QUESTION'		=> $topic_data['poll_title'],
 		'TOTAL_VOTES' 		=> $poll_total,
 		'POLL_LEFT_CAP_IMG'	=> $user->img('poll_left'),
@@ -976,9 +1007,45 @@ if (!empty($topic_data['poll_start']))
 		'S_POLL_ACTION'		=> $viewtopic_url,
 
 		'U_VIEW_RESULTS'	=> $viewtopic_url . '&amp;view=viewpoll',
-	));
+	);
+
+	/**
+	* Event to add/modify poll template data
+	*
+	* @event core.viewtopic_modify_poll_template_data
+	* @var	array	cur_voted_id					Array with options' IDs current user has voted for
+	* @var	int		poll_end						The poll end time
+	* @var	array	poll_info						Array with the poll information
+	* @var	array	poll_options_template_data		Array with the poll options template data
+	* @var	array	poll_template_data				Array with the common poll template data
+	* @var	int		poll_total						Total poll votes count
+	* @var	int		poll_most						Mostly voted option votes count
+	* @var	array	topic_data						All the information from the topic and forum tables for this topic
+	* @var	string	viewtopic_url					URL to the topic page
+	* @var	array	vote_counts						Array with the vote counts for every poll option
+	* @var	array	voted_id						Array with updated options' IDs current user is voting for
+	* @since 3.1.5-RC1
+	*/
+	$vars = array(
+		'cur_voted_id',
+		'poll_end',
+		'poll_info',
+		'poll_options_template_data',
+		'poll_template_data',
+		'poll_total',
+		'poll_most',
+		'topic_data',
+		'viewtopic_url',
+		'vote_counts',
+		'voted_id',
+	);
+	extract($phpbb_dispatcher->trigger_event('core.viewtopic_modify_poll_template_data', compact($vars)));
+
+	$template->assign_block_vars_array('poll_option', $poll_options_template_data);
+
+	$template->assign_vars($poll_template_data);
 
-	unset($poll_end, $poll_info, $voted_id);
+	unset($poll_end, $poll_info, $poll_options_template_data, $poll_template_data, $voted_id);
 }
 
 // If the user is trying to reach the second half of the topic, fetch it starting from the end
