diff --git a/install/data/footer.json b/install/data/footer.json
new file mode 100644
index 0000000000..69d55448ac
--- /dev/null
+++ b/install/data/footer.json
@@ -0,0 +1,10 @@
+[
+ {
+ "widget": "html",
+ "data" : {
+ "html": "",
+ "title":"",
+ "container":""
+ }
+ }
+]
\ No newline at end of file
diff --git a/install/data/welcome.md b/install/data/welcome.md
new file mode 100644
index 0000000000..399850e6e2
--- /dev/null
+++ b/install/data/welcome.md
@@ -0,0 +1,10 @@
+# Welcome to your brand new NodeBB forum!
+
+This is what a topic and post looks like. As an administator, you can edit the post\'s title and content.
+To customise your forum, go to the [Administrator Control Panel](../../admin). You can modify all aspects of your forum there, including installation of third-party plugins.
+
+## Additional Resources
+
+* [NodeBB Documentation](https://docs.nodebb.org)
+* [Community Support Forum](https://community.nodebb.org)
+* [Project repository](https://github.com/nodebb/nodebb)
\ No newline at end of file
diff --git a/package.json b/package.json
index 8de4dbc58c..a7907757e8 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"templates.js": "^0.1.15",
"uglify-js": "git+https://github.com/julianlam/UglifyJS2.git",
"underscore": "~1.7.0",
- "validator": "~3.28.0",
+ "validator": "^3.30.0",
"winston": "^0.9.0",
"xregexp": "~2.0.0"
},
diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json
index 0258b28154..bf4e7229fd 100644
--- a/public/language/en_GB/error.json
+++ b/public/language/en_GB/error.json
@@ -52,6 +52,8 @@
"invalid-title": "Invalid title!",
"too-many-posts": "You can only post once every %1 seconds - please wait before posting again",
"too-many-posts-newbie": "As a new user, you can only post once every %1 seconds until you have earned %2 reputation - please wait before posting again",
+ "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 characters",
+ "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 characters",
"file-too-big": "Maximum allowed file size is %1 kbs - please upload a smaller file",
"cant-vote-self-post": "You cannot vote for your own post",
diff --git a/public/language/en_GB/tags.json b/public/language/en_GB/tags.json
index f67b2ca7b5..a3f75bb2e6 100644
--- a/public/language/en_GB/tags.json
+++ b/public/language/en_GB/tags.json
@@ -1,7 +1,7 @@
{
"no_tag_topics": "There are no topics with this tag.",
"tags": "Tags",
- "enter_tags_here": "Enter tags here. Press enter after each tag.",
+ "enter_tags_here": "Enter tags here. %1-%2 characters. Press enter after each tag.",
"enter_tags_here_short": "Enter tags...",
"no_tags": "There are no tags yet."
}
\ No newline at end of file
diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less
index 8a508db2e0..ee9a87b605 100644
--- a/public/less/admin/admin.less
+++ b/public/less/admin/admin.less
@@ -169,7 +169,6 @@
border-radius: 3px;
box-shadow: 0px 1px 3px 0px rgba(165, 165, 165, 0.75);
margin-bottom: 20px;
- max-width: 1000px;
&.panel-default .panel-heading {
background: #fefefe;
@@ -232,7 +231,7 @@
}
}
-
+
#acp-search {
input {
background: black;
@@ -250,7 +249,7 @@
width: 200px;
}
}
-
+
.search-match {
font-weight: 700;
color: black;
diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js
index 7b8f8cb5b4..b2ae0c0afb 100644
--- a/public/src/admin/manage/groups.js
+++ b/public/src/admin/manage/groups.js
@@ -154,8 +154,8 @@ define('admin/manage/groups', [
socket.emit('admin.user.search', {query: searchText}, function(err, results) {
if (!err && results && results.users.length > 0) {
var numResults = results.users.length, x;
- if (numResults > 4) {
- numResults = 4;
+ if (numResults > 20) {
+ numResults = 20;
}
groupDetailsSearchResults.empty();
diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js
index dc8bfaeaf1..86e5d1f66d 100644
--- a/public/src/client/account/edit.js
+++ b/public/src/client/account/edit.js
@@ -266,12 +266,15 @@ define('forum/account/edit', ['forum/account/header', 'uploader'], function(head
password_confirm.on('blur', onPasswordConfirmChanged);
$('#changePasswordBtn').on('click', function() {
+ var btn = $(this);
if ((passwordvalid && passwordsmatch) || app.user.isAdmin) {
+ btn.addClass('disabled').find('i').removeClass('hide');
socket.emit('user.changePassword', {
'currentPassword': currentPassword.val(),
'newPassword': password.val(),
'uid': ajaxify.variables.get('theirid')
}, function(err) {
+ btn.removeClass('disabled').find('i').addClass('hide');
currentPassword.val('');
password.val('');
password_confirm.val('');
diff --git a/public/src/modules/composer.js b/public/src/modules/composer.js
index 088cafe260..29154d1523 100644
--- a/public/src/modules/composer.js
+++ b/public/src/modules/composer.js
@@ -228,6 +228,8 @@ define('composer', [
var data = {
allowTopicsThumbnail: allowTopicsThumbnail,
showTags: isTopic || isMain,
+ minimumTagLength: config.minimumTagLength,
+ maximumTagLength: config.maximumTagLength,
isTopic: isTopic,
showHandleInput: (app.user.uid === 0 || (isEditing && isGuestPost && app.user.isAdmin)) && config.allowGuestHandles,
handle: composer.posts[post_uuid] ? composer.posts[post_uuid].handle || '' : undefined
diff --git a/public/src/modules/composer/tags.js b/public/src/modules/composer/tags.js
index 93b33fbb38..f074a5abc2 100644
--- a/public/src/modules/composer/tags.js
+++ b/public/src/modules/composer/tags.js
@@ -14,7 +14,17 @@ define('composer/tags', function() {
tagEl.tagsinput({
maxTags: config.tagsPerTopic,
- confirmKeys: [13, 44]
+ confirmKeys: [13, 44],
+ trimValue: true
+ });
+
+ tagEl.on('beforeItemAdd', function(event) {
+ event.cancel = event.item.length < config.minimumTagLength || event.item.length > config.maximumTagLength;
+ if (event.item.length < config.minimumTagLength) {
+ app.alertError('[[error:tag-too-short, ' + config.minimumTagLength + ']]');
+ } else if (event.item.length > config.maximumTagLength) {
+ app.alertError('[[error:tag-too-long, ' + config.maximumTagLength + ']]');
+ }
});
tagEl.on('itemAdded', function(event) {
diff --git a/public/src/widgets.js b/public/src/widgets.js
index 2200f58d5e..936ce9cd95 100644
--- a/public/src/widgets.js
+++ b/public/src/widgets.js
@@ -53,8 +53,7 @@
if (location === 'footer' && !$('#content [widget-area="footer"]').length) {
$('#content').append($('
'));
} else if (location === 'sidebar' && !$('#content [widget-area="sidebar"]').length) {
- $('#content > *').wrapAll($(''));
- $('#content').append($(''));
+ $('#content > *').wrapAll($(''));
} else if (location === 'header' && !$('#content [widget-area="header"]').length) {
$('#content').prepend($(''));
}
@@ -69,11 +68,11 @@
ajaxify.widgets.reposition(location);
}
- $('#content [widget-area] img:not(.user-img)').addClass('img-responsive');
+ $('#content [widget-area] img:not(.user-img)').addClass('img-responsive');
}
-
+
$(window).trigger('action:widgets.loaded', {});
-
+
if (typeof callback === 'function') {
callback();
}
diff --git a/public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css b/public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css
index 98cfa7f3c1..55f7c09df0 100644
--- a/public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css
+++ b/public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css
@@ -10,6 +10,7 @@
border-radius: 4px;
max-width: 100%;
line-height: 22px;
+ cursor: text;
}
.bootstrap-tagsinput input {
border: none;
diff --git a/public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js b/public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js
index aff7ea6cb8..d9047ac1ec 100644
--- a/public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js
+++ b/public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js
@@ -1,506 +1,6 @@
-(function ($) {
- "use strict";
+/*
+ * bootstrap-tagsinput v0.4.2 by Tim Schlechter
+ *
+ */
- var defaultOptions = {
- tagClass: function(item) {
- return 'label label-info';
- },
- itemValue: function(item) {
- return item ? item.toString() : item;
- },
- itemText: function(item) {
- return this.itemValue(item);
- },
- freeInput: true,
- maxTags: undefined,
- confirmKeys: [13],
- onTagExists: function(item, $tag) {
- $tag.hide().fadeIn();
- }
- };
-
- /**
- * Constructor function
- */
- function TagsInput(element, options) {
- this.itemsArray = [];
-
- this.$element = $(element);
- this.$element.hide();
-
- this.isSelect = (element.tagName === 'SELECT');
- this.multiple = (this.isSelect && element.hasAttribute('multiple'));
- this.objectItems = options && options.itemValue;
- this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
- this.inputSize = Math.max(1, this.placeholderText.length);
-
- this.$container = $('');
- this.$input = $('').appendTo(this.$container);
-
- this.$element.after(this.$container);
-
- this.build(options);
- }
-
- TagsInput.prototype = {
- constructor: TagsInput,
-
- /**
- * Adds the given item as a new tag. Pass true to dontPushVal to prevent
- * updating the elements val()
- */
- add: function(item, dontPushVal) {
- var self = this;
-
- if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
- return;
-
- // Ignore falsey values, except false
- if (item !== false && !item)
- return;
-
- // Throw an error when trying to add an object while the itemValue option was not set
- if (typeof item === "object" && !self.objectItems)
- throw("Can't add objects when itemValue option is not set");
-
- // Ignore strings only containg whitespace
- if (item.toString().match(/^\s*$/))
- return;
-
- // If SELECT but not multiple, remove current tag
- if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
- self.remove(self.itemsArray[0]);
-
- if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
- var items = item.split(',');
- if (items.length > 1) {
- for (var i = 0; i < items.length; i++) {
- this.add(items[i], true);
- }
-
- if (!dontPushVal)
- self.pushVal();
- return;
- }
- }
-
- var itemValue = self.options.itemValue(item),
- itemText = self.options.itemText(item),
- tagClass = self.options.tagClass(item);
-
- // Ignore items allready added
- var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
- if (existing) {
- // Invoke onTagExists
- if (self.options.onTagExists) {
- var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
- self.options.onTagExists(item, $existingTag);
- }
- return;
- }
-
- // register item in internal array and map
- self.itemsArray.push(item);
-
- // add a tag element
- var $tag = $('' + htmlEncode(itemText) + '');
- $tag.data('item', item);
- self.findInputWrapper().before($tag);
- $tag.after(' ');
-
- // add if item represents a value not present in one of the 's options
- if (self.isSelect && !$('option[value="' + escape(itemValue) + '"]',self.$element)[0]) {
- var $option = $('');
- $option.data('item', item);
- $option.attr('value', itemValue);
- self.$element.append($option);
- }
-
- if (!dontPushVal)
- self.pushVal();
-
- // Add class when reached maxTags
- if (self.options.maxTags === self.itemsArray.length)
- self.$container.addClass('bootstrap-tagsinput-max');
-
- self.$element.trigger($.Event('itemAdded', { item: item }));
- },
-
- /**
- * Removes the given item. Pass true to dontPushVal to prevent updating the
- * elements val()
- */
- remove: function(item, dontPushVal) {
- var self = this;
-
- if (self.objectItems) {
- if (typeof item === "object")
- item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } )[0];
- else
- item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } )[0];
- }
-
- if (item) {
- $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
- $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
- self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
- }
-
- if (!dontPushVal)
- self.pushVal();
-
- // Remove class when reached maxTags
- if (self.options.maxTags > self.itemsArray.length)
- self.$container.removeClass('bootstrap-tagsinput-max');
-
- self.$element.trigger($.Event('itemRemoved', { item: item }));
- },
-
- /**
- * Removes all items
- */
- removeAll: function() {
- var self = this;
-
- $('.tag', self.$container).remove();
- $('option', self.$element).remove();
-
- while(self.itemsArray.length > 0)
- self.itemsArray.pop();
-
- self.pushVal();
-
- if (self.options.maxTags && !this.isEnabled())
- this.enable();
- },
-
- /**
- * Refreshes the tags so they match the text/value of their corresponding
- * item.
- */
- refresh: function() {
- var self = this;
- $('.tag', self.$container).each(function() {
- var $tag = $(this),
- item = $tag.data('item'),
- itemValue = self.options.itemValue(item),
- itemText = self.options.itemText(item),
- tagClass = self.options.tagClass(item);
-
- // Update tag's class and inner text
- $tag.attr('class', null);
- $tag.addClass('tag ' + htmlEncode(tagClass));
- $tag.contents().filter(function() {
- return this.nodeType == 3;
- })[0].nodeValue = htmlEncode(itemText);
-
- if (self.isSelect) {
- var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
- option.attr('value', itemValue);
- }
- });
- },
-
- /**
- * Returns the items added as tags
- */
- items: function() {
- return this.itemsArray;
- },
-
- /**
- * Assembly value by retrieving the value of each item, and set it on the
- * element.
- */
- pushVal: function() {
- var self = this,
- val = $.map(self.items(), function(item) {
- return self.options.itemValue(item).toString();
- });
-
- self.$element.val(val, true).trigger('change');
- },
-
- /**
- * Initializes the tags input behaviour on the element
- */
- build: function(options) {
- var self = this;
-
- self.options = $.extend({}, defaultOptions, options);
- var typeahead = self.options.typeahead || {};
-
- // When itemValue is set, freeInput should always be false
- if (self.objectItems)
- self.options.freeInput = false;
-
- makeOptionItemFunction(self.options, 'itemValue');
- makeOptionItemFunction(self.options, 'itemText');
- makeOptionItemFunction(self.options, 'tagClass');
-
- // for backwards compatibility, self.options.source is deprecated
- if (self.options.source)
- typeahead.source = self.options.source;
-
- if (typeahead.source && $.fn.typeahead) {
- makeOptionFunction(typeahead, 'source');
-
- self.$input.typeahead({
- source: function (query, process) {
- function processItems(items) {
- var texts = [];
-
- for (var i = 0; i < items.length; i++) {
- var text = self.options.itemText(items[i]);
- map[text] = items[i];
- texts.push(text);
- }
- process(texts);
- }
-
- this.map = {};
- var map = this.map,
- data = typeahead.source(query);
-
- if ($.isFunction(data.success)) {
- // support for Angular promises
- data.success(processItems);
- } else {
- // support for functions and jquery promises
- $.when(data)
- .then(processItems);
- }
- },
- updater: function (text) {
- self.add(this.map[text]);
- },
- matcher: function (text) {
- return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
- },
- sorter: function (texts) {
- return texts.sort();
- },
- highlighter: function (text) {
- var regex = new RegExp( '(' + this.query + ')', 'gi' );
- return text.replace( regex, "$1" );
- }
- });
- }
-
- self.$container.on('click', $.proxy(function(event) {
- self.$input.focus();
- }, self));
-
- self.$container.on('keypress', 'input', $.proxy(function(event) {
- var $input = $(event.target);
- if (self.options.freeInput && $.inArray(event.which, self.options.confirmKeys) >= 0) {
- self.add($input.val());
- $input.val('');
- event.preventDefault();
- }
- }));
-
- self.$container.on('keydown', 'input', $.proxy(function(event) {
- var $input = $(event.target),
- $inputWrapper = self.findInputWrapper();
-
- switch (event.which) {
- // BACKSPACE
- case 8:
- if (doGetCaretPosition($input[0]) === 0) {
- var prev = $inputWrapper.prev();
- if (prev) {
- self.remove(prev.data('item'));
- }
- }
- break;
-
- // DELETE
- case 46:
- if (doGetCaretPosition($input[0]) === 0) {
- var next = $inputWrapper.next();
- if (next) {
- self.remove(next.data('item'));
- }
- }
- break;
-
- // LEFT ARROW
- case 37:
- // Try to move the input before the previous tag
- var $prevTag = $inputWrapper.prev();
- if ($input.val().length === 0 && $prevTag[0]) {
- $prevTag.before($inputWrapper);
- $input.focus();
- }
- break;
- // RIGHT ARROW
- case 39:
- // Try to move the input after the next tag
- var $nextTag = $inputWrapper.next();
- if ($input.val().length === 0 && $nextTag[0]) {
- $nextTag.after($inputWrapper);
- $input.focus();
- }
- break;
- default:
- break
- }
-
- // Reset internal input's size
- $input.attr('size', Math.max(this.inputSize, $input.val().length));
- }, self));
-
- // Remove icon clicked
- self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
- self.remove($(event.target).closest('.tag').data('item'));
- }, self));
-
- // Only add existing value as tags when using strings as tags
- if (self.options.itemValue === defaultOptions.itemValue) {
- if (self.$element[0].tagName === 'INPUT') {
- self.add(self.$element.val());
- } else {
- $('option', self.$element).each(function() {
- self.add($(this).attr('value'), true);
- });
- }
- }
- },
-
- /**
- * Removes all tagsinput behaviour and unregsiter all event handlers
- */
- destroy: function() {
- var self = this;
-
- // Unbind events
- self.$container.off('keypress', 'input');
- self.$container.off('click', '[role=remove]');
-
- self.$container.remove();
- self.$element.removeData('tagsinput');
- self.$element.show();
- },
-
- /**
- * Sets focus on the tagsinput
- */
- focus: function() {
- this.$input.focus();
- },
-
- /**
- * Returns the internal input element
- */
- input: function() {
- return this.$input;
- },
-
- /**
- * Returns the element which is wrapped around the internal input. This
- * is normally the $container, but typeahead.js moves the $input element.
- */
- findInputWrapper: function() {
- var elt = this.$input[0],
- container = this.$container[0];
- while(elt && elt.parentNode !== container)
- elt = elt.parentNode;
-
- return $(elt);
- }
- };
-
- /**
- * Register JQuery plugin
- */
- $.fn.tagsinput = function(arg1, arg2) {
- var results = [];
-
- this.each(function() {
- var tagsinput = $(this).data('tagsinput');
-
- // Initialize a new tags input
- if (!tagsinput) {
- tagsinput = new TagsInput(this, arg1);
- $(this).data('tagsinput', tagsinput);
- results.push(tagsinput);
-
- if (this.tagName === 'SELECT') {
- $('option', $(this)).attr('selected', 'selected');
- }
-
- // Init tags from $(this).val()
- $(this).val($(this).val());
- } else {
- // Invoke function on existing tags input
- var retVal = tagsinput[arg1](arg2);
- if (retVal !== undefined)
- results.push(retVal);
- }
- });
-
- if ( typeof arg1 == 'string') {
- // Return the results from the invoked function calls
- return results.length > 1 ? results : results[0];
- } else {
- return results;
- }
- };
-
- $.fn.tagsinput.Constructor = TagsInput;
-
- /**
- * Most options support both a string or number as well as a function as
- * option value. This function makes sure that the option with the given
- * key in the given options is wrapped in a function
- */
- function makeOptionItemFunction(options, key) {
- if (typeof options[key] !== 'function') {
- var propertyName = options[key];
- options[key] = function(item) { return item[propertyName]; };
- }
- }
- function makeOptionFunction(options, key) {
- if (typeof options[key] !== 'function') {
- var value = options[key];
- options[key] = function() { return value; };
- }
- }
- /**
- * HtmlEncodes the given value
- */
- var htmlEncodeContainer = $('');
- function htmlEncode(value) {
- if (value) {
- return htmlEncodeContainer.text(value).html();
- } else {
- return '';
- }
- }
-
- /**
- * Returns the position of the caret in the given input field
- * http://flightschool.acylt.com/devnotes/caret-position-woes/
- */
- function doGetCaretPosition(oField) {
- var iCaretPos = 0;
- if (document.selection) {
- oField.focus ();
- var oSel = document.selection.createRange();
- oSel.moveStart ('character', -oField.value.length);
- iCaretPos = oSel.text.length;
- } else if (oField.selectionStart || oField.selectionStart == '0') {
- iCaretPos = oField.selectionStart;
- }
- return (iCaretPos);
- }
-
- /**
- * Initialize tagsinput behaviour on inputs and selects which have
- * data-role=tagsinput
- */
- $(function() {
- $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
- });
-})(window.jQuery);
+!function(a){"use strict";function b(b,c){this.itemsArray=[],this.$element=a(b),this.$element.hide(),this.isSelect="SELECT"===b.tagName,this.multiple=this.isSelect&&b.hasAttribute("multiple"),this.objectItems=c&&c.itemValue,this.placeholderText=b.hasAttribute("placeholder")?this.$element.attr("placeholder"):"",this.inputSize=Math.max(1,this.placeholderText.length),this.$container=a(''),this.$input=a('').appendTo(this.$container),this.$element.after(this.$container);var d=(this.inputSize<3?3:this.inputSize)+"em";this.$input.get(0).style.cssText="width: "+d+" !important;",this.build(c)}function c(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(a){return a[c]}}}function d(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(){return c}}}function e(a){return a?i.text(a).html():""}function f(a){var b=0;if(document.selection){a.focus();var c=document.selection.createRange();c.moveStart("character",-a.value.length),b=c.text.length}else(a.selectionStart||"0"==a.selectionStart)&&(b=a.selectionStart);return b}function g(b,c){var d=!1;return a.each(c,function(a,c){if("number"==typeof c&&b.which===c)return d=!0,!1;if(b.which===c.which){var e=!c.hasOwnProperty("altKey")||b.altKey===c.altKey,f=!c.hasOwnProperty("shiftKey")||b.shiftKey===c.shiftKey,g=!c.hasOwnProperty("ctrlKey")||b.ctrlKey===c.ctrlKey;if(e&&f&&g)return d=!0,!1}}),d}var h={tagClass:function(){return"label label-info"},itemValue:function(a){return a?a.toString():a},itemText:function(a){return this.itemValue(a)},freeInput:!0,addOnBlur:!0,maxTags:void 0,maxChars:void 0,confirmKeys:[13,44],onTagExists:function(a,b){b.hide().fadeIn()},trimValue:!1,allowDuplicates:!1};b.prototype={constructor:b,add:function(b,c){var d=this;if(!(d.options.maxTags&&d.itemsArray.length>=d.options.maxTags||b!==!1&&!b)){if("string"==typeof b&&d.options.trimValue&&(b=a.trim(b)),"object"==typeof b&&!d.objectItems)throw"Can't add objects when itemValue option is not set";if(!b.toString().match(/^\s*$/)){if(d.isSelect&&!d.multiple&&d.itemsArray.length>0&&d.remove(d.itemsArray[0]),"string"==typeof b&&"INPUT"===this.$element[0].tagName){var f=b.split(",");if(f.length>1){for(var g=0;gd.options.maxInputLength)){var l=a.Event("beforeItemAdd",{item:b,cancel:!1});if(d.$element.trigger(l),!l.cancel){d.itemsArray.push(b);var m=a(''+e(i)+'');if(m.data("item",b),d.findInputWrapper().before(m),m.after(" "),d.isSelect&&!a('option[value="'+encodeURIComponent(h)+'"]',d.$element)[0]){var n=a("");n.data("item",b),n.attr("value",h),d.$element.append(n)}c||d.pushVal(),(d.options.maxTags===d.itemsArray.length||d.items().toString().length===d.options.maxInputLength)&&d.$container.addClass("bootstrap-tagsinput-max"),d.$element.trigger(a.Event("itemAdded",{item:b}))}}}else if(d.options.onTagExists){var o=a(".tag",d.$container).filter(function(){return a(this).data("item")===k});d.options.onTagExists(b,o)}}}},remove:function(b,c){var d=this;if(d.objectItems&&(b="object"==typeof b?a.grep(d.itemsArray,function(a){return d.options.itemValue(a)==d.options.itemValue(b)}):a.grep(d.itemsArray,function(a){return d.options.itemValue(a)==b}),b=b[b.length-1]),b){var e=a.Event("beforeItemRemove",{item:b,cancel:!1});if(d.$element.trigger(e),e.cancel)return;a(".tag",d.$container).filter(function(){return a(this).data("item")===b}).remove(),a("option",d.$element).filter(function(){return a(this).data("item")===b}).remove(),-1!==a.inArray(b,d.itemsArray)&&d.itemsArray.splice(a.inArray(b,d.itemsArray),1)}c||d.pushVal(),d.options.maxTags>d.itemsArray.length&&d.$container.removeClass("bootstrap-tagsinput-max"),d.$element.trigger(a.Event("itemRemoved",{item:b}))},removeAll:function(){var b=this;for(a(".tag",b.$container).remove(),a("option",b.$element).remove();b.itemsArray.length>0;)b.itemsArray.pop();b.pushVal()},refresh:function(){var b=this;a(".tag",b.$container).each(function(){var c=a(this),d=c.data("item"),f=b.options.itemValue(d),g=b.options.itemText(d),h=b.options.tagClass(d);if(c.attr("class",null),c.addClass("tag "+e(h)),c.contents().filter(function(){return 3==this.nodeType})[0].nodeValue=e(g),b.isSelect){var i=a("option",b.$element).filter(function(){return a(this).data("item")===d});i.attr("value",f)}})},items:function(){return this.itemsArray},pushVal:function(){var b=this,c=a.map(b.items(),function(a){return b.options.itemValue(a).toString()});b.$element.val(c,!0).trigger("change")},build:function(b){var e=this;if(e.options=a.extend({},h,b),e.objectItems&&(e.options.freeInput=!1),c(e.options,"itemValue"),c(e.options,"itemText"),d(e.options,"tagClass"),e.options.typeahead){var i=e.options.typeahead||{};d(i,"source"),e.$input.typeahead(a.extend({},i,{source:function(b,c){function d(a){for(var b=[],d=0;d$1")}}))}if(e.options.typeaheadjs){var j=e.options.typeaheadjs||{};e.$input.typeahead(null,j).on("typeahead:selected",a.proxy(function(a,b){e.add(j.valueKey?b[j.valueKey]:b),e.$input.typeahead("val","")},e))}e.$container.on("click",a.proxy(function(){e.$element.attr("disabled")||e.$input.removeAttr("disabled"),e.$input.focus()},e)),e.options.addOnBlur&&e.options.freeInput&&e.$input.on("focusout",a.proxy(function(){0===a(".typeahead, .twitter-typeahead",e.$container).length&&(e.add(e.$input.val()),e.$input.val(""))},e)),e.$container.on("keydown","input",a.proxy(function(b){var c=a(b.target),d=e.findInputWrapper();if(e.$element.attr("disabled"))return void e.$input.attr("disabled","disabled");switch(b.which){case 8:if(0===f(c[0])){var g=d.prev();g&&e.remove(g.data("item"))}break;case 46:if(0===f(c[0])){var h=d.next();h&&e.remove(h.data("item"))}break;case 37:var i=d.prev();0===c.val().length&&i[0]&&(i.before(d),c.focus());break;case 39:var j=d.next();0===c.val().length&&j[0]&&(j.after(d),c.focus())}{var k=c.val().length;Math.ceil(k/5)}c.attr("size",Math.max(this.inputSize,c.val().length))},e)),e.$container.on("keypress","input",a.proxy(function(b){var c=a(b.target);if(e.$element.attr("disabled"))return void e.$input.attr("disabled","disabled");var d=c.val(),f=e.options.maxChars&&d.length>=e.options.maxChars;e.options.freeInput&&(g(b,e.options.confirmKeys)||f)&&(e.add(f?d.substr(0,e.options.maxChars):d),c.val(""),b.preventDefault());{var h=c.val().length;Math.ceil(h/5)}c.attr("size",Math.max(this.inputSize,c.val().length))},e)),e.$container.on("click","[data-role=remove]",a.proxy(function(b){e.$element.attr("disabled")||e.remove(a(b.target).closest(".tag").data("item"))},e)),e.options.itemValue===h.itemValue&&("INPUT"===e.$element[0].tagName?e.add(e.$element.val()):a("option",e.$element).each(function(){e.add(a(this).attr("value"),!0)}))},destroy:function(){var a=this;a.$container.off("keypress","input"),a.$container.off("click","[role=remove]"),a.$container.remove(),a.$element.removeData("tagsinput"),a.$element.show()},focus:function(){this.$input.focus()},input:function(){return this.$input},findInputWrapper:function(){for(var b=this.$input[0],c=this.$container[0];b&&b.parentNode!==c;)b=b.parentNode;return a(b)}},a.fn.tagsinput=function(c,d){var e=[];return this.each(function(){var f=a(this).data("tagsinput");if(f)if(c||d){if(void 0!==f[c]){var g=f[c](d);void 0!==g&&e.push(g)}}else e.push(f);else f=new b(this,c),a(this).data("tagsinput",f),e.push(f),"SELECT"===this.tagName&&a("option",a(this)).attr("selected","selected"),a(this).val(a(this).val())}),"string"==typeof c?e.length>1?e:e[0]:e},a.fn.tagsinput.Constructor=b;var i=a("");a(function(){a("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput()})}(window.jQuery);
diff --git a/src/categories.js b/src/categories.js
index 22c7807c96..a4b6ce2092 100644
--- a/src/categories.js
+++ b/src/categories.js
@@ -64,7 +64,9 @@ var async = require('async'),
category.isIgnored = results.isIgnored[0];
category.topic_row_size = 'col-md-9';
- callback(null, category);
+ plugins.fireHook('filter:category.get', {category: category, uid: data.uid}, function(err, data) {
+ callback(err, data ? data.category : null);
+ });
});
});
};
diff --git a/src/controllers/api.js b/src/controllers/api.js
index ae112b9bd6..c335cdf272 100644
--- a/src/controllers/api.js
+++ b/src/controllers/api.js
@@ -43,6 +43,8 @@ apiController.getConfig = function(req, res, next) {
config.maxReconnectionAttempts = meta.config.maxReconnectionAttempts || 5;
config.reconnectionDelay = meta.config.reconnectionDelay || 200;
config.tagsPerTopic = meta.config.tagsPerTopic || 5;
+ config.minimumTagLength = meta.config.minimumTagLength || 3;
+ config.maximumTagLength = meta.config.maximumTagLength || 15;
config.topicsPerPage = meta.config.topicsPerPage || 20;
config.postsPerPage = meta.config.postsPerPage || 20;
config.maximumFileSize = meta.config.maximumFileSize;
diff --git a/src/controllers/categories.js b/src/controllers/categories.js
index 25690a6810..9ef288272c 100644
--- a/src/controllers/categories.js
+++ b/src/controllers/categories.js
@@ -3,6 +3,7 @@
var categoriesController = {},
async = require('async'),
nconf = require('nconf'),
+ validator = require('validator'),
privileges = require('../privileges'),
user = require('../user'),
categories = require('../categories'),
@@ -22,7 +23,7 @@ categoriesController.recent = function(req, res, next) {
}
data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
- data['rssFeedUrl'] = nconf.get('relative_path') + '/recent.rss';
+ data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss';
data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[recent:title]]'}]);
res.render('recent', data);
});
@@ -92,6 +93,72 @@ categoriesController.unreadTotal = function(req, res, next) {
});
};
+categoriesController.list = function(req, res, next) {
+ async.parallel({
+ header: function (next) {
+ res.locals.metaTags = [{
+ name: "title",
+ content: validator.escape(meta.config.title || 'NodeBB')
+ }, {
+ name: "description",
+ content: validator.escape(meta.config.description || '')
+ }, {
+ property: 'og:title',
+ content: 'Index | ' + validator.escape(meta.config.title || 'NodeBB')
+ }, {
+ property: 'og:type',
+ content: 'website'
+ }];
+
+ if(meta.config['brand:logo']) {
+ res.locals.metaTags.push({
+ property: 'og:image',
+ content: meta.config['brand:logo']
+ });
+ }
+
+ next(null);
+ },
+ categories: function (next) {
+ var uid = req.user ? req.user.uid : 0;
+ categories.getCategoriesByPrivilege(uid, 'find', function (err, categoryData) {
+ if (err) {
+ return next(err);
+ }
+ var childCategories = [];
+
+ for(var i=categoryData.length - 1; i>=0; --i) {
+
+ if (Array.isArray(categoryData[i].children) && categoryData[i].children.length) {
+ childCategories.push.apply(childCategories, categoryData[i].children);
+ }
+
+ if (categoryData[i].parent && categoryData[i].parent.cid) {
+ categoryData.splice(i, 1);
+ }
+ }
+
+ async.parallel([
+ function(next) {
+ categories.getRecentTopicReplies(categoryData, uid, next);
+ },
+ function(next) {
+ categories.getRecentTopicReplies(childCategories, uid, next);
+ }
+ ], function(err) {
+ next(err, categoryData);
+ });
+ });
+ }
+ }, function (err, data) {
+ if (err) {
+ return next(err);
+ }
+ // TODO: template should be called categories.tpl
+ res.render('home', data);
+ });
+};
+
categoriesController.get = function(req, res, next) {
var cid = req.params.category_id,
page = req.query.page || 1,
@@ -259,7 +326,7 @@ categoriesController.get = function(req, res, next) {
data.currentPage = page;
data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
- data['rssFeedUrl'] = nconf.get('relative_path') + '/category/' + data.cid + '.rss';
+ data.rssFeedUrl = nconf.get('relative_path') + '/category/' + data.cid + '.rss';
data.pagination = pagination.create(data.currentPage, data.pageCount);
data.pagination.rel.forEach(function(rel) {
diff --git a/src/controllers/index.js b/src/controllers/index.js
index ff337250f4..805ba52d20 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -1,21 +1,10 @@
"use strict";
-var topicsController = require('./topics'),
- categoriesController = require('./categories'),
- tagsController = require('./tags'),
- searchController = require('./search'),
- usersController = require('./users'),
- groupsController = require('./groups'),
- accountsController = require('./accounts'),
- staticController = require('./static'),
- apiController = require('./api'),
- adminController = require('./admin'),
- helpers = require('./helpers'),
-
- async = require('async'),
+var async = require('async'),
nconf = require('nconf'),
validator = require('validator'),
winston = require('winston'),
+
auth = require('../routes/authentication'),
meta = require('../meta'),
user = require('../user'),
@@ -23,85 +12,32 @@ var topicsController = require('./topics'),
topics = require('../topics'),
plugins = require('../plugins'),
categories = require('../categories'),
- privileges = require('../privileges');
+ privileges = require('../privileges'),
+ helpers = require('./helpers');
var Controllers = {
- topics: topicsController,
- categories: categoriesController,
- tags: tagsController,
- search: searchController,
- users: usersController,
- groups: groupsController,
- accounts: accountsController,
- static: staticController,
- api: apiController,
- admin: adminController
+ topics: require('./topics'),
+ categories: require('./categories'),
+ tags: require('./tags'),
+ search: require('./search'),
+ users: require('./users'),
+ groups: require('./groups'),
+ accounts: require('./accounts'),
+ static: require('./static'),
+ api: require('./api'),
+ admin: require('./admin'),
};
Controllers.home = function(req, res, next) {
- async.parallel({
- header: function (next) {
- res.locals.metaTags = [{
- name: "title",
- content: validator.escape(meta.config.title || 'NodeBB')
- }, {
- name: "description",
- content: validator.escape(meta.config.description || '')
- }, {
- property: 'og:title',
- content: 'Index | ' + validator.escape(meta.config.title || 'NodeBB')
- }, {
- property: 'og:type',
- content: 'website'
- }];
-
- if(meta.config['brand:logo']) {
- res.locals.metaTags.push({
- property: 'og:image',
- content: meta.config['brand:logo']
- });
- }
-
- next(null);
- },
- categories: function (next) {
- var uid = req.user ? req.user.uid : 0;
- categories.getCategoriesByPrivilege(uid, 'find', function (err, categoryData) {
- if (err) {
- return next(err);
- }
- var childCategories = [];
-
- for(var i=categoryData.length - 1; i>=0; --i) {
-
- if (Array.isArray(categoryData[i].children) && categoryData[i].children.length) {
- childCategories.push.apply(childCategories, categoryData[i].children);
- }
-
- if (categoryData[i].parent && categoryData[i].parent.cid) {
- categoryData.splice(i, 1);
- }
- }
-
- async.parallel([
- function(next) {
- categories.getRecentTopicReplies(categoryData, uid, next);
- },
- function(next) {
- categories.getRecentTopicReplies(childCategories, uid, next);
- }
- ], function(err) {
- next(err, categoryData);
- });
- });
- }
- }, function (err, data) {
- if (err) {
- return next(err);
- }
- res.render('home', data);
- });
+ var route = meta.config.homePageRoute || 'home';
+ if (route === 'home') {
+ return Controllers.categories.list(req, res, next);
+ } else if (route === 'recent') {
+ Controllers.categories.recent(req, res, next);
+ } else if (route === 'popular') {
+ Controllers.categories.popular(req, res, next);
+ }
};
Controllers.reset = function(req, res, next) {
diff --git a/src/controllers/templates.js b/src/controllers/templates.js
new file mode 100644
index 0000000000..0d686a1455
--- /dev/null
+++ b/src/controllers/templates.js
@@ -0,0 +1,90 @@
+"use strict";
+
+var async = require('async'),
+ nconf = require('nconf'),
+ fs = require('fs'),
+ path = require('path'),
+ meta = require('../meta'),
+ plugins = require('../plugins'),
+ utils = require('../../public/src/utils'),
+ templatesController = {};
+
+
+var availableTemplatesCache = null;
+var configCache = null;
+
+templatesController.getTemplatesListing = function(req, res, next) {
+ async.parallel({
+ availableTemplates: function(next) {
+ getAvailableTemplates(next);
+ },
+ templatesConfig: function(next) {
+ async.waterfall([
+ function(next) {
+ readConfigFile(next);
+ },
+ function(config, next) {
+ config.custom_mapping['^/?$'] = meta.config.homePageRoute || 'home';
+
+ plugins.fireHook('filter:templates.get_config', config, next);
+ }
+ ], next);
+ },
+ }, function(err, results) {
+ if (err) {
+ return next(err);
+ }
+
+ res.json(results);
+ });
+};
+
+function readConfigFile(callback) {
+ if (configCache) {
+ return callback(null, configCache);
+ }
+ fs.readFile(path.join(nconf.get('views_dir'), 'config.json'), function(err, config) {
+ if (err) {
+ return callback(err);
+ }
+ try {
+ config = JSON.parse(config.toString());
+ } catch (err) {
+ return callback(err);
+ }
+ configCache = config;
+ callback(null, config);
+ });
+}
+
+function getAvailableTemplates(callback) {
+ if (availableTemplatesCache) {
+ return callback(null, availableTemplatesCache);
+ }
+
+ async.parallel({
+ views: function(next) {
+ utils.walk(nconf.get('views_dir'), next);
+ },
+ extended: function(next) {
+ plugins.fireHook('filter:templates.get_virtual', [], next);
+ }
+ }, function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+ var availableTemplates = results.views.filter(function(value, index, self) {
+ return value && self.indexOf(value) === index;
+ }).map(function(el) {
+ return el && el.replace(nconf.get('views_dir') + '/', '');
+ });
+
+ availableTemplatesCache = availableTemplates.concat(results.extended);
+ callback(null, availableTemplatesCache);
+ });
+
+}
+
+
+
+module.exports = templatesController;
diff --git a/src/install.js b/src/install.js
index 54fb075ced..45d44d75ad 100644
--- a/src/install.js
+++ b/src/install.js
@@ -389,13 +389,23 @@ function createWelcomePost(next) {
var db = require('./database'),
Topics = require('./topics');
- db.sortedSetCard('topics:tid', function(err, numTopics) {
- if (numTopics === 0) {
+ async.parallel([
+ function(next) {
+ fs.readFile(path.join(__dirname, '../', 'install/data/welcome.md'), next);
+ },
+ function(next) {
+ db.getObjectField('global', 'topicCount', next);
+ }
+ ], function(err, results) {
+ var content = results[0],
+ numTopics = results[1];
+
+ if (!parseInt(numTopics, 10)) {
Topics.post({
uid: 1,
cid: 2,
title: 'Welcome to your NodeBB!',
- content: '# Welcome to your brand new NodeBB forum!\n\nThis is what a topic and post looks like. As an administator, you can edit the post\'s title and content.\n\nTo customise your forum, go to the [Administrator Control Panel](../../admin). You can modify all aspects of your forum there, including installation of third-party plugins.\n\n## Additional Resources\n\n* [NodeBB Documentation](https://docs.nodebb.org)\n* [Community Support Forum](https://community.nodebb.org)\n* [Project repository](https://github.com/nodebb/nodebb)'
+ content: content.toString()
}, next);
} else {
next();
@@ -420,12 +430,24 @@ function enableDefaultPlugins(next) {
function setCopyrightWidget(next) {
var db = require('./database');
-
- db.init(function(err) {
- if (!err) {
- db.setObjectField('widgets:global', 'footer', "[{\"widget\":\"html\",\"data\":{\"html\":\"\",\"title\":\"\",\"container\":\"\"}}]", next);
+ async.parallel({
+ footerJSON: function(next) {
+ fs.readFile(path.join(__dirname, '../', 'install/data/footer.json'), next);
+ },
+ footer: function(next) {
+ db.getObjectField('widgets:global', 'footer', next);
}
- });
+ }, function(err, results) {
+ if (err) {
+ return next(err);
+ }
+
+ if (!results.footer && results.footerJSON) {
+ db.setObjectField('widgets:global', 'footer', results.footerJSON.toString(), next);
+ } else {
+ next();
+ }
+ });
}
install.setup = function (callback) {
diff --git a/src/plugins.js b/src/plugins.js
index 0c8a4ba394..730377c673 100644
--- a/src/plugins.js
+++ b/src/plugins.js
@@ -6,6 +6,7 @@ var fs = require('fs'),
winston = require('winston'),
semver = require('semver'),
express = require('express'),
+ nconf = require('nconf'),
db = require('./database'),
emitter = require('./emitter'),
@@ -13,6 +14,7 @@ var fs = require('fs'),
translator = require('../public/src/translator'),
utils = require('../public/src/utils'),
hotswap = require('./hotswap'),
+ pkg = require('../package.json'),
controllers = require('./controllers'),
app, middleware;
@@ -169,7 +171,7 @@ var fs = require('fs'),
Plugins.getAll = function(callback) {
var request = require('request');
- request('https://packages.nodebb.org/api/v1/plugins', function(err, res, body) {
+ request((nconf.get('registry') || 'https://packages.nodebb.org') + '/api/v1/plugins/' + pkg.version, function(err, res, body) {
var plugins = [];
try {
@@ -184,8 +186,9 @@ var fs = require('fs'),
plugins[i].id = plugins[i].name;
plugins[i].installed = false;
plugins[i].active = false;
- plugins[i].url = plugins[i].repository ? plugins[i].repository.url : '';
+ plugins[i].url = plugins[i].url ? plugins[i].url : plugins[i].repository ? plugins[i].repository.url : '';
plugins[i].latest = getLatestVersion(plugins[i].versions);
+ // plugins[i].latest = plugins[i].latest;
pluginMap[plugins[i].name] = plugins[i];
}
@@ -261,7 +264,7 @@ var fs = require('fs'),
function(dirs, next) {
dirs = dirs.filter(function(dir){
- return dir.substr(0, 14) === 'nodebb-plugin-' || dir.substr(0, 14) === 'nodebb-widget-';
+ return dir.startsWith('nodebb-plugin-') || dir.startsWith('nodebb-widget-') || dir.startsWith('nodebb-theme-')
}).map(function(dir){
return path.join(npmPluginPath, dir);
});
diff --git a/src/postTools.js b/src/postTools.js
index c00a8a4b9c..0fe8be23f8 100644
--- a/src/postTools.js
+++ b/src/postTools.js
@@ -34,13 +34,14 @@ var winston = require('winston'),
},
function(postData, next) {
postData.content = data.content;
- plugins.fireHook('filter:post.save', postData, next);
+ plugins.fireHook('filter:post.edit', {post: postData, uid: data.uid}, next);
}
- ], function(err, postData) {
+ ], function(err, result) {
if (err) {
return callback(err);
}
+ var postData = result.post;
async.parallel({
post: function(next) {
var d = {
diff --git a/src/routes/api.js b/src/routes/api.js
index da281280cc..47632ccce7 100644
--- a/src/routes/api.js
+++ b/src/routes/api.js
@@ -1,17 +1,11 @@
"use strict";
-var path = require('path'),
- async = require('async'),
- fs = require('fs'),
- nconf = require('nconf'),
- express = require('express'),
+var express = require('express'),
posts = require('../posts'),
categories = require('../categories'),
- plugins = require('../plugins'),
- utils = require('../../public/src/utils'),
- uploadsController = require('../controllers/uploads');
-
+ uploadsController = require('../controllers/uploads'),
+ templatesController = require('../controllers/templates');
module.exports = function(app, middleware, controllers) {
@@ -22,7 +16,7 @@ module.exports = function(app, middleware, controllers) {
router.get('/widgets/render', controllers.api.renderWidgets);
router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.accounts.getUserByUID);
- router.get('/get_templates_listing', getTemplatesListing);
+ router.get('/get_templates_listing', templatesController.getTemplatesListing);
router.get('/categories/:cid/moderators', getModerators);
router.get('/recent/posts/:term?', getRecentPosts);
@@ -40,56 +34,6 @@ function getModerators(req, res, next) {
});
}
-var templatesListingCache = {};
-
-function getTemplatesListing(req, res, next) {
- if (templatesListingCache.availableTemplates && templatesListingCache.templatesConfig) {
- return res.json(templatesListingCache);
- }
-
- async.parallel({
- views: function(next) {
- utils.walk(nconf.get('views_dir'), next);
- },
- extended: function(next) {
- plugins.fireHook('filter:templates.get_virtual', [], next);
- },
- config: function(next) {
- fs.readFile(path.join(nconf.get('views_dir'), 'config.json'), function(err, config) {
- if (err) {
- return next(err);
- }
-
- try {
- config = JSON.parse(config.toString());
- } catch (err) {
- return next(err);
- }
-
- plugins.fireHook('filter:templates.get_config', config, next);
- });
- },
- }, function(err, results) {
- if (err) {
- return next(err);
- }
-
- var data = results.views.filter(function(value, index, self) {
- return value && self.indexOf(value) === index;
- }).map(function(el) {
- return el && el.replace(nconf.get('views_dir') + '/', '');
- });
-
- data = data.concat(results.extended);
-
- templatesListingCache = {
- availableTemplates: data,
- templatesConfig: results.config
- };
-
- res.json(templatesListingCache);
- });
-}
function getRecentPosts(req, res, next) {
var uid = (req.user) ? req.user.uid : 0;
diff --git a/src/routes/index.js b/src/routes/index.js
index f8012c9456..94767dcc36 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -50,6 +50,7 @@ function tagRoutes(app, middleware, controllers) {
}
function categoryRoutes(app, middleware, controllers) {
+ setupPageRoute(app, '/categories', middleware, [], controllers.categories.list);
setupPageRoute(app, '/popular/:term?', middleware, [], controllers.categories.popular);
setupPageRoute(app, '/recent', middleware, [], controllers.categories.recent);
setupPageRoute(app, '/unread', middleware, [middleware.authenticate], controllers.categories.unread);
diff --git a/src/socket.io/user.js b/src/socket.io/user.js
index 4df8915994..fd23405758 100644
--- a/src/socket.io/user.js
+++ b/src/socket.io/user.js
@@ -84,35 +84,38 @@ SocketUser.reset.send = function(socket, email, callback) {
};
SocketUser.reset.commit = function(socket, data, callback) {
- if(data && data.code && data.password) {
- async.series([
- async.apply(db.getObjectField, 'reset:uid', data.code),
- async.apply(user.reset.commit, data.code, data.password)
- ], function(err, data) {
- if (err) {
- return callback(err);
- }
+ if (!data || !data.code || !data.password) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
- var uid = data[0],
- now = new Date(),
- parsedDate = now.getFullYear() + '/' + (now.getMonth()+1) + '/' + now.getDate();
+ async.parallel({
+ uid: async.apply(db.getObjectField, 'reset:uid', data.code),
+ reset: async.apply(user.reset.commit, data.code, data.password)
+ }, function(err, results) {
+ if (err) {
+ return callback(err);
+ }
- user.getUserField(uid, 'username', function(err, username) {
- emailer.send('reset_notify', uid, {
- username: username,
- date: parsedDate,
- site_title: meta.config.title || 'NodeBB',
- subject: '[[email:reset.notify.subject]]'
- });
- });
- events.log({
- type: 'password-reset',
- uid: socket.uid,
- ip: socket.ip
+ var uid = results.uid,
+ now = new Date(),
+ parsedDate = now.getFullYear() + '/' + (now.getMonth()+1) + '/' + now.getDate();
+
+ user.getUserField(uid, 'username', function(err, username) {
+ emailer.send('reset_notify', uid, {
+ username: username,
+ date: parsedDate,
+ site_title: meta.config.title || 'NodeBB',
+ subject: '[[email:reset.notify.subject]]'
});
- callback();
});
- }
+
+ events.log({
+ type: 'password-reset',
+ uid: uid,
+ ip: socket.ip
+ });
+ callback();
+ });
};
SocketUser.checkStatus = function(socket, uid, callback) {
@@ -151,6 +154,7 @@ SocketUser.changePassword = function(socket, data, callback) {
targetUid: data.uid,
ip: socket.ip
});
+ callback();
});
};
diff --git a/src/user.js b/src/user.js
index 035933154a..109e67a3f0 100644
--- a/src/user.js
+++ b/src/user.js
@@ -7,6 +7,7 @@ var async = require('async'),
plugins = require('./plugins'),
db = require('./database'),
meta = require('./meta'),
+ topics = require('./topics'),
groups = require('./groups'),
Password = require('./password');
@@ -147,25 +148,31 @@ var async = require('async'),
User.updateOnlineUsers = function(uid, callback) {
callback = callback || function() {};
- db.sortedSetScore('users:online', uid, function(err, score) {
- var now = Date.now();
- if (err || now - parseInt(score, 10) < 300000) {
- return callback(err);
- }
- db.sortedSetAdd('users:online', now, uid, function(err) {
- if (err) {
- return callback(err);
+
+ var now = Date.now();
+ async.waterfall([
+ function(next) {
+ db.sortedSetScore('users:online', uid, next);
+ },
+ function(userOnlineTime, next) {
+ if (now - parseInt(userOnlineTime, 10) < 300000) {
+ return callback();
}
+ db.sortedSetAdd('users:online', now, uid, next);
+ },
+ function(next) {
+ topics.pushUnreadCount(uid);
plugins.fireHook('action:user.online', {uid: uid, timestamp: now});
- });
- });
+ next();
+ }
+ ], callback);
};
User.setUserField = function(uid, field, value, callback) {
callback = callback || function() {};
db.setObjectField('user:' + uid, field, value, function(err) {
if (err) {
- return callback(err)
+ return callback(err);
}
plugins.fireHook('action:user.set', {uid: uid, field: field, value: value, type: 'set'});
callback();
diff --git a/src/user/reset.js b/src/user/reset.js
index 3ce2f86cf8..60ff820dbf 100644
--- a/src/user/reset.js
+++ b/src/user/reset.js
@@ -63,7 +63,7 @@ var async = require('async'),
}
if (!validated) {
- return;
+ return callback(new Error('[[error:reset-code-not-valid]]'));
}
db.getObjectField('reset:uid', code, function(err, uid) {
diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl
index 5c35ea8542..66a27dee61 100644
--- a/src/views/admin/settings/general.tpl
+++ b/src/views/admin/settings/general.tpl
@@ -28,6 +28,20 @@
+
+