diff --git a/package.json b/package.json index 06cc7fb64e..f237242210 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "async": "~1.4.2", - "bcryptjs": "~2.2.1", + "bcryptjs": "~2.3.0", "body-parser": "^1.9.0", "colors": "^1.1.0", "compression": "^1.1.0", @@ -39,18 +39,18 @@ "mkdirp": "~0.5.0", "mmmagic": "^0.4.0", "morgan": "^1.3.2", - "nconf": "~0.7.1", - "nodebb-plugin-composer-default": "1.0.16", + "nconf": "~0.8.2", + "nodebb-plugin-composer-default": "1.0.19", "nodebb-plugin-dbsearch": "0.2.17", - "nodebb-plugin-emoji-extended": "0.4.14", + "nodebb-plugin-emoji-extended": "0.4.15", "nodebb-plugin-markdown": "4.0.6", - "nodebb-plugin-mentions": "1.0.6", + "nodebb-plugin-mentions": "1.0.8", "nodebb-plugin-soundpack-default": "0.1.4", "nodebb-plugin-spam-be-gone": "0.4.2", "nodebb-rewards-essentials": "0.0.5", - "nodebb-theme-lavender": "2.0.6", - "nodebb-theme-persona": "3.0.45", - "nodebb-theme-vanilla": "4.0.20", + "nodebb-theme-lavender": "2.0.8", + "nodebb-theme-persona": "3.0.56", + "nodebb-theme-vanilla": "4.0.24", "nodebb-widget-essentials": "2.0.2", "npm": "^2.1.4", "passport": "^0.3.0", @@ -73,7 +73,7 @@ "underscore.deep": "^0.5.1", "validator": "^4.0.5", "winston": "^1.0.1", - "xregexp": "~2.0.0" + "xregexp": "~3.0.0" }, "devDependencies": { "mocha": "~1.13.0", diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index 252e893d0d..167ab1aa66 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -77,7 +77,8 @@ "group-name-too-short": "Group name too short", "group-already-exists": "Group already exists", "group-name-change-not-allowed": "Group name change not allowed", - "group-already-member": "You are already part of this group", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", "group-needs-owner": "This group requires at least one owner", "group-already-invited": "This user has already been invited", "group-already-requested": "Your membership request has already been submitted", diff --git a/public/language/en_GB/modules.json b/public/language/en_GB/modules.json index e4f509842f..673ad026a9 100644 --- a/public/language/en_GB/modules.json +++ b/public/language/en_GB/modules.json @@ -24,6 +24,7 @@ "composer.discard": "Are you sure you wish to discard this post?", "composer.submit_and_lock": "Submit and Lock", "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", "bootbox.ok": "OK", "bootbox.cancel": "Cancel", diff --git a/public/language/en_GB/notifications.json b/public/language/en_GB/notifications.json index 52d5a10194..7a96e0da53 100644 --- a/public/language/en_GB/notifications.json +++ b/public/language/en_GB/notifications.json @@ -14,8 +14,8 @@ "new_message_from": "New message from %1", "upvoted_your_post_in": "%1 has upvoted your post in %2.", - "moved_your_post": "%1 has moved your post.", - "moved_your_topic": "%1 has moved your topic.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", "favourited_your_post_in": "%1 has favourited your post in %2.", "user_flagged_post_in": "%1 flagged a post in %2", "user_posted_to" : "%1 has posted a reply to: %2", diff --git a/public/language/en_GB/pages.json b/public/language/en_GB/pages.json index 6d6164ef9e..f398d5b1ff 100644 --- a/public/language/en_GB/pages.json +++ b/public/language/en_GB/pages.json @@ -29,6 +29,9 @@ "chat": "Chatting with %1", "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", "account/following": "People %1 follows", "account/followers": "People who follow %1", "account/posts": "Posts made by %1", diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index ab2031be4d..c26d0c05f8 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -32,7 +32,6 @@ "bookmark_instructions" : "Click here to return to the last unread post in this thread.", "flag_title": "Flag this post for moderation", - "flag_confirm": "Are you sure you want to flag this post?", "flag_success": "This post has been flagged for moderation.", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", @@ -117,5 +116,10 @@ "most_votes": "Most votes", "most_posts": "Most posts", - "stale_topic_warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?" + "stale_topic_warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + + "spam": "Spam", + "offensive": "Offensive", + "custom-flag-reason": "Enter a flagging reason" + } diff --git a/public/language/en_GB/user.json b/public/language/en_GB/user.json index 3505d1814c..a4b3f33475 100644 --- a/public/language/en_GB/user.json +++ b/public/language/en_GB/user.json @@ -38,6 +38,8 @@ "profile_update_success": "Profile has been updated successfully!", "change_picture": "Change Picture", + "change_username": "Change Username", + "change_email": "Change Email", "edit": "Edit", "default_picture": "Default Icon", "uploaded_picture": "Uploaded Picture", diff --git a/public/less/admin/general/navigation.less b/public/less/admin/general/navigation.less index 1353d35013..bb3efac15b 100644 --- a/public/less/admin/general/navigation.less +++ b/public/less/admin/general/navigation.less @@ -2,6 +2,7 @@ #navigation { + #active-navigation { .active { background-color: #eee; @@ -23,6 +24,9 @@ .iconPicker i { cursor: pointer; } + .form-group { + min-height: 80px; + } } ul { diff --git a/public/less/admin/manage/groups.less b/public/less/admin/manage/groups.less index f6dac84921..ceeba06df7 100644 --- a/public/less/admin/manage/groups.less +++ b/public/less/admin/manage/groups.less @@ -1,5 +1,20 @@ .group { - .current_members { + [component="groups/members"] { padding: 0; + tbody { + max-height: 500px; + display: block; + overflow-y: auto; + padding-bottom: 100px; + .member-name { + width: 100%; + } + } + + + img { + width: 32px; + height: 32px; + } } } \ No newline at end of file diff --git a/public/less/generics.less b/public/less/generics.less index ce824ab04f..4115f7d117 100644 --- a/public/less/generics.less +++ b/public/less/generics.less @@ -9,7 +9,7 @@ .border-radius(3px); &.disabled { - -webkit-filter: grayscale(30%); + background-color: #888!important; .opacity(0.5); } } @@ -54,4 +54,4 @@ height: 100%; vertical-align: middle; } -} \ No newline at end of file +} diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index 0309fb48fd..88998bc5f6 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -47,7 +47,8 @@ function selectMenuItem(url) { url = url .replace(/\/\d+$/, '') - .split('/').slice(0, 3).join('/'); + .split('/').slice(0, 3).join('/') + .split('?')[0]; // If index is requested, load the dashboard if (url === 'admin') { diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js index 1d346ccd8a..a57fc06701 100644 --- a/public/src/admin/manage/categories.js +++ b/public/src/admin/manage/categories.js @@ -1,5 +1,5 @@ "use strict"; -/*global define, socket, app, bootbox, templates, ajaxify, RELATIVE_PATH, Sortable */ +/*global define, socket, app, bootbox, templates, ajaxify, Sortable */ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-serializeobject.min'], function() { var Categories = {}, newCategoryId = -1, sortables; @@ -17,12 +17,17 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri // Enable/Disable toggle events $('.categories').on('click', 'button[data-action="toggle"]', function() { - var self = $(this), - rowEl = self.parents('li'), - cid = rowEl.attr('data-cid'), - disabled = rowEl.hasClass('disabled'); + var $this = $(this), + cid = $this.attr('data-cid'), + parentEl = $this.parents('li[data-cid="' + cid + '"]'), + disabled = parentEl.hasClass('disabled'); - Categories.toggle(cid, disabled); + var children = parentEl.find('li[data-cid]').map(function() { + return $(this).attr('data-cid'); + }).get(); + + Categories.toggle([cid].concat(children), !disabled); + return false; }); }; @@ -94,14 +99,16 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri } }; - Categories.toggle = function(cid, disabled) { + Categories.toggle = function(cids, disabled) { var payload = {}; - payload[cid] = { - disabled: disabled ? 1 : 0 - }; + cids.forEach(function(cid) { + payload[cid] = { + disabled: disabled ? 1 : 0 + }; + }); - socket.emit('admin.categories.update', payload, function(err, result) { + socket.emit('admin.categories.update', payload, function(err) { if (err) { return app.alertError(err.message); } diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 441afe29d6..0337064ad3 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -240,7 +240,7 @@ define('admin/manage/category', [ } categories = categories.filter(function(category) { - return category && parseInt(category.cid, 10) !== parseInt(ajaxify.data.category.cid, 10); + return category && !category.disabled && parseInt(category.cid, 10) !== parseInt(ajaxify.data.category.cid, 10); }); templates.parse('partials/category_list', { diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js index 37d2f64295..c708cc3a14 100644 --- a/public/src/admin/manage/group.js +++ b/public/src/admin/manage/group.js @@ -1,16 +1,16 @@ "use strict"; -/*global define, templates, socket, ajaxify, app, admin, bootbox, utils, config */ +/*global define, templates, socket, ajaxify, app, bootbox, translator */ define('admin/manage/group', [ + 'forum/groups/memberlist', 'iconSelect', 'admin/modules/colorpicker' -], function(iconSelect, colorpicker) { +], function(memberList, iconSelect, colorpicker) { var Groups = {}; Groups.init = function() { var groupDetailsSearch = $('#group-details-search'), groupDetailsSearchResults = $('#group-details-search-results'), - groupMembersEl = $('ul.current_members'), groupIcon = $('#group-icon'), changeGroupUserTitle = $('#change-group-user-title'), changeGroupLabelColor = $('#change-group-label-color'), @@ -20,6 +20,8 @@ define('admin/manage/group', [ var groupName = ajaxify.data.group.name; + memberList.init(); + changeGroupUserTitle.keyup(function() { groupLabelPreview.text(changeGroupUserTitle.val()); }); @@ -46,10 +48,16 @@ define('admin/manage/group', [ } groupDetailsSearchResults.empty(); + for (x = 0; x < numResults; x++) { foundUser = $('
  • '); foundUser - .attr({title: results.users[x].username, 'data-uid': results.users[x].uid}) + .attr({title: results.users[x].username, + 'data-uid': results.users[x].uid, + 'data-username': results.users[x].username, + 'data-userslug': results.users[x].userslug, + 'data-picture': results.users[x].picture + }) .append($('').attr('src', results.users[x].picture)) .append($('').html(results.users[x].username)); @@ -64,45 +72,74 @@ define('admin/manage/group', [ groupDetailsSearchResults.on('click', 'li[data-uid]', function() { var userLabel = $(this), - uid = parseInt(userLabel.attr('data-uid'), 10), - members = []; + uid = parseInt(userLabel.attr('data-uid'), 10); - groupMembersEl.find('li[data-uid]').each(function() { - members.push(parseInt($(this).attr('data-uid'), 10)); - }); - - if (members.indexOf(uid) === -1) { - socket.emit('admin.groups.join', { - groupName: groupName, - uid: uid - }, function(err, data) { - if (!err) { - groupMembersEl.append(userLabel.clone(true)); - } - }); - } - }); - - groupMembersEl.on('click', 'li[data-uid]', function() { - var uid = $(this).attr('data-uid'); - - bootbox.confirm('Are you sure you want to remove this user?', function(confirm) { - if (!confirm) { - return; + socket.emit('admin.groups.join', { + groupName: groupName, + uid: uid + }, function(err) { + if (err) { + return app.alertError(err.message); } - socket.emit('admin.groups.leave', { - groupName: groupName, - uid: uid - }, function(err, data) { - if (err) { - return app.alertError(err.message); - } - groupMembersEl.find('li[data-uid="' + uid + '"]').remove(); + var member = { + uid: userLabel.attr('data-uid'), + username: userLabel.attr('data-username'), + userslug: userLabel.attr('data-userslug'), + picture: userLabel.attr('data-picture') + }; + + templates.parse('partials/groups/memberlist', 'members', {group: {isOwner: ajaxify.data.group.isOwner, members: [member]}}, function(html) { + translator.translate(html, function(html) { + $('[component="groups/members"] tr').first().before(html); + }); }); }); }); + $('[component="groups/members"]').on('click', '[data-action]', function() { + var btnEl = $(this), + userRow = btnEl.parents('[data-uid]'), + ownerFlagEl = userRow.find('.member-name i'), + isOwner = !ownerFlagEl.hasClass('invisible') ? true : false, + uid = userRow.attr('data-uid'), + action = btnEl.attr('data-action'); + + switch(action) { + case 'toggleOwnership': + socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), { + toUid: uid, + groupName: groupName + }, function(err) { + if (err) { + return app.alertError(err.message); + } + ownerFlagEl.toggleClass('invisible'); + }); + break; + + case 'kick': + bootbox.confirm('Are you sure you want to remove this user?', function(confirm) { + if (!confirm) { + return; + } + socket.emit('admin.groups.leave', { + uid: uid, + groupName: groupName + }, function(err) { + if (err) { + return app.alertError(err.message); + } + userRow.slideUp().remove(); + }); + + }); + break; + default: + break; + } + }); + $('#group-icon').on('click', function() { iconSelect.init(groupIcon); }); diff --git a/public/src/admin/manage/tags.js b/public/src/admin/manage/tags.js index d85df5c266..27c5b2459b 100644 --- a/public/src/admin/manage/tags.js +++ b/public/src/admin/manage/tags.js @@ -1,5 +1,5 @@ "use strict"; -/*global define, socket, app, admin, utils, bootbox, RELATIVE_PATH*/ +/*global define, socket, app, utils, bootbox*/ define('admin/manage/tags', [ 'forum/infinitescroll', @@ -25,12 +25,12 @@ define('admin/manage/tags', [ } timeoutId = setTimeout(function() { - socket.emit('topics.searchAndLoadTags', {query: $('#tag-search').val()}, function(err, tags) { + socket.emit('topics.searchAndLoadTags', {query: $('#tag-search').val()}, function(err, result) { if (err) { return app.alertError(err.message); } - infinitescroll.parseAndTranslate('admin/manage/tags', 'tags', {tags: tags}, function(html) { + infinitescroll.parseAndTranslate('admin/manage/tags', 'tags', {tags: result.tags}, function(html) { $('.tag-list').html(html); utils.makeNumbersHumanReadable(html.find('.human-readable-number')); timeoutId = 0; @@ -43,7 +43,7 @@ define('admin/manage/tags', [ } function handleModify() { - $('#modify').on('click', function(ev) { + $('#modify').on('click', function() { var tagsToModify = $('.tag-row.selected'); if (!tagsToModify.length) { return; diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index 2f11307471..19c0abe8fd 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -46,7 +46,7 @@ $(document).ready(function() { return true; } - app.enterRoom(''); + app.leaveCurrentRoom(); $(window).off('scroll'); @@ -56,6 +56,7 @@ $(document).ready(function() { url = ajaxify.start(url, quiet); + $('body').removeClass(ajaxify.data.bodyClass); $('#footer, #content').removeClass('hide').addClass('ajaxifying'); ajaxify.loadData(url, function(err, data) { @@ -141,6 +142,7 @@ $(document).ready(function() { templates.parse(tpl_url, data, function(template) { translator.translate(template, function(translatedTemplate) { + $('body').addClass(data.bodyClass); $('#content').html(translatedTemplate); ajaxify.end(url, tpl_url); @@ -222,9 +224,7 @@ $(document).ready(function() { $(window).trigger('action:ajaxify.dataLoaded', {url: url, data: data}); - if (callback) { - callback(null, data); - } + callback(null, data); }, error: function(data, textStatus) { if (data.status === 0 && textStatus === 'error') { diff --git a/public/src/app.js b/public/src/app.js index e2265fbe24..f739ac9c4f 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -71,12 +71,14 @@ app.cacheBuster = null; } }); - require(['taskbar', 'helpers'], function(taskbar, helpers) { + require(['taskbar', 'helpers', 'forum/pagination'], function(taskbar, helpers, pagination) { taskbar.init(); // templates.js helpers helpers.register(); + pagination.init(); + $(window).trigger('action:app.load'); }); }); @@ -143,14 +145,25 @@ app.cacheBuster = null; 'icon:text': app.user['icon:text'] }, function(err) { if (err) { - app.alertError(err.message); - return; + return app.alertError(err.message); } app.currentRoom = room; }); } }; + app.leaveCurrentRoom = function() { + if (!socket) { + return; + } + socket.emit('meta.rooms.leaveCurrent', function(err) { + if (err) { + return app.alertError(err.message); + } + app.currentRoom = ''; + }); + } + function highlightNavigationLink() { var path = window.location.pathname; $('#main-nav li').removeClass('active'); diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js index 7271e854bc..7969a1bcf1 100644 --- a/public/src/client/account/edit.js +++ b/public/src/client/account/edit.js @@ -1,12 +1,11 @@ 'use strict'; -/* globals define, ajaxify, socket, app, config, utils, bootbox */ +/* globals define, ajaxify, socket, app, config, templates, bootbox */ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], function(header, uploader, translator) { var AccountEdit = {}, uploadedPicture = '', - selectedImageType = '', - currentEmail; + selectedImageType = ''; AccountEdit.init = function() { uploadedPicture = ajaxify.data.uploadedpicture; @@ -23,12 +22,9 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], }); }); - currentEmail = $('#inputEmail').val(); - handleImageChange(); handleAccountDelete(); handleEmailConfirm(); - handlePasswordChange(); updateSignature(); updateAboutMe(); }; @@ -36,8 +32,6 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], function updateProfile() { var userData = { uid: $('#inputUID').val(), - username: $('#inputUsername').val(), - email: $('#inputEmail').val(), fullname: $('#inputFullname').val(), website: $('#inputWebsite').val(), birthday: $('#inputBirthday').val(), @@ -57,27 +51,13 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], $('#user-current-picture').attr('src', data.picture); } - if (data.userslug) { - var oldslug = $('.account-username-box').attr('data-userslug'); - $('.account-username-box a').each(function() { - $(this).attr('href', $(this).attr('href').replace(oldslug, data.userslug)); - }); - - $('.account-username-box').attr('data-userslug', data.userslug); - } - - if (currentEmail !== data.email) { - currentEmail = data.email; - $('#confirm-email').removeClass('hide'); - } - - updateHeader(data.picture, userData.username, data.userslug); + updateHeader(data.picture); }); return false; } - function updateHeader(picture, username, userslug) { + function updateHeader(picture) { require(['components'], function(components) { if (parseInt(ajaxify.data.theirid, 10) !== parseInt(ajaxify.data.yourid, 10)) { return; @@ -88,11 +68,6 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], if (picture) { components.get('header/userpicture').attr('src', picture); } - - if (username && userslug) { - components.get('header/profilelink').attr('href', config.relative_path + '/user/' + userslug); - components.get('header/username').text(username); - } }); } @@ -273,88 +248,6 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], }); } - function handlePasswordChange() { - var currentPassword = $('#inputCurrentPassword'); - var password_notify = $('#password-notify'); - var password_confirm_notify = $('#password-confirm-notify'); - var password = $('#inputNewPassword'); - var password_confirm = $('#inputNewPasswordAgain'); - var passwordvalid = false; - var passwordsmatch = false; - - function onPasswordChanged() { - if (password.val().length < config.minimumPasswordLength) { - showError(password_notify, '[[user:change_password_error_length]]'); - passwordvalid = false; - } else if (!utils.isPasswordValid(password.val())) { - showError(password_notify, '[[user:change_password_error]]'); - passwordvalid = false; - } else { - showSuccess(password_notify); - passwordvalid = true; - } - } - - function onPasswordConfirmChanged() { - if (password.val() !== password_confirm.val()) { - showError(password_confirm_notify, '[[user:change_password_error_match]]'); - passwordsmatch = false; - } else { - if (password.val()) { - showSuccess(password_confirm_notify); - } else { - password_confirm_notify.parent().removeClass('alert-success alert-danger'); - password_confirm_notify.children().show(); - password_confirm_notify.find('.msg').html(''); - } - - passwordsmatch = true; - } - } - - password.on('blur', onPasswordChanged); - password_confirm.on('blur', onPasswordConfirmChanged); - - $('#changePasswordBtn').on('click', function() { - onPasswordChanged(); - onPasswordConfirmChanged(); - - 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.data.theirid - }, function(err) { - btn.removeClass('disabled').find('i').addClass('hide'); - currentPassword.val(''); - password.val(''); - password_confirm.val(''); - passwordsmatch = false; - passwordvalid = false; - - if (err) { - onPasswordChanged(); - onPasswordConfirmChanged(); - return app.alertError(err.message); - } - - app.alertSuccess('[[user:change_password_success]]'); - }); - } else { - if (!passwordsmatch) { - app.alertError('[[user:change_password_error_match]]'); - } - - if (!passwordvalid) { - app.alertError('[[user:change_password_error]]'); - } - } - return false; - }); - } - function changeUserPicture(type, callback) { socket.emit('user.changePicture', { type: type, @@ -384,24 +277,6 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator'], }); } - function showError(element, msg) { - translator.translate(msg, function(msg) { - element.find('.error').html(msg).removeClass('hide').siblings().addClass('hide'); - - element.parent() - .removeClass('alert-success') - .addClass('alert-danger'); - element.show(); - }); - } - - function showSuccess(element) { - element.find('.success').removeClass('hide').siblings().addClass('hide'); - element.parent() - .removeClass('alert-danger') - .addClass('alert-success'); - element.show(); - } return AccountEdit; -}); \ No newline at end of file +}); diff --git a/public/src/client/account/edit/email.js b/public/src/client/account/edit/email.js new file mode 100644 index 0000000000..4014039740 --- /dev/null +++ b/public/src/client/account/edit/email.js @@ -0,0 +1,39 @@ +'use strict'; + +/* globals define, ajaxify, socket, app */ + +define('forum/account/edit/email', ['forum/account/header'], function(header) { + var AccountEditEmail = {}; + + AccountEditEmail.init = function() { + header.init(); + + $('#submitBtn').on('click', function () { + var userData = { + uid: $('#inputUID').val(), + email: $('#inputNewEmail').val(), + password: $('#inputCurrentPassword').val() + }; + + if (!userData.email) { + return; + } + + var btn = $(this); + btn.addClass('disabled').find('i').removeClass('hide'); + + socket.emit('user.changeUsernameEmail', userData, function(err) { + btn.removeClass('disabled').find('i').addClass('hide'); + if (err) { + return app.alertError(err.message); + } + + ajaxify.go('user/' + ajaxify.data.userslug); + }); + + return false; + }); + }; + + return AccountEditEmail; +}); diff --git a/public/src/client/account/edit/password.js b/public/src/client/account/edit/password.js new file mode 100644 index 0000000000..3e67045925 --- /dev/null +++ b/public/src/client/account/edit/password.js @@ -0,0 +1,116 @@ +'use strict'; + +/* globals define, ajaxify, socket, app, config, utils */ + +define('forum/account/edit/password', ['forum/account/header', 'translator'], function(header, translator) { + var AccountEditPassword = {}; + + AccountEditPassword.init = function() { + header.init(); + + handlePasswordChange(); + }; + + function handlePasswordChange() { + var currentPassword = $('#inputCurrentPassword'); + var password_notify = $('#password-notify'); + var password_confirm_notify = $('#password-confirm-notify'); + var password = $('#inputNewPassword'); + var password_confirm = $('#inputNewPasswordAgain'); + var passwordvalid = false; + var passwordsmatch = false; + + function onPasswordChanged() { + if (password.val().length < config.minimumPasswordLength) { + showError(password_notify, '[[user:change_password_error_length]]'); + passwordvalid = false; + } else if (!utils.isPasswordValid(password.val())) { + showError(password_notify, '[[user:change_password_error]]'); + passwordvalid = false; + } else { + showSuccess(password_notify); + passwordvalid = true; + } + } + + function onPasswordConfirmChanged() { + if (password.val() !== password_confirm.val()) { + showError(password_confirm_notify, '[[user:change_password_error_match]]'); + passwordsmatch = false; + } else { + if (password.val()) { + showSuccess(password_confirm_notify); + } else { + password_confirm_notify.parent().removeClass('alert-success alert-danger'); + password_confirm_notify.children().show(); + password_confirm_notify.find('.msg').html(''); + } + + passwordsmatch = true; + } + } + + password.on('blur', onPasswordChanged); + password_confirm.on('blur', onPasswordConfirmChanged); + + $('#changePasswordBtn').on('click', function() { + onPasswordChanged(); + onPasswordConfirmChanged(); + + 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.data.theirid + }, function(err) { + btn.removeClass('disabled').find('i').addClass('hide'); + currentPassword.val(''); + password.val(''); + password_confirm.val(''); + passwordsmatch = false; + passwordvalid = false; + + if (err) { + onPasswordChanged(); + onPasswordConfirmChanged(); + return app.alertError(err.message); + } + ajaxify.go('user/' + ajaxify.data.userslug); + app.alertSuccess('[[user:change_password_success]]'); + }); + } else { + if (!passwordsmatch) { + app.alertError('[[user:change_password_error_match]]'); + } + + if (!passwordvalid) { + app.alertError('[[user:change_password_error]]'); + } + } + return false; + }); + } + + function showError(element, msg) { + translator.translate(msg, function(msg) { + element.find('.error').html(msg).removeClass('hide').siblings().addClass('hide'); + + element.parent() + .removeClass('alert-success') + .addClass('alert-danger'); + element.show(); + }); + } + + function showSuccess(element) { + element.find('.success').removeClass('hide').siblings().addClass('hide'); + element.parent() + .removeClass('alert-danger') + .addClass('alert-success'); + element.show(); + } + + return AccountEditPassword; +}); diff --git a/public/src/client/account/edit/username.js b/public/src/client/account/edit/username.js new file mode 100644 index 0000000000..4fcddf81f5 --- /dev/null +++ b/public/src/client/account/edit/username.js @@ -0,0 +1,43 @@ +'use strict'; + +/* globals define, ajaxify, socket, app, utils, config */ + +define('forum/account/edit/username', ['forum/account/header'], function(header) { + var AccountEditUsername = {}; + + AccountEditUsername.init = function() { + header.init(); + + $('#submitBtn').on('click', function updateUsername() { + var userData = { + uid: $('#inputUID').val(), + username: $('#inputNewUsername').val(), + password: $('#inputCurrentPassword').val() + }; + + if (!userData.username) { + return; + } + var btn = $(this); + btn.addClass('disabled').find('i').removeClass('hide'); + socket.emit('user.changeUsernameEmail', userData, function(err) { + btn.removeClass('disabled').find('i').addClass('hide'); + if (err) { + return app.alertError(err.message); + } + + var userslug = utils.slugify(userData.username); + if (userData.username && userslug && parseInt(userData.uid, 10) === parseInt(app.user.uid, 10)) { + $('[component="header/profilelink"]').attr('href', config.relative_path + '/user/' + userslug); + $('[component="header/username"]').text(userData.username); + } + + ajaxify.go('user/' + userslug); + }); + + return false; + }); + }; + + return AccountEditUsername; +}); diff --git a/public/src/client/category.js b/public/src/client/category.js index 7a295c1b28..634317a21c 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -2,7 +2,6 @@ /* global define, config, templates, app, utils, ajaxify, socket */ define('forum/category', [ - 'forum/pagination', 'forum/infinitescroll', 'share', 'navigator', @@ -10,13 +9,12 @@ define('forum/category', [ 'sort', 'components', 'translator' - -], function(pagination, infinitescroll, share, navigator, categoryTools, sort, components, translator) { +], function(infinitescroll, share, navigator, categoryTools, sort, components, translator) { var Category = {}; $(window).on('action:ajaxify.start', function(ev, data) { if (ajaxify.currentPage !== data.url) { - navigator.hide(); + navigator.disable(); removeListeners(); } @@ -41,12 +39,12 @@ define('forum/category', [ sort.handleSort('categoryTopicSort', 'user.setCategorySort', 'category/' + ajaxify.data.slug); - enableInfiniteLoadingOrPagination(); - if (!config.usePagination) { navigator.init('[component="category/topic"]', ajaxify.data.topic_count, Category.toTop, Category.toBottom, Category.navigatorCallback); } + enableInfiniteLoadingOrPagination(); + $('[component="category"]').on('click', '[component="topic/header"]', function() { var clickedIndex = $(this).parents('[data-index]').attr('data-index'); $('[component="category/topic"]').each(function(index, el) { @@ -112,7 +110,7 @@ define('forum/category', [ if (config.usePagination) { var page = Math.ceil((parseInt(bookmarkIndex, 10) + 1) / config.topicsPerPage); - if (parseInt(page, 10) !== pagination.currentPage) { + if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) { pagination.loadPage(page, function() { Category.scrollToTopic(bookmarkIndex, clickedIndex, 400); }); @@ -175,8 +173,7 @@ define('forum/category', [ if (!config.usePagination) { infinitescroll.init($('[component="category"]'), Category.loadMoreTopics); } else { - navigator.hide(); - pagination.init(ajaxify.data.currentPage, ajaxify.data.pageCount); + navigator.disable(); } } diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 511a81ca15..3d7bd7bda0 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -140,7 +140,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', Chats.switchChat = function(uid, username) { if (!$('[component="chat/messages"]').length) { - ajaxify.go('chats/' + username); + return ajaxify.go('chats/' + utils.slugify(username)); } var contactEl = $('.chats-list [data-uid="' + uid + '"]'); diff --git a/public/src/client/footer.js b/public/src/client/footer.js index 5e39ac142c..288123c8e1 100644 --- a/public/src/client/footer.js +++ b/public/src/client/footer.js @@ -7,21 +7,13 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu Chat.prepareDOM(); translator.prepareDOM(); - function updateUnreadTopicCount(err, count) { - if (err) { - return console.warn('Error updating unread count', err); - } - + function updateUnreadTopicCount(count) { $('#unread-count i') .toggleClass('unread-count', count > 0) .attr('data-content', count > 20 ? '20+' : count); } - function updateUnreadChatCount(err, count) { - if (err) { - return console.warn('Error updating unread count', err); - } - + function updateUnreadChatCount(count) { components.get('chat/icon') .toggleClass('unread-count', count > 0) .attr('data-content', count > 20 ? '20+' : count); @@ -62,11 +54,20 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu socket.on('event:new_post', onNewPost); } - socket.on('event:unread.updateCount', updateUnreadTopicCount); - socket.emit('user.getUnreadCount', updateUnreadTopicCount); + if (app.user.uid) { + socket.emit('user.getUnreadCounts', function(err, data) { + if (err) { + return app.alert(err.message); + } + updateUnreadTopicCount(data.unreadTopicCount); + updateUnreadChatCount(data.unreadChatCount); + Notifications.updateNotifCount(data.unreadNotificationCount); + }); + } + + socket.on('event:unread.updateCount', updateUnreadTopicCount); socket.on('event:unread.updateChatCount', updateUnreadChatCount); - socket.emit('user.getUnreadChatCount', updateUnreadChatCount); initUnreadTopics(); }); diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index f5fb69e401..be624b2160 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -1,17 +1,22 @@ "use strict"; -/* globals define, socket, ajaxify, app, bootbox, RELATIVE_PATH, utils */ +/* globals define, socket, ajaxify, app, bootbox, utils */ + +define('forum/groups/details', [ + 'forum/groups/memberlist', + 'iconSelect', + 'components', + 'vendor/colorpicker/colorpicker', + 'vendor/jquery/draggable-background/backgroundDraggable' +], function(memberList, iconSelect, components) { -define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescroll', 'vendor/colorpicker/colorpicker', 'vendor/jquery/draggable-background/backgroundDraggable'], function(iconSelect, components, infinitescroll) { var Details = { cover: {} }; - var searchInterval; var groupName; Details.init = function() { - var detailsPage = components.get('groups/container'), - settingsFormEl = detailsPage.find('form'); + var detailsPage = components.get('groups/container'); groupName = ajaxify.data.group.name; @@ -20,8 +25,8 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol Details.initialiseCover(); } - handleMemberSearch(); - handleMemberInfiniteScroll(); + memberList.init(); + handleMemberInvitations(); components.get('groups/activity').find('.content img:not(.not-responsive)').addClass('img-responsive'); @@ -291,44 +296,6 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol }); }; - function handleMemberSearch() { - $('[component="groups/members/search"]').on('keyup', function() { - var query = $(this).val(); - if (searchInterval) { - clearInterval(searchInterval); - searchInterval = 0; - } - - searchInterval = setTimeout(function() { - socket.emit('groups.searchMembers', {groupName: groupName, query: query}, function(err, results) { - if (err) { - return app.alertError(err.message); - } - - infinitescroll.parseAndTranslate('groups/details', 'members', { - group: { - members: results.users, - isOwner: ajaxify.data.group.isOwner - } - }, function(html) { - $('[component="groups/members"] tbody').html(html); - $('[component="groups/members"]').attr('data-nextstart', 20); - }); - }); - }, 250); - }); - } - - function handleMemberInfiniteScroll() { - $('[component="groups/members"] tbody').on('scroll', function() { - var $this = $(this); - var bottom = ($this[0].scrollHeight - $this.height()) * 0.9; - if ($this.scrollTop() > bottom) { - loadMoreMembers(); - } - }); - } - function handleMemberInvitations() { if (ajaxify.data.group.isOwner) { var searchInput = $('[component="groups/members/invite"]'); @@ -349,48 +316,5 @@ define('forum/groups/details', ['iconSelect', 'components', 'forum/infinitescrol } } - function loadMoreMembers() { - - var members = $('[component="groups/members"]'); - if (members.attr('loading')) { - return; - } - - members.attr('loading', 1); - socket.emit('groups.loadMoreMembers', { - groupName: groupName, - after: members.attr('data-nextstart') - }, function(err, data) { - if (err) { - return app.alertError(err.message); - } - - if (data && data.users.length) { - onMembersLoaded(data.users, function() { - members.removeAttr('loading'); - members.attr('data-nextstart', data.nextStart); - }); - } else { - members.removeAttr('loading'); - } - }); - } - - function onMembersLoaded(users, callback) { - users = users.filter(function(user) { - return !$('[component="groups/members"] [data-uid="' + user.uid + '"]').length; - }); - - infinitescroll.parseAndTranslate('groups/details', 'members', { - group: { - members: users, - isOwner: ajaxify.data.group.isOwner - } - }, function(html) { - $('[component="groups/members"] tbody').append(html); - callback(); - }); - } - return Details; }); \ No newline at end of file diff --git a/public/src/client/groups/memberlist.js b/public/src/client/groups/memberlist.js new file mode 100644 index 0000000000..0cbc0e9116 --- /dev/null +++ b/public/src/client/groups/memberlist.js @@ -0,0 +1,97 @@ +"use strict"; +/* globals define, socket, ajaxify, app */ + +define('forum/groups/memberlist', ['components', 'forum/infinitescroll'], function(components, infinitescroll) { + + var MemberList = {}; + var searchInterval; + var groupName; + + MemberList.init = function() { + groupName = ajaxify.data.group.name; + + handleMemberSearch(); + handleMemberInfiniteScroll(); + }; + + function handleMemberSearch() { + $('[component="groups/members/search"]').on('keyup', function() { + var query = $(this).val(); + if (searchInterval) { + clearInterval(searchInterval); + searchInterval = 0; + } + + searchInterval = setTimeout(function() { + socket.emit('groups.searchMembers', {groupName: groupName, query: query}, function(err, results) { + if (err) { + return app.alertError(err.message); + } + parseAndTranslate(results.users, function(html) { + $('[component="groups/members"] tbody').html(html); + $('[component="groups/members"]').attr('data-nextstart', 20); + }); + }); + }, 250); + }); + } + + function handleMemberInfiniteScroll() { + $('[component="groups/members"] tbody').on('scroll', function() { + var $this = $(this); + var bottom = ($this[0].scrollHeight - $this.innerHeight()) * 0.9; + + if ($this.scrollTop() > bottom) { + loadMoreMembers(); + } + }); + } + + function loadMoreMembers() { + var members = $('[component="groups/members"]'); + if (members.attr('loading')) { + return; + } + + members.attr('loading', 1); + socket.emit('groups.loadMoreMembers', { + groupName: groupName, + after: members.attr('data-nextstart') + }, function(err, data) { + if (err) { + return app.alertError(err.message); + } + + if (data && data.users.length) { + onMembersLoaded(data.users, function() { + members.removeAttr('loading'); + members.attr('data-nextstart', data.nextStart); + }); + } else { + members.removeAttr('loading'); + } + }); + } + + function onMembersLoaded(users, callback) { + users = users.filter(function(user) { + return !$('[component="groups/members"] [data-uid="' + user.uid + '"]').length; + }); + + parseAndTranslate(users, function(html) { + $('[component="groups/members"] tbody').append(html); + callback(); + }); + } + + function parseAndTranslate(users, callback) { + infinitescroll.parseAndTranslate('groups/details', 'members', { + group: { + members: users, + isOwner: ajaxify.data.group.isOwner + } + }, callback); + } + + return MemberList; +}); \ No newline at end of file diff --git a/public/src/client/notifications.js b/public/src/client/notifications.js index 4f2dfbf197..7c0dcd48c3 100644 --- a/public/src/client/notifications.js +++ b/public/src/client/notifications.js @@ -2,12 +2,12 @@ /* globals define, socket, app */ -define('forum/notifications', ['components', 'notifications'], function(components, notifs) { +define('forum/notifications', ['components', 'notifications', 'forum/infinitescroll'], function(components, notifs, infinitescroll) { var Notifications = {}; Notifications.init = function() { var listEl = $('.notifications-list'); - listEl.on('click', '[component="notifications/item/link"]', function(e) { + listEl.on('click', '[component="notifications/item/link"]', function() { var nid = $(this).parents('[data-nid]').attr('data-nid'); socket.emit('notifications.markRead', nid, function(err) { if (err) { @@ -28,7 +28,32 @@ define('forum/notifications', ['components', 'notifications'], function(componen notifs.updateNotifCount(0); }); }); + + infinitescroll.init(loadMoreNotifications); }; + function loadMoreNotifications(direction) { + if (direction < 0) { + return; + } + var notifList = $('.notifications-list'); + infinitescroll.loadMore('notifications.loadMore', { + after: notifList.attr('data-nextstart') + }, function(data, done) { + if (!data) { + return done(); + } + notifList.attr('data-nextstart', data.nextStart); + if (!data.notifications || !data.notifications.length) { + return done(); + } + infinitescroll.parseAndTranslate('notifications', 'notifications', {notifications: data.notifications}, function(html) { + notifList.append(html); + html.find('.timeago').timeago(); + done(); + }); + }); + } + return Notifications; }); diff --git a/public/src/client/pagination.js b/public/src/client/pagination.js index a94967c7c5..534b4e27f2 100644 --- a/public/src/client/pagination.js +++ b/public/src/client/pagination.js @@ -4,14 +4,8 @@ define('forum/pagination', function() { var pagination = {}; - pagination.currentPage = 0; - pagination.pageCount = 0; - - pagination.init = function(currentPage, pageCount) { - pagination.currentPage = parseInt(currentPage, 10); - pagination.pageCount = parseInt(pageCount, 10); - - $('.pagination').on('click', '.select-page', function(e) { + pagination.init = function() { + $('body').on('click', '.pagination .select-page', function(e) { e.preventDefault(); bootbox.prompt('Enter page number:', function(pageNum) { pagination.loadPage(pageNum); @@ -22,10 +16,14 @@ define('forum/pagination', function() { pagination.loadPage = function(page, callback) { callback = callback || function() {}; page = parseInt(page, 10); - if (!utils.isNumber(page) || page < 1 || page > pagination.pageCount) { + if (!utils.isNumber(page) || page < 1 || page > ajaxify.data.pagination.pageCount) { return; } - var url = window.location.pathname.slice(1).split('/').slice(0, 3).join('/') + '?page=' + page; + + var query = utils.params(); + query.page = page; + + var url = window.location.pathname + '?' + $.param(query); ajaxify.go(url, callback); }; diff --git a/public/src/client/tags.js b/public/src/client/tags.js index 4763d3aa44..dedeb4a746 100644 --- a/public/src/client/tags.js +++ b/public/src/client/tags.js @@ -24,7 +24,7 @@ define('forum/tags', ['forum/infinitescroll'], function(infinitescroll) { if (err) { return app.alertError(err.message); } - onTagsLoaded(results, true, function() { + onTagsLoaded(results.tags, true, function() { timeoutId = 0; }); }); diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 89b6e30a1d..faaaddbf00 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -4,7 +4,6 @@ /* globals define, app, socket, config, ajaxify, RELATIVE_PATH, utils */ define('forum/topic', [ - 'forum/pagination', 'forum/infinitescroll', 'forum/topic/threadTools', 'forum/topic/postTools', @@ -14,13 +13,13 @@ define('forum/topic', [ 'navigator', 'sort', 'components' -], function(pagination, infinitescroll, threadTools, postTools, events, browsing, posts, navigator, sort, components) { +], function(infinitescroll, threadTools, postTools, events, browsing, posts, navigator, sort, components) { var Topic = {}, currentUrl = ''; $(window).on('action:ajaxify.start', function(ev, data) { if (ajaxify.currentPage !== data.url) { - navigator.hide(); + navigator.disable(); components.get('navbar/title').find('span').text('').hide(); app.removeAlert('bookmark'); @@ -147,7 +146,7 @@ define('forum/topic', [ if (components.get('post/anchor', postIndex).length) { return navigator.scrollToPostIndex(postIndex, true); } - } else if (bookmark && (!config.usePagination || (config.usePagination && pagination.currentPage === 1)) && ajaxify.data.postcount > 10) { + } else if (bookmark && (!config.usePagination || (config.usePagination && ajaxify.data.pagination.currentPage === 1)) && ajaxify.data.postcount > 5) { app.alert({ alert_id: 'bookmark', message: '[[topic:bookmark_instructions]]', @@ -217,13 +216,10 @@ define('forum/topic', [ if (!config.usePagination) { infinitescroll.init($('[component="topic"]'), posts.loadMorePosts); } else { - navigator.hide(); - - pagination.init(parseInt(ajaxify.data.currentPage, 10), parseInt(ajaxify.data.pageCount, 10)); + navigator.disable(); } } - function updateTopicTitle() { if ($(window).scrollTop() > 50) { components.get('navbar/title').find('span').text(ajaxify.data.title).show(); @@ -281,7 +277,7 @@ define('forum/topic', [ var bookmarkKey = 'topic:' + ajaxify.data.tid + ':bookmark'; var currentBookmark = ajaxify.data.bookmark || localStorage.getItem(bookmarkKey); - if (!currentBookmark || parseInt(index, 10) > parseInt(currentBookmark, 10)) { + if (ajaxify.data.postcount > 5 && (!currentBookmark || parseInt(index, 10) > parseInt(currentBookmark, 10))) { if (app.user.uid) { socket.emit('topics.bookmark', { 'tid': ajaxify.data.tid, diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index fe9196e7fb..47c7f2c328 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -162,6 +162,7 @@ define('forum/topic/events', [ function onPostPurged(pid) { components.get('post', 'pid', pid).fadeOut(500, function() { $(this).remove(); + posts.showBottomPostBar(); }); postTools.updatePostCount(); diff --git a/public/src/client/topic/flag.js b/public/src/client/topic/flag.js new file mode 100644 index 0000000000..3101c0cd1c --- /dev/null +++ b/public/src/client/topic/flag.js @@ -0,0 +1,64 @@ +'use strict'; + +/* globals define, app, socket, templates, translator */ + +define('forum/topic/flag', [], function() { + + var Flag = {}, + flagModal, + flagCommit; + + Flag.showFlagModal = function(pid) { + parseModal(function(html) { + flagModal = $(html); + + flagModal.on('hidden.bs.modal', function() { + flagModal.remove(); + }); + + flagCommit = flagModal.find('#flag-post-commit'); + + flagModal.on('click', '.flag-reason', function() { + flagPost(pid, $(this).text()); + }); + + flagCommit.on('click', function() { + flagPost(pid, flagModal.find('#flag-reason-custom').val()); + }); + + flagModal.modal('show'); + + flagModal.find('#flag-reason-custom').on('keyup blur change', checkFlagButtonEnable); + }); + }; + + function parseModal(callback) { + templates.parse('partials/modals/flag_post_modal', {}, function(html) { + translator.translate(html, callback); + }); + } + + function flagPost(pid, reason) { + if (!pid || !reason) { + return; + } + socket.emit('posts.flag', {pid: pid, reason: reason}, function(err) { + if (err) { + return app.alertError(err.message); + } + + flagModal.modal('hide'); + app.alertSuccess('[[topic:flag_success]]'); + }); + } + + function checkFlagButtonEnable() { + if (flagModal.find('#flag-reason-custom').val()) { + flagCommit.removeAttr('disabled'); + } else { + flagCommit.attr('disabled', true); + } + } + + return Flag; +}); diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 002849f167..764ec7cd0a 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -1,6 +1,6 @@ 'use strict'; -/* globals define, app, ajaxify, bootbox, socket, templates, utils */ +/* globals define, app, ajaxify, bootbox, socket, templates, utils, config */ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator'], function(share, navigator, components, translator) { @@ -15,6 +15,8 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator share.addShareHandlers(topicName); addVoteHandler(); + + PostTools.updatePostCount(ajaxify.data.postcount); }; PostTools.toggle = function(pid, isDeleted) { @@ -28,20 +30,16 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator postEl.find('[component="post/purge"]').toggleClass('hidden', !isDeleted); }; - PostTools.updatePostCount = function() { - socket.emit('topics.postcount', ajaxify.data.tid, function(err, postCount) { - if (!err) { - var postCountEl = components.get('topic/post-count'); - postCountEl.html(postCount).attr('title', postCount); - utils.makeNumbersHumanReadable(postCountEl); - navigator.setCount(postCount); - } - }); + PostTools.updatePostCount = function(postCount) { + var postCountEl = components.get('topic/post-count'); + postCountEl.html(postCount).attr('title', postCount); + utils.makeNumbersHumanReadable(postCountEl); + navigator.setCount(postCount); }; function addVoteHandler() { - components.get('topic').on('mouseenter', '[data-pid] .votes', function() { - loadDataAndCreateTooltip($(this)); + components.get('topic').on('mouseenter', '[data-pid] [component="post/vote-count"]', function() { + loadDataAndCreateTooltip($(this).parent()); }); } @@ -55,6 +53,13 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function createTooltip(el, data) { + function doCreateTooltip(title) { + el.attr('title', title).tooltip('fixTitle').tooltip('show'); + el.on('hidden.bs.tooltip', function() { + el.tooltip('destroy'); + el.off('hidden.bs.tooltip'); + }); + } var usernames = data.usernames; if (!usernames.length) { return; @@ -63,11 +68,11 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator usernames = usernames.join(', ').replace(/,/g, '|'); translator.translate('[[topic:users_and_others, ' + usernames + ', ' + data.otherCount + ']]', function(translated) { translated = translated.replace(/\|/g, ','); - el.attr('title', translated).tooltip('destroy').tooltip('show'); + doCreateTooltip(translated); }); } else { usernames = usernames.join(', '); - el.attr('title', usernames).tooltip('destroy').tooltip('show'); + doCreateTooltip(usernames); } } @@ -103,40 +108,42 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator }); postContainer.on('click', '[component="post/flag"]', function() { - flagPost(getData($(this), 'data-pid')); + var pid = getData($(this), 'data-pid'); + require(['forum/topic/flag'], function(flag) { + flag.showFlagModal(pid); + }); }); - postContainer.on('click', '[component="post/edit"]', function(e) { + postContainer.on('click', '[component="post/edit"]', function() { var btn = $(this); $(window).trigger('action:composer.post.edit', { pid: getData(btn, 'data-pid') }); }); - postContainer.on('click', '[component="post/delete"]', function(e) { + postContainer.on('click', '[component="post/delete"]', function() { togglePostDelete($(this), tid); }); - postContainer.on('click', '[component="post/restore"]', function(e) { + postContainer.on('click', '[component="post/restore"]', function() { togglePostDelete($(this), tid); }); - postContainer.on('click', '[component="post/purge"]', function(e) { + postContainer.on('click', '[component="post/purge"]', function() { purgePost($(this), tid); }); - postContainer.on('click', '[component="post/move"]', function(e) { + postContainer.on('click', '[component="post/move"]', function() { openMovePostModal($(this)); }); - postContainer.on('click', '[component="post/chat"]', function(e) { + postContainer.on('click', '[component="post/chat"]', function() { openChat($(this)); }); } function onReplyClicked(button, tid, topicName) { showStaleWarning(function(proceed) { - console.log('proceed is', proceed); if (!proceed) { var selectionText = '', selection = window.getSelection ? window.getSelection() : document.selection.createRange(); @@ -363,22 +370,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator }); } - function flagPost(pid) { - translator.translate('[[topic:flag_confirm]]', function(message) { - bootbox.confirm(message, function(confirm) { - if (!confirm) { - return; - } - socket.emit('posts.flag', pid, function(err) { - if (err) { - return app.alertError(err.message); - } - app.alertSuccess('[[topic:flag_success]]'); - }); - }); - }); - } function openChat(button) { var post = button.parents('[data-pid]'); diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index fd5f310b59..784a11c4a4 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -28,6 +28,8 @@ define('forum/topic/posts', [ }); updatePostCounts(data.posts); + ajaxify.data.postcount ++; + postTools.updatePostCount(ajaxify.data.postcount); if (config.usePagination) { onNewPostPagination(data); @@ -51,15 +53,15 @@ define('forum/topic/posts', [ var posts = data.posts; - pagination.pageCount = Math.max(1, Math.ceil((posts[0].topic.postcount - 1) / config.postsPerPage)); + ajaxify.data.pagination.pageCount = Math.max(1, Math.ceil((posts[0].topic.postcount - 1) / config.postsPerPage)); var direction = config.topicPostSort === 'oldest_to_newest' || config.topicPostSort === 'most_votes' ? 1 : -1; - var isPostVisible = (pagination.currentPage === pagination.pageCount && direction === 1) || (pagination.currentPage === 1 && direction === -1); + var isPostVisible = (ajaxify.data.pagination.currentPage === ajaxify.data.pagination.pageCount && direction === 1) || (ajaxify.data.pagination.currentPage === 1 && direction === -1); if (isPostVisible) { createNewPosts(data, components.get('post').not('[data-index=0]'), direction, scrollToPost); } else if (parseInt(posts[0].uid, 10) === parseInt(app.user.uid, 10)) { - pagination.loadPage(pagination.pageCount, scrollToPost); + pagination.loadPage(ajaxify.data.pagination.pageCount, scrollToPost); } } @@ -220,17 +222,15 @@ define('forum/topic/posts', [ $this.wrap(''); } }); - postTools.updatePostCount(); - addBlockquoteEllipses(posts.find('[component="post/content"] > blockquote')); + + addBlockquoteEllipses(posts.find('[component="post/content"] > blockquote > blockquote')); hidePostToolsForDeletedPosts(posts); - showBottomPostBar(); + Posts.showBottomPostBar(); }; - function showBottomPostBar() { - if (components.get('post').length > 1 || !components.get('post', 'index', 0).length) { - $('.bottom-post-bar').removeClass('hidden'); - } - } + Posts.showBottomPostBar = function() { + $('.bottom-post-bar').toggleClass('hidden', components.get('post').length <= 1 && !!components.get('post', 'index', 0).length); + }; function hidePostToolsForDeletedPosts(posts) { posts.each(function() { diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index 9bf07d72c5..3f178c3b85 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -10,8 +10,11 @@ var helpers = {}; helpers.displayMenuItem = function(data, index) { - var item = data.navigation[index], - properties = item.properties; + var item = data.navigation[index]; + if (!item) { + return false; + } + var properties = item.properties; if (properties) { if ((properties.loggedIn && !data.config.loggedIn) || @@ -37,7 +40,7 @@ property = tag.property ? 'property="' + tag.property + '" ' : '', content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : ''; - return ''; + return '\n\t'; }; helpers.buildLinkTag = function(tag) { @@ -47,7 +50,7 @@ href = tag.href ? 'href="' + tag.href + '" ' : '', sizes = tag.sizes ? 'sizes="' + tag.sizes + '" ' : ''; - return ''; + return '\n\t'; }; helpers.stringify = function(obj) { diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js index 251cc78182..d635eae181 100644 --- a/public/src/modules/navigator.js +++ b/public/src/modules/navigator.js @@ -18,7 +18,7 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com toTop = toTop || function() {}; toBottom = toBottom || function() {}; - $(window).on('scroll', navigator.update); + $(window).off('scroll', navigator.update).on('scroll', navigator.update); $('.pagination-block .dropdown-menu').off('click').on('click', function(e) { e.stopPropagation(); @@ -74,7 +74,12 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com toggle(true); }; - navigator.hide = function() { + navigator.disable = function() { + count = 0; + index = 1; + navigator.selector = navigator.callback = null; + $(window).off('scroll', navigator.update); + toggle(false); }; @@ -92,13 +97,19 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com var middleOfViewport = $(window).scrollTop() + $(window).height() / 2; - index = parseInt($(navigator.selector).first().attr('data-index'), 10); - + index = parseInt($(navigator.selector).first().attr('data-index'), 10) + 1; + var previousDistance = Number.MAX_VALUE; $(navigator.selector).each(function() { - index++; - if ($(this).offset().top > middleOfViewport) { + var distanceToMiddle = Math.abs(middleOfViewport - $(this).offset().top); + + if (distanceToMiddle > previousDistance) { return false; } + + if (distanceToMiddle < previousDistance) { + index = parseInt($(this).attr('data-index'), 10) + 1; + previousDistance = distanceToMiddle; + } }); if (typeof navigator.callback === 'function') { @@ -162,7 +173,7 @@ define('navigator', ['forum/pagination', 'components'], function(pagination, com if (config.usePagination) { var page = Math.max(1, Math.ceil(postIndex / config.postsPerPage)); - if (parseInt(page, 10) !== pagination.currentPage) { + if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) { pagination.loadPage(page, function() { navigator.scrollToPostIndex(postIndex, highlight, duration); }); diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index 03f5e7b2d5..cb6a4ef04e 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -64,14 +64,6 @@ define('notifications', ['sounds', 'translator', 'components'], function(sound, Notifications.updateNotifCount(count); } - socket.emit('notifications.getCount', function(err, count) { - if (!err) { - Notifications.updateNotifCount(count); - } else { - Notifications.updateNotifCount(0); - } - }); - socket.on('event:new_notification', function(notifData) { app.alert({ alert_id: 'new_notif', diff --git a/public/src/modules/sort.js b/public/src/modules/sort.js index a6fe38f90b..960e21facb 100644 --- a/public/src/modules/sort.js +++ b/public/src/modules/sort.js @@ -10,7 +10,7 @@ define('sort', ['components'], function(components) { var currentSetting = threadSort.find('a[data-sort="' + config[field] + '"]'); currentSetting.find('i').addClass('fa-check'); - components.get('topic').on('click', '[component="thread/sort"] a', function() { + $('.category, .topic').on('click', '[component="thread/sort"] a', function() { var newSetting = $(this).attr('data-sort'); socket.emit(method, newSetting, function(err) { if (err) { diff --git a/public/src/modules/sounds.js b/public/src/modules/sounds.js index 3314500ce5..4a6bb80f51 100644 --- a/public/src/modules/sounds.js +++ b/public/src/modules/sounds.js @@ -5,31 +5,30 @@ define('sounds', ['buzz'], function(buzz) { var Sounds = {}; var loadedSounds = {}; - var eventSoundMapping = {}; - var files = {}; - - loadFiles(); - - loadMapping(); + var eventSoundMapping; + var files; socket.on('event:sounds.reloadMapping', loadMapping); - function loadFiles() { - socket.emit('modules.sounds.getSounds', function(err, sounds) { - if (err) { - return app.alertError('[sounds] Could not initialise!'); - } - - files = sounds; - }); - } - - function loadMapping() { + function loadMapping(callback) { + callback = callback || function() {}; socket.emit('modules.sounds.getMapping', function(err, mapping) { if (err) { return app.alertError('[sounds] Could not load sound mapping!'); } eventSoundMapping = mapping; + callback(); + }); + } + + function loadData(callback) { + socket.emit('modules.sounds.getData', function(err, data) { + if (err) { + return app.alertError('[sounds] Could not load sound mapping!'); + } + eventSoundMapping = data.mapping; + files = data.files; + callback(); }); } @@ -38,22 +37,37 @@ define('sounds', ['buzz'], function(buzz) { } function loadFile(fileName, callback) { + function createSound() { + if (files && files[fileName]) { + loadedSounds[fileName] = new buzz.sound(files[fileName]); + } + callback(); + } + if (isSoundLoaded(fileName)) { return callback(); } - if (files && files[fileName]) { - loadedSounds[fileName] = new buzz.sound(files[fileName]); + if (!files || !files[fileName]) { + return loadData(createSound); } - callback(); + createSound(); } Sounds.play = function(name) { + function play() { + Sounds.playFile(eventSoundMapping[name]); + } + if (!config.notificationSounds) { return; } - Sounds.playFile(eventSoundMapping[name]); + if (!eventSoundMapping) { + return loadData(play); + } + + play(); }; Sounds.playFile = function(fileName) { diff --git a/public/src/utils.js b/public/src/utils.js index 930e03501f..a2f8ea7a80 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -381,6 +381,12 @@ }; } + if (typeof String.prototype.rtrim != 'function') { + String.prototype.rtrim = function() { + return this.replace(/\s+$/g, ''); + }; + } + if ('undefined' !== typeof window) { window.utils = module.exports; } diff --git a/public/vendor/jquery/timeago/locales/jquery.timeago.it-short.js b/public/vendor/jquery/timeago/locales/jquery.timeago.it-short.js new file mode 100644 index 0000000000..f4d92ad209 --- /dev/null +++ b/public/vendor/jquery/timeago/locales/jquery.timeago.it-short.js @@ -0,0 +1,20 @@ +// Italian shortened +jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "", + suffixFromNow: "", + seconds: "1m", + minute: "1m", + minutes: "%dm", + hour: "1h", + hours: "%dh", + day: "1g", + days: "%dg", + month: "1me", + months: "%dme", + year: "1a", + years: "%da", + wordSeparator: " ", + numbers: [] +}; \ No newline at end of file diff --git a/src/categories/data.js b/src/categories/data.js index c844e632ad..cccacc8a68 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -53,17 +53,11 @@ module.exports = function(Categories) { } if (category.description) { - plugins.fireHook('filter:parse.raw', category.description, function(err, parsedDescription) { - if (err) { - return callback(err); - } - category.descriptionParsed = parsedDescription; - category.description = validator.escape(category.description); - callback(null, category); - }); - } else { - callback(null, category); + category.description = validator.escape(category.description); + category.descriptionParsed = category.descriptionParsed || category.description; } + + callback(null, category); } Categories.getCategoryField = function(cid, field, callback) { diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js index d7a2a165a0..84cb005de0 100644 --- a/src/categories/recentreplies.js +++ b/src/categories/recentreplies.js @@ -3,14 +3,13 @@ var async = require('async'), winston = require('winston'), + validator = require('validator'), _ = require('underscore'), - meta = require('../meta'), db = require('../database'), posts = require('../posts'), topics = require('../topics'), - privileges = require('../privileges'), - plugins = require('../plugins'); + privileges = require('../privileges'); module.exports = function(Categories) { Categories.getRecentReplies = function(cid, uid, count, callback) { @@ -38,25 +37,21 @@ module.exports = function(Categories) { async.waterfall([ function(next) { - async.map(categoryData, getRecentTopicPids, next); + async.map(categoryData, getRecentTopicTids, next); }, function(results, next) { - var pids = _.flatten(results); + var tids = _.flatten(results); - pids = pids.filter(function(pid, index, array) { - return !!pid && array.indexOf(pid) === index; + tids = tids.filter(function(tid, index, array) { + return !!tid && array.indexOf(tid) === index; }); - privileges.posts.filter('read', pids, uid, next); + privileges.topics.filterTids('read', tids, uid, next); }, - function(pids, next) { - if (meta.config.teaserPost === 'first') { - getMainPosts(pids, uid, next); - } else { - posts.getPostSummaryByPids(pids, uid, {stripTags: true}, next); - } + function(tids, next) { + getTopics(tids, next); }, - function(posts, next) { - assignPostsToCategories(categoryData, posts); + function(topics, next) { + assignTopicsToCategories(categoryData, topics); bubbleUpChildrenPosts(categoryData); @@ -65,29 +60,86 @@ module.exports = function(Categories) { ], callback); }; - function getMainPosts(pids, uid, callback) { + function getRecentTopicTids(category, callback) { + var count = parseInt(category.numRecentReplies, 10); + if (!count) { + return callback(null, []); + } + + if (count === 1) { + async.waterfall([ + function (next) { + db.getSortedSetRevRange('cid:' + category.cid + ':pids', 0, 0, next); + }, + function (pid, next) { + posts.getPostField(pid, 'tid', next); + }, + function (tid, next) { + next(null, [tid]); + } + ], callback); + return; + } + + async.parallel({ + pinnedTids: function(next) { + db.getSortedSetRevRangeByScore('cid:' + category.cid + ':tids', 0, -1, '+inf', Date.now(), next); + }, + tids: function(next) { + db.getSortedSetRevRangeByScore('cid:' + category.cid + ':tids', 0, Math.max(1, count), Date.now(), 0, next); + } + }, function(err, results) { + if (err) { + return callback(err); + } + + results.tids = results.tids.concat(results.pinnedTids); + + callback(null, results.tids); + }); + } + + function getTopics(tids, callback) { + var topicData; async.waterfall([ - function(next) { - var keys = pids.map(function(pid) { - return 'post:' + pid; - }); - db.getObjectsFields(keys, ['tid'], next); + function (next) { + topics.getTopicsFields(tids, ['tid', 'mainPid', 'slug', 'title', 'teaserPid', 'cid', 'postcount'], next); }, - function(posts, next) { - var keys = posts.map(function(post) { - return 'topic:' + post.tid; + function (_topicData, next) { + topicData = _topicData; + topicData.forEach(function(topic) { + topic.teaserPid = topic.teaserPid || topic.mainPid; }); - db.getObjectsFields(keys, ['mainPid'], next); + + topics.getTeasers(topicData, next); }, - function(topics, next) { - var mainPids = topics.map(function(topic) { - return topic.mainPid; + function (teasers, next) { + teasers.forEach(function(teaser, index) { + if (teaser) { + teaser.cid = topicData[index].cid; + teaser.tid = teaser.uid = teaser.user.uid = undefined; + teaser.topic = { + slug: topicData[index].slug, + title: validator.escape(topicData[index].title) + }; + } }); - posts.getPostSummaryByPids(mainPids, uid, {stripTags: true}, next); + teasers = teasers.filter(Boolean); + next(null, teasers); } ], callback); } + function assignTopicsToCategories(categories, topics) { + categories.forEach(function(category) { + category.posts = topics.filter(function(topic) { + return topic.cid && parseInt(topic.cid, 10) === parseInt(category.cid, 10); + }).sort(function(a, b) { + return b.pid - a.pid; + }).slice(0, parseInt(category.numRecentReplies, 10)); + }); + } + function bubbleUpChildrenPosts(categoryData) { categoryData.forEach(function(category) { if (category.posts.length) { @@ -97,7 +149,7 @@ module.exports = function(Categories) { getPostsRecursive(category, posts); posts.sort(function(a, b) { - return b.timestamp - a.timestamp; + return b.pid - a.pid; }); if (posts.length) { category.posts = [posts[0]]; @@ -115,64 +167,6 @@ module.exports = function(Categories) { }); } - function assignPostsToCategories(categories, posts) { - categories.forEach(function(category) { - category.posts = posts.filter(function(post) { - return post.category && (parseInt(post.category.cid, 10) === parseInt(category.cid, 10) || - parseInt(post.category.parentCid, 10) === parseInt(category.cid, 10)); - }).sort(function(a, b) { - return b.timestamp - a.timestamp; - }).slice(0, parseInt(category.numRecentReplies, 10)); - }); - } - - function getRecentTopicPids(category, callback) { - var count = parseInt(category.numRecentReplies, 10); - if (!count) { - return callback(null, []); - } - - db.getSortedSetRevRange('cid:' + category.cid + ':pids', 0, 0, function(err, pids) { - if (err || !Array.isArray(pids) || !pids.length) { - return callback(err, []); - } - - if (count === 1) { - return callback(null, pids); - } - - async.parallel({ - pinnedTids: function(next) { - db.getSortedSetRevRangeByScore('cid:' + category.cid + ':tids', 0, -1, '+inf', Date.now(), next); - }, - tids: function(next) { - db.getSortedSetRevRangeByScore('cid:' + category.cid + ':tids', 0, Math.max(0, count), Date.now(), 0, next); - } - }, function(err, results) { - if (err) { - return callback(err); - } - - results.tids = results.tids.concat(results.pinnedTids); - - async.map(results.tids, topics.getLatestUndeletedPid, function(err, topicPids) { - if (err) { - return callback(err); - } - - pids = pids.concat(topicPids).filter(function(pid, index, array) { - return !!pid && array.indexOf(pid) === index; - }).sort(function(a, b) { - return b - a; - }).slice(0, count); - - callback(null, pids); - }); - }); - }); - } - - Categories.moveRecentReplies = function(tid, oldCid, cid) { updatePostCount(tid, oldCid, cid); topics.getPids(tid, function(err, pids) { diff --git a/src/categories/update.js b/src/categories/update.js index 4ebfda2f11..ffd24e7d98 100644 --- a/src/categories/update.js +++ b/src/categories/update.js @@ -68,6 +68,8 @@ module.exports = function(Categories) { if (key === 'order') { updateOrder(cid, value, callback); + } else if (key === 'description') { + parseDescription(cid, value, callback); } else { callback(); } @@ -119,4 +121,13 @@ module.exports = function(Categories) { }); } + function parseDescription(cid, description, callback) { + plugins.fireHook('filter:parse.raw', description, function(err, parsedDescription) { + if (err) { + return callback(err); + } + Categories.setCategoryField(cid, 'descriptionParsed', parsedDescription, callback); + }); + } + }; diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index 11f6535d8c..1f95db984b 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -19,7 +19,7 @@ chatsController.get = function(req, res, callback) { // In case a userNAME is passed in instead of a slug, the route should not 404 var slugified = utils.slugify(req.params.userslug); if (req.params.userslug && req.params.userslug !== slugified) { - return res.redirect(nconf.get('relative_path') + '/chats/' + slugified); + return helpers.redirect(res, '/chats/' + slugified); } async.parallel({ diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index 1120ad65e3..a84dc7c991 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -14,6 +14,48 @@ var async = require('async'), var editController = {}; editController.get = function(req, res, callback) { + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, function(err, userData) { + if (err || !userData) { + return callback(err); + } + + userData.title = '[[pages:account/edit, ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:edit]]'}]); + + res.render('account/edit', userData); + }); +}; + +editController.password = function(req, res, next) { + renderRoute('password', req, res, next); +}; + +editController.username = function(req, res, next) { + renderRoute('username', req, res, next); +}; + +editController.email = function(req, res, next) { + renderRoute('email', req, res, next); +}; + +function renderRoute(name, req, res, next) { + getUserData(req, next, function(err, userData) { + if (err) { + return next(err); + } + + userData.title = '[[pages:account/edit/' + name + ', ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([ + {text: userData.username, url: '/user/' + userData.userslug}, + {text: '[[user:edit]]', url: '/user/' + userData.userslug + '/edit'}, + {text: '[[user:' + name + ']]'} + ]); + + res.render('account/edit/' + name, userData); + }); +} + +function getUserData(req, next, callback) { var userData; async.waterfall([ function(next) { @@ -22,7 +64,7 @@ editController.get = function(req, res, callback) { function(data, next) { userData = data; if (!userData) { - return callback(); + return next(); } db.getObjectField('user:' + userData.uid, 'password', next); } @@ -33,13 +75,9 @@ editController.get = function(req, res, callback) { userData['username:disableEdit'] = parseInt(meta.config['username:disableEdit'], 10) === 1; userData.hasPassword = !!password; - userData.title = '[[pages:account/edit, ' + userData.username + ']]'; - userData.breadcrumbs = helpers.buildBreadcrumbs([{text: userData.username, url: '/user/' + userData.userslug}, {text: '[[user:edit]]'}]); - - res.render('account/edit', userData); + callback(null, userData); }); -}; - +} editController.uploadPicture = function (req, res, next) { var userPhoto = req.files.files[0]; diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js index 2f540e9d94..aa60892f47 100644 --- a/src/controllers/accounts/notifications.js +++ b/src/controllers/accounts/notifications.js @@ -7,12 +7,13 @@ var user = require('../../user'), var notificationsController = {}; notificationsController.get = function(req, res, next) { - user.notifications.getAll(req.uid, 40, function(err, notifications) { + user.notifications.getAll(req.uid, 0, 39, function(err, notifications) { if (err) { return next(err); } res.render('notifications', { notifications: notifications, + nextStart: 40, title: '[[pages:notifications]]', breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:notifications]]'}]) }); diff --git a/src/controllers/admin.js b/src/controllers/admin.js index b620dce3d2..0580bd42a4 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -24,7 +24,8 @@ var adminController = { navigation: require('./admin/navigation'), themes: require('./admin/themes'), users: require('./admin/users'), - uploads: require('./admin/uploads') + uploads: require('./admin/uploads'), + info: require('./admin/info') }; diff --git a/src/controllers/admin/groups.js b/src/controllers/admin/groups.js index 259f93759d..dc8bf6ff82 100644 --- a/src/controllers/admin/groups.js +++ b/src/controllers/admin/groups.js @@ -59,12 +59,13 @@ groupsController.get = function(req, res, callback) { if (!exists) { return callback(); } - groups.get(groupName, {uid: req.uid}, next); + groups.get(groupName, {uid: req.uid, truncateUserList: true, userListCount: 20}, next); } ], function(err, group) { if (err) { return callback(err); } + group.isOwner = true; res.render('admin/manage/group', {group: group}); }); }; diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js new file mode 100644 index 0000000000..a9a489f6c4 --- /dev/null +++ b/src/controllers/admin/info.js @@ -0,0 +1,29 @@ +'use strict'; + +var os = require('os'); + +var infoController = {}; + +infoController.get = function(req, res, next) { + + var data = { + process: { + pid: process.pid, + title: process.title, + arch: process.arch, + platform: process.platform, + version: process.version, + versions: process.versions, + memoryUsage: process.memoryUsage(), + uptime: process.uptime() + }, + os: { + hostname: os.hostname() + } + }; + + res.render('admin/development/info', {info: JSON.stringify(data, null, 4)}); +}; + + +module.exports = infoController; \ No newline at end of file diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 16242fe18b..eb6a4a59f4 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -114,7 +114,7 @@ function validateUpload(req, res, next, uploadedFile, allowedTypes) { } }); - res.json({error: '[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]'}); + res.json({error: '[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]'}); return false; } diff --git a/src/controllers/api.js b/src/controllers/api.js index 56308ef535..421673e7c4 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -84,6 +84,7 @@ apiController.getConfig = function(req, res, next) { config.categoryTopicSort = meta.config.categoryTopicSort || 'newest_to_oldest'; config.csrf_token = req.csrfToken(); config.searchEnabled = plugins.hasListeners('filter:search.query'); + config.bootswatchSkin = 'default'; if (!req.user) { return filterConfig(); @@ -103,6 +104,7 @@ apiController.getConfig = function(req, res, next) { config.topicPostSort = settings.topicPostSort || config.topicPostSort; config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; config.topicSearchEnabled = settings.topicSearchEnabled || false; + config.bootswatchSkin = settings.bootswatchSkin || config.bootswatchSkin; filterConfig(); }); diff --git a/src/controllers/categories.js b/src/controllers/categories.js index 220779c524..b2d36b91e8 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -65,6 +65,15 @@ categoriesController.list = function(req, res, next) { return next(err); } + data.categories.forEach(function(category) { + if (category && Array.isArray(category.posts) && category.posts.length) { + category.teaser = { + url: nconf.get('relative_path') + '/topic/' + category.posts[0].topic.slug + '/' + category.posts[0].index, + timestampISO: category.posts[0].timestamp + }; + } + }); + data.title = '[[pages:categories]]'; if (req.path.startsWith('/api/categories') || req.path.startsWith('/categories')) { data.breadcrumbs = helpers.buildBreadcrumbs([{text: data.title}]); @@ -81,7 +90,7 @@ categoriesController.list = function(req, res, next) { categoriesController.get = function(req, res, callback) { var cid = req.params.category_id, - page = parseInt(req.query.page, 10) || 1, + currentPage = parseInt(req.query.page, 10) || 1, pageCount = 1, userPrivileges; @@ -127,7 +136,7 @@ categoriesController.get = function(req, res, callback) { return helpers.redirect(res, '/category/' + cid + '/' + req.params.slug + (topicIndex > topicCount ? '/' + topicCount : '')); } - if (settings.usePagination && (page < 1 || page > pageCount)) { + if (settings.usePagination && (currentPage < 1 || currentPage > pageCount)) { return callback(); } @@ -135,7 +144,7 @@ categoriesController.get = function(req, res, callback) { topicIndex = Math.max(topicIndex - (settings.topicsPerPage - 1), 0); } else if (!req.query.page) { var index = Math.max(parseInt((topicIndex || 0), 10), 0); - page = Math.ceil((index + 1) / settings.topicsPerPage); + currentPage = Math.ceil((index + 1) / settings.topicsPerPage); topicIndex = 0; } @@ -149,7 +158,7 @@ categoriesController.get = function(req, res, callback) { set = 'cid:' + cid + ':tids:posts'; } - var start = (page - 1) * settings.topicsPerPage + topicIndex, + var start = (currentPage - 1) * settings.topicsPerPage + topicIndex, stop = start + settings.topicsPerPage - 1; next(null, { @@ -194,8 +203,11 @@ categoriesController.get = function(req, res, callback) { }); }, function(categoryData, next) { + if (!categoryData.children.length) { + return next(null, categoryData); + } var allCategories = []; - categories.flattenCategories(allCategories, [categoryData]); + categories.flattenCategories(allCategories, categoryData.children); categories.getRecentTopicReplies(allCategories, req.uid, function(err) { next(err, categoryData); }); @@ -249,12 +261,10 @@ categoriesController.get = function(req, res, callback) { return callback(err); } - data.currentPage = page; - data.pageCount = pageCount; data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; data.rssFeedUrl = nconf.get('relative_path') + '/category/' + data.cid + '.rss'; data.title = data.name; - data.pagination = pagination.create(data.currentPage, data.pageCount); + data.pagination = pagination.create(currentPage, pageCount); data.pagination.rel.forEach(function(rel) { rel.href = nconf.get('url') + '/category/' + data.slug + rel.href; res.locals.linkTags.push(rel); diff --git a/src/controllers/search.js b/src/controllers/search.js index 329f599783..e32ab67c4b 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -2,7 +2,8 @@ 'use strict'; var async = require('async'), - validator = require('validator'), + + meta = require('../meta'), plugins = require('../plugins'), search = require('../search'), categories = require('../categories'), @@ -17,6 +18,10 @@ searchController.search = function(req, res, next) { return next(); } + if (!req.user && parseInt(meta.config.allowGuestSearching, 10) !== 1) { + return helpers.notAllowed(req, res); + } + var page = Math.max(1, parseInt(req.query.page, 10)) || 1; if (req.query.categories && !Array.isArray(req.query.categories)) { req.query.categories = [req.query.categories]; @@ -51,6 +56,7 @@ searchController.search = function(req, res, next) { searchData.pagination = pagination.create(page, searchData.pageCount, req.query); searchData.showAsPosts = !req.query.showAs || req.query.showAs === 'posts'; searchData.showAsTopics = req.query.showAs === 'topics'; + searchData.title = '[[global:header.search]]'; searchData.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:search]]'}]); searchData.expandSearch = !req.params.term; diff --git a/src/controllers/topics.js b/src/controllers/topics.js index abbb245f28..af62540ff6 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -18,6 +18,8 @@ var topicsController = {}, topicsController.get = function(req, res, callback) { var tid = req.params.topic_id, sort = req.query.sort, + currentPage = parseInt(req.query.page, 10) || 1, + pageCount = 1, userPrivileges; if ((req.params.post_index && !utils.isNumber(req.params.post_index)) || !utils.isNumber(tid)) { @@ -56,14 +58,13 @@ topicsController.get = function(req, res, callback) { var settings = results.settings; var postCount = parseInt(results.topic.postcount, 10); - var pageCount = Math.max(1, Math.ceil((postCount - 1) / settings.postsPerPage)); - var page = parseInt(req.query.page, 10) || 1; + pageCount = Math.max(1, Math.ceil((postCount - 1) / settings.postsPerPage)); if (utils.isNumber(req.params.post_index) && (req.params.post_index < 1 || req.params.post_index > postCount)) { return helpers.redirect(res, '/topic/' + req.params.topic_id + '/' + req.params.slug + (req.params.post_index > postCount ? '/' + postCount : '')); } - if (settings.usePagination && (page < 1 || page > pageCount)) { + if (settings.usePagination && (currentPage < 1 || currentPage > pageCount)) { return callback(); } @@ -105,10 +106,10 @@ topicsController.get = function(req, res, callback) { index = Math.max(0, req.params.post_index - 1) || 0; } - page = Math.max(1, Math.ceil(index / settings.postsPerPage)); + currentPage = Math.max(1, Math.ceil(index / settings.postsPerPage)); } - var start = (page - 1) * settings.postsPerPage + postIndex, + var start = (currentPage - 1) * settings.postsPerPage + postIndex, stop = start + settings.postsPerPage - 1; topics.getTopicWithPosts(tid, set, req.uid, start, stop, reverse, function (err, topicData) { @@ -120,9 +121,6 @@ topicsController.get = function(req, res, callback) { return next(err); } - topicData.pageCount = pageCount; - topicData.currentPage = page; - topics.modifyByPrivilege(topicData.posts, results.privileges); plugins.fireHook('filter:controllers.topic.get', topicData, next); @@ -238,7 +236,7 @@ topicsController.get = function(req, res, callback) { }, { rel: 'canonical', - href: nconf.get('url') + '/topic/' + topicData.slug + href: nconf.get('url') + '/topic/' + topicData.slug + (currentPage > 1 ? '?page=' + currentPage : '') } ]; @@ -261,7 +259,7 @@ topicsController.get = function(req, res, callback) { data['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1; data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; data.rssFeedUrl = nconf.get('relative_path') + '/topic/' + data.tid + '.rss'; - data.pagination = pagination.create(data.currentPage, data.pageCount); + data.pagination = pagination.create(currentPage, pageCount); data.pagination.rel.forEach(function(rel) { rel.href = nconf.get('url') + '/topic/' + data.slug + rel.href; res.locals.linkTags.push(rel); diff --git a/src/controllers/users.js b/src/controllers/users.js index 96c553a554..0c8e03f2c0 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -58,6 +58,9 @@ usersController.getUsersSortedByPosts = function(req, res, next) { }; usersController.getUsersSortedByReputation = function(req, res, next) { + if (parseInt(meta.config['reputation:disabled'], 10) === 1) { + return next(); + } usersController.getUsers('users:reputation', 0, 49, req, res, next); }; @@ -217,6 +220,7 @@ usersController.getMap = function(req, res, next) { res.render('usersMap', { rooms: data, + 'reputation:disabled': parseInt(meta.config['reputation:disabled'], 10) === 1, title: '[[pages:users/map]]', breadcrumbs: helpers.buildBreadcrumbs([{text: '[[global:users]]', url: '/users'}, {text: '[[global:map]]'}]) }); @@ -230,6 +234,7 @@ function render(req, res, data, next) { } data.templateData.inviteOnly = meta.config.registrationType === 'invite-only'; + data.templateData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; res.render('users', data.templateData); }); } diff --git a/src/database/mongo.js b/src/database/mongo.js index 49f2da5edd..0563e02633 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -178,25 +178,55 @@ module.info = function(db, callback) { async.parallel({ - serverStats: function(next) { + serverStatus: function(next) { db.command({'serverStatus': 1}, next); }, stats: function(next) { - db.stats({scale:1024}, next); + db.command({'dbStats': 1}, next); + }, + listCollections: function(next) { + db.listCollections().toArray(function(err, items) { + if (err) { + return next(err); + } + async.map(items, function(collection, next) { + db.collection(collection.name).stats(next); + }, next); + }); } }, function(err, results) { if (err) { return callback(err); } var stats = results.stats; + var scale = 1024 * 1024; + + results.listCollections = results.listCollections.map(function(collectionInfo) { + return { + name: collectionInfo.ns, + count: collectionInfo.count, + size: collectionInfo.size, + avgObjSize: collectionInfo.avgObjSize, + storageSize: collectionInfo.storageSize, + totalIndexSize: collectionInfo.totalIndexSize, + indexSizes: collectionInfo.indexSizes + }; + }); + + stats.mem = results.serverStatus.mem; + stats.collectionData = results.listCollections; + stats.network = results.serverStatus.network; + stats.raw = JSON.stringify(stats, null, 4); stats.avgObjSize = (stats.avgObjSize / 1024).toFixed(2); - stats.dataSize = (stats.dataSize / 1024).toFixed(2); - stats.storageSize = (stats.storageSize / 1024).toFixed(2); - stats.fileSize = (stats.fileSize / 1024).toFixed(2); - stats.indexSize = (stats.indexSize / 1024).toFixed(2); - stats.mem = results.serverStats.mem; - stats.raw = JSON.stringify(stats, null, 4); + stats.dataSize = (stats.dataSize / scale).toFixed(2); + stats.storageSize = (stats.storageSize / scale).toFixed(2); + stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0; + stats.indexSize = (stats.indexSize / scale).toFixed(2); + stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1'; + stats.host = results.serverStatus.host; + stats.version = results.serverStatus.version; + stats.uptime = results.serverStatus.uptime; stats.mongo = true; callback(null, stats); diff --git a/src/database/mongo/helpers.js b/src/database/mongo/helpers.js index 0be79b5388..db84dbb369 100644 --- a/src/database/mongo/helpers.js +++ b/src/database/mongo/helpers.js @@ -6,6 +6,7 @@ helpers.toMap = function(data) { var map = {}; for (var i = 0; i'; - str = (res.locals.postHeader ? res.locals.postHeader : '') + str + (res.locals.preFooter ? res.locals.preFooter : ''); - - if (res.locals.footer) { - str = str + res.locals.footer; - } else if (res.locals.adminFooter) { - str = str + res.locals.adminFooter; - } - - if (res.locals.renderHeader || res.locals.renderAdminHeader) { - var method = res.locals.renderHeader ? middleware.renderHeader : middleware.admin.renderHeader; - method(req, res, options, function(err, template) { - if (err) { - return fn(err); - } - str = template + str; - var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB'; - language = req.query.lang || language; - translator.translate(str, language, function(translated) { - fn(err, translated); - }); - }); - } else { - fn(err, str); - } - }); - }; - - next(); -}; - middleware.routeTouchIcon = function(req, res) { if (meta.config['brand:logo'] && validator.isURL(meta.config['brand:logo'])) { return res.redirect(meta.config['brand:logo']); @@ -403,8 +169,6 @@ middleware.addExpiresHeaders = function(req, res, next) { next(); }; - - middleware.privateTagListing = function(req, res, next) { if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) { controllers.helpers.notAllowed(req, res); @@ -414,32 +178,26 @@ middleware.privateTagListing = function(req, res, next) { }; middleware.exposeGroupName = function(req, res, next) { - if (!req.params.hasOwnProperty('slug')) { return next(); } + expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next); +}; - groups.getGroupNameByGroupSlug(req.params.slug, function(err, groupName) { +middleware.exposeUid = function(req, res, next) { + expose('uid', user.getUidByUserslug, 'userslug', req, res, next); +}; + +function expose(exposedField, method, field, req, res, next) { + if (!req.params.hasOwnProperty(field)) { + return next(); + } + method(req.params[field], function(err, id) { if (err) { return next(err); } - res.locals.groupName = groupName; + res.locals[exposedField] = id; next(); }); -}; - -middleware.exposeUid = function(req, res, next) { - if (req.params.hasOwnProperty('userslug')) { - user.getUidByUserslug(req.params.userslug, function(err, uid) { - if (err) { - return next(err); - } - - res.locals.uid = uid; - next(); - }); - } else { - next(); - } -}; +} middleware.requireUser = function(req, res, next) { if (req.user) { @@ -449,32 +207,23 @@ middleware.requireUser = function(req, res, next) { res.render('403', {title: '[[global:403.title]]'}); }; -function redirectToLogin(req, res) { - req.session.returnTo = nconf.get('relative_path') + req.url.replace(/^\/api/, ''); - return controllers.helpers.redirect(res, '/login'); -} - - - -function modifyTitle(obj) { - var title = controllers.helpers.buildTitle('[[pages:home]]'); - obj.browserTitle = title; - - if (obj.metaTags) { - obj.metaTags.forEach(function(tag, i) { - if (tag.property === 'og:title') { - obj.metaTags[i].content = title; - } - }); +middleware.privateUploads = function(req, res, next) { + if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) { + return next(); } + if (req.path.startsWith('/uploads/files')) { + return res.status(403).json('not-allowed'); + } + next(); +}; - return title; -} module.exports = function(webserver) { app = webserver; middleware.admin = require('./admin')(webserver); + require('./header')(app, middleware); + require('./render')(middleware); require('./maintenance')(middleware); return middleware; diff --git a/src/middleware/render.js b/src/middleware/render.js new file mode 100644 index 0000000000..4a80d4b9c8 --- /dev/null +++ b/src/middleware/render.js @@ -0,0 +1,95 @@ +'use strict'; + +var nconf = require('nconf'); +var translator = require('../../public/src/modules/translator'); + +module.exports = function(middleware) { + + middleware.processRender = function(req, res, next) { + // res.render post-processing, modified from here: https://gist.github.com/mrlannigan/5051687 + var render = res.render; + res.render = function(template, options, fn) { + var self = this, + req = this.req, + defaultFn = function(err, str){ + if (err) { + return req.next(err); + } + + self.send(str); + }; + options = options || {}; + + if ('function' === typeof options) { + fn = options; + options = {}; + } + + options.loggedIn = req.user ? parseInt(req.user.uid, 10) !== 0 : false; + options.relative_path = nconf.get('relative_path'); + options.template = {name: template}; + options.template[template] = true; + options.bodyClass = buildBodyClass(req); + + res.locals.template = template; + + if (res.locals.isAPI) { + if (req.route && req.route.path === '/api/') { + options.title = '[[pages:home]]'; + } + + return res.json(options); + } + + if ('function' !== typeof fn) { + fn = defaultFn; + } + + var ajaxifyData = encodeURIComponent(JSON.stringify(options)); + render.call(self, template, options, function(err, str) { + if (err) { + return fn(err); + } + + str = str + ''; + str = (res.locals.postHeader ? res.locals.postHeader : '') + str + (res.locals.preFooter ? res.locals.preFooter : ''); + + if (res.locals.footer) { + str = str + res.locals.footer; + } else if (res.locals.adminFooter) { + str = str + res.locals.adminFooter; + } + + if (res.locals.renderHeader || res.locals.renderAdminHeader) { + var method = res.locals.renderHeader ? middleware.renderHeader : middleware.admin.renderHeader; + method(req, res, options, function(err, template) { + if (err) { + return fn(err); + } + str = template + str; + var language = res.locals.config ? res.locals.config.userLang || 'en_GB' : 'en_GB'; + language = req.query.lang || language; + translator.translate(str, language, function(translated) { + fn(err, translated); + }); + }); + } else { + fn(err, str); + } + }); + }; + + next(); + }; + + function buildBodyClass(req) { + var clean = req.path.replace(/^\/api/, '').replace(/^\//, ''); + var parts = clean.split('/').slice(0, 3); + parts.forEach(function(p, index) { + parts[index] = index ? parts[index - 1] + '-' + p : 'page-' + (p || 'home'); + }); + + return parts.join(' '); + } + +}; \ No newline at end of file diff --git a/src/navigation/admin.js b/src/navigation/admin.js index d608e1b620..b66982a310 100644 --- a/src/navigation/admin.js +++ b/src/navigation/admin.js @@ -7,6 +7,7 @@ var admin = {}, db = require('../database'), translator = require('../../public/src/modules/translator'); +var navigationCache = null; admin.save = function(data, callback) { var order = Object.keys(data), @@ -23,6 +24,7 @@ admin.save = function(data, callback) { return JSON.stringify(data); }); + navigationCache = null; async.waterfall([ function(next) { db.delete('navigation:enabled', next); @@ -41,10 +43,19 @@ admin.getAdmin = function(callback) { }; admin.get = function(callback) { + if (navigationCache) { + return callback(null, navigationCache); + } + db.getSortedSetRange('navigation:enabled', 0, -1, function(err, data) { - callback(err, data.map(function(item, idx) { + if (err) { + return callback(err); + } + navigationCache = data.map(function(item, idx) { return JSON.parse(item)[idx]; - })); + }); + + callback(null, navigationCache); }); }; diff --git a/src/navigation/index.js b/src/navigation/index.js index 6c8be11d2f..7d536dcca9 100644 --- a/src/navigation/index.js +++ b/src/navigation/index.js @@ -2,8 +2,6 @@ var navigation = {}, - plugins = require('../plugins'), - db = require('../database'), admin = require('./admin'), translator = require('../../public/src/modules/translator'); diff --git a/src/notifications.js b/src/notifications.js index 48e59f168a..a3d4e61ef2 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -229,7 +229,7 @@ var async = require('async'), return callback(); } - db.getObject('notification:' + nid, function(err, notification) { + db.getObject('notifications:' + nid, function(err, notification) { if (err || !notification) { return callback(err || new Error('[[error:no-notification]]')); } diff --git a/src/pagination.js b/src/pagination.js index 58813c391b..15b8aa6eb3 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -10,7 +10,9 @@ pagination.create = function(currentPage, pageCount, queryObj) { prev: {page: 1, active: currentPage > 1}, next: {page: 1, active: currentPage < pageCount}, rel: [], - pages: [] + pages: [], + currentPage: 1, + pageCount: 1 }; } pageCount = parseInt(pageCount, 10); @@ -44,7 +46,7 @@ pagination.create = function(currentPage, pageCount, queryObj) { } } - var data = {rel: [], pages: pages}; + var data = {rel: [], pages: pages, currentPage: currentPage, pageCount: pageCount}; queryObj.page = previous; data.prev = {page: previous, active: currentPage > 1, qs: qs.stringify(queryObj)}; queryObj.page = next; diff --git a/src/posts/flags.js b/src/posts/flags.js index cbfa1f832c..3adb6541f2 100644 --- a/src/posts/flags.js +++ b/src/posts/flags.js @@ -9,7 +9,10 @@ var async = require('async'), module.exports = function(Posts) { - Posts.flag = function(post, uid, callback) { + Posts.flag = function(post, uid, reason, callback) { + if (!parseInt(uid, 10) || !reason) { + return callback(); + } async.parallel({ hasFlagged: async.apply(hasFlagged, post.pid, uid), exists: async.apply(Posts.exists, post.pid) @@ -36,6 +39,9 @@ module.exports = function(Posts) { function(next) { db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next); }, + function(next) { + db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next); + }, function(next) { if (parseInt(post.uid, 10)) { db.sortedSetAdd('uid:' + post.uid + ':flag:pids', now, post.pid, next); @@ -50,7 +56,7 @@ module.exports = function(Posts) { next(); } } - ], function(err, results) { + ], function(err) { callback(err); }); }); @@ -80,26 +86,75 @@ module.exports = function(Posts) { }, function(next) { db.delete('pid:' + pid + ':flag:uids', next); + }, + function(next) { + db.delete('pid:' + pid + ':flag:uid:reason', next); } - ], function(err, results) { + ], function(err) { callback(err); }); }; Posts.dismissAllFlags = function(callback) { - db.delete('posts:flagged', callback); - }; - - Posts.getFlags = function(set, uid, start, stop, callback) { - db.getSortedSetRevRange(set, start, stop, function(err, pids) { + db.getSortedSetRange('posts:flagged', 0, -1, function(err, pids) { if (err) { return callback(err); } - - Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags']}, callback); + async.eachLimit(pids, 50, Posts.dismissFlag, callback); }); }; + Posts.getFlags = function(set, uid, start, stop, callback) { + async.waterfall([ + function (next) { + db.getSortedSetRevRange(set, start, stop, next); + }, + function (pids, next) { + getFlaggedPostsWithReasons(pids, uid, next); + } + ], callback); + }; + + function getFlaggedPostsWithReasons(pids, uid, callback) { + async.waterfall([ + function (next) { + async.parallel({ + uidsReasons: function(next) { + async.map(pids, function(pid, next) { + db.getSortedSetRange('pid:' + pid + ':flag:uid:reason', 0, -1, next); + }, next); + }, + posts: function(next) { + Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags']}, next); + } + }, next); + }, + function (results, next) { + async.map(results.uidsReasons, function(uidReasons, next) { + async.map(uidReasons, function(uidReason, next) { + var uid = uidReason.split(':')[0]; + var reason = uidReason.substr(uidReason.indexOf(':') + 1); + user.getUserFields(uid, ['username', 'userslug', 'picture'], function(err, userData) { + next(err, {user: userData, reason: reason}); + }); + }, next); + }, function(err, reasons) { + if (err) { + return callback(err); + } + + results.posts.forEach(function(post, index) { + if (post) { + post.flagReasons = reasons[index]; + } + }); + + next(null, results.posts); + }); + } + ], callback); + } + Posts.getUserFlags = function(byUsername, sortBy, callerUID, start, stop, callback) { async.waterfall([ function(next) { @@ -112,7 +167,7 @@ module.exports = function(Posts) { db.getSortedSetRevRange('uid:' + uid + ':flag:pids', 0, -1, next); }, function(pids, next) { - Posts.getPostSummaryByPids(pids, callerUID, {stripTags: false, extraFields: ['flags']}, next); + getFlaggedPostsWithReasons(pids, callerUID, next); }, function(posts, next) { if (sortBy === 'count') { @@ -120,6 +175,7 @@ module.exports = function(Posts) { return b.flags - a.flags; }); } + next(null, posts.slice(start, stop)); } ], callback); diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 43518861d7..fa03bca51b 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -179,7 +179,8 @@ module.exports = function(privileges) { 'topics:create': results['topics:create'][0] || isAdminOrMod, editable: isAdminOrMod, view_deleted: isAdminOrMod, - read: results.read[0] || isAdminOrMod + read: results.read[0] || isAdminOrMod, + isAdminOrMod: isAdminOrMod }, callback); }); }; diff --git a/src/routes/accounts.js b/src/routes/accounts.js index 40163e095c..2d492c6c78 100644 --- a/src/routes/accounts.js +++ b/src/routes/accounts.js @@ -17,8 +17,11 @@ module.exports = function (app, middleware, controllers) { setupPageRoute(app, '/user/:userslug/favourites', middleware, accountMiddlewares, controllers.accounts.posts.getFavourites); setupPageRoute(app, '/user/:userslug/watched', middleware, accountMiddlewares, controllers.accounts.posts.getWatchedTopics); setupPageRoute(app, '/user/:userslug/edit', middleware, accountMiddlewares, controllers.accounts.edit.get); + setupPageRoute(app, '/user/:userslug/edit/username', middleware, accountMiddlewares, controllers.accounts.edit.username); + setupPageRoute(app, '/user/:userslug/edit/email', middleware, accountMiddlewares, controllers.accounts.edit.email); + setupPageRoute(app, '/user/:userslug/edit/password', middleware, accountMiddlewares, controllers.accounts.edit.password); setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get); setupPageRoute(app, '/notifications', middleware, [middleware.authenticate], controllers.accounts.notifications.get); - setupPageRoute(app, '/chats/:userslug?', middleware, [middleware.redirectToLoginIfGuest], controllers.accounts.chats.get); + setupPageRoute(app, '/chats/:userslug?', middleware, [middleware.authenticate], controllers.accounts.chats.get); }; diff --git a/src/routes/admin.js b/src/routes/admin.js index f60b398bb2..06edc0b805 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -80,6 +80,7 @@ function addRoutes(router, middleware, controllers) { router.get('/advanced/post-cache', middlewares, controllers.admin.postCache.get); router.get('/development/logger', middlewares, controllers.admin.logger.get); + router.get('/development/info', middlewares, controllers.admin.info.get); } module.exports = function(app, middleware, controllers) { diff --git a/src/routes/api.js b/src/routes/api.js index e0f1684f05..a888c20747 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -2,8 +2,6 @@ var express = require('express'), - posts = require('../posts'), - categories = require('../categories'), uploadsController = require('../controllers/uploads'); module.exports = function(app, middleware, controllers) { @@ -22,6 +20,7 @@ module.exports = function(app, middleware, controllers) { router.get('/categories/:cid/moderators', controllers.api.getModerators); router.get('/recent/posts/:term?', controllers.api.getRecentPosts); router.get('/unread/total', middleware.authenticate, controllers.unread.unreadTotal); + router.get('/topic/teaser/:topic_id', controllers.topics.teaser); var multipart = require('connect-multiparty'); var multipartMiddleware = multipart(); diff --git a/src/routes/index.js b/src/routes/index.js index 31573544d9..57a8c8134a 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,7 +4,6 @@ var nconf = require('nconf'), path = require('path'), winston = require('winston'), controllers = require('../controllers'), - meta = require('../meta'), plugins = require('../plugins'), express = require('express'), @@ -30,14 +29,12 @@ function mainRoutes(app, middleware, controllers) { setupPageRoute(app, '/compose', middleware, [middleware.authenticate], controllers.compose); setupPageRoute(app, '/confirm/:code', middleware, [], controllers.confirmEmail); setupPageRoute(app, '/outgoing', middleware, [], controllers.outgoing); - setupPageRoute(app, '/search/:term?', middleware, [middleware.guestSearchingAllowed], controllers.search.search); + setupPageRoute(app, '/search/:term?', middleware, [], controllers.search.search); setupPageRoute(app, '/reset/:code?', middleware, [], controllers.reset); setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse); } function topicRoutes(app, middleware, controllers) { - app.get('/api/topic/teaser/:topic_id', controllers.topics.teaser); - setupPageRoute(app, '/topic/:topic_id/:slug/:post_index?', middleware, [], controllers.topics.get); setupPageRoute(app, '/topic/:topic_id/:slug?', middleware, [], controllers.topics.get); } @@ -120,15 +117,7 @@ module.exports = function(app, middleware) { require('./debug')(app, middleware, controllers); } - app.use(function(req, res, next) { - if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) { - return next(); - } - if (req.path.startsWith('/uploads/files')) { - return res.status(403).json('not-allowed'); - } - next(); - }); + app.use(middleware.privateUploads); app.use(relativePath, express.static(path.join(__dirname, '../../', 'public'), { maxAge: app.enabled('cache') ? 5184000000 : 0 @@ -144,7 +133,11 @@ module.exports = function(app, middleware) { }; function handle404(app, middleware) { - app.use(function(req, res, next) { + var relativePath = nconf.get('relative_path'); + var isLanguage = new RegExp('^' + relativePath + '/language/[\\w]{2,}/.*.json'), + isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js'); + + app.use(function(req, res) { if (plugins.hasListeners('action:meta.override404')) { return plugins.fireHook('action:meta.override404', { req: req, @@ -153,14 +146,14 @@ function handle404(app, middleware) { }); } - var relativePath = nconf.get('relative_path'); - var isLanguage = new RegExp('^' + relativePath + '/language/[\\w]{2,}/.*.json'), - isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js'); - if (isClientScript.test(req.url)) { res.type('text/javascript').status(200).send(''); } else if (isLanguage.test(req.url)) { res.status(200).json({}); + } else if (req.path.startsWith(relativePath + '/uploads')) { + res.status(404).send(''); + } else if (req.path === '/favicon.ico') { + res.status(404).send(''); } else if (req.accepts('html')) { if (process.env.NODE_ENV === 'development') { winston.warn('Route requested but not found: ' + req.url); @@ -182,7 +175,7 @@ function handle404(app, middleware) { } function handleErrors(app, middleware) { - app.use(function(err, req, res, next) { + app.use(function(err, req, res) { if (err.code === 'EBADCSRFTOKEN') { winston.error(req.path + '\n', err.message); return res.sendStatus(403); diff --git a/src/search.js b/src/search.js index ffd19b1cd6..94bc4a708f 100644 --- a/src/search.js +++ b/src/search.js @@ -22,15 +22,13 @@ search.search = function(data, callback) { return callback(err); } - result.search_query = validator.escape(query); + data.search_query = validator.escape(query); if (searchIn === 'titles' || searchIn === 'titlesposts') { searchIn = 'posts'; } - result[searchIn] = data.matches; - result.matchCount = data.matchCount; - result.pageCount = data.pageCount; - result.time = (process.elapsedTimeSince(start) / 1000).toFixed(2); - callback(null, result); + + data.time = (process.elapsedTimeSince(start) / 1000).toFixed(2); + callback(null, data); } var start = process.hrtime(); @@ -38,18 +36,12 @@ search.search = function(data, callback) { var query = data.query; var searchIn = data.searchIn || 'titlesposts'; - var result = { - posts: [], - users: [], - tags: [] - }; - if (searchIn === 'posts' || searchIn === 'titles' || searchIn === 'titlesposts') { searchInContent(data, done); } else if (searchIn === 'users') { - searchInUsers(data, done); + user.search(data, done); } else if (searchIn === 'tags') { - searchInTags(query, done); + topics.searchAndLoadTags(data, done); } else { callback(new Error('[[error:unknown-search-filter]]')); } @@ -91,7 +83,7 @@ function searchInContent(data, callback) { var matchCount = 0; if (!results || (!results.pids.length && !results.tids.length)) { - return callback(null, {matches: [], matchCount: matchCount, pageCount: 1}); + return callback(null, {posts: [], matchCount: matchCount, pageCount: 1}); } async.waterfall([ @@ -118,7 +110,7 @@ function searchInContent(data, callback) { posts.getPostSummaryByPids(pids, data.uid, {}, next); }, function(posts, next) { - next(null, {matches: posts, matchCount: matchCount, pageCount: Math.max(1, Math.ceil(parseInt(matchCount, 10) / 10))}); + next(null, {posts: posts, matchCount: matchCount, pageCount: Math.max(1, Math.ceil(parseInt(matchCount, 10) / 10))}); } ], callback); }); @@ -315,16 +307,12 @@ function sortPosts(posts, data) { } data.sortBy = data.sortBy || 'timestamp'; data.sortDirection = data.sortDirection || 'desc'; + var direction = data.sortDirection === 'desc' ? 1 : -1; + if (data.sortBy === 'timestamp') { - if (data.sortDirection === 'desc') { - posts.sort(function(p1, p2) { - return p2.timestamp - p1.timestamp; - }); - } else { - posts.sort(function(p1, p2) { - return p1.timestamp - p2.timestamp; - }); - } + posts.sort(function(p1, p2) { + return direction * (p2.timestamp - p1.timestamp); + }); return; } @@ -336,21 +324,13 @@ function sortPosts(posts, data) { return; } - var value = firstPost[fields[0]][fields[1]]; - var isNumeric = utils.isNumber(value); + var isNumeric = utils.isNumber(firstPost[fields[0]][fields[1]]); if (isNumeric) { - if (data.sortDirection === 'desc') { - posts.sort(function(p1, p2) { - return p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]]; - }); - } else { - posts.sort(function(p1, p2) { - return p1[fields[0]][fields[1]] - p2[fields[0]][fields[1]]; - }); - } + posts.sort(function(p1, p2) { + return direction * (p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]]); + }); } else { - var direction = data.sortDirection === 'desc' ? 1 : -1; posts.sort(function(p1, p2) { if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) { return direction; @@ -435,25 +415,6 @@ function getSearchUids(data, callback) { } } -function searchInUsers(data, callback) { - user.search(data, function(err, results) { - if (err) { - return callback(err); - } - callback(null, {matches: results.users, matchCount: results.matchCount, pageCount: results.pageCount}); - }); -} - -function searchInTags(query, callback) { - topics.searchAndLoadTags({query: query}, function(err, tags) { - if (err) { - return callback(err); - } - - callback(null, {matches: tags, matchCount: tags.length, pageCount: 1}); - }); -} - search.searchQuery = function(index, content, cids, uids, callback) { plugins.fireHook('filter:search.query', { index: index, diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js index 0cd47a778f..206604ec8f 100644 --- a/src/socket.io/admin/groups.js +++ b/src/socket.io/admin/groups.js @@ -1,5 +1,6 @@ "use strict"; +var async = require('async'); var groups = require('../../groups'), Groups = {}; @@ -20,7 +21,17 @@ Groups.join = function(socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - groups.join(data.groupName, data.uid, callback); + async.waterfall([ + function (next) { + groups.isMember(data.uid, data.groupName, next); + }, + function (isMember, next) { + if (isMember) { + return next(new Error('[[error:group-already-member]]')); + } + groups.join(data.groupName, data.uid, next); + } + ], callback); }; Groups.leave = function(socket, data, callback) { @@ -32,7 +43,17 @@ Groups.leave = function(socket, data, callback) { return callback(new Error('[[error:cant-remove-self-as-admin]]')); } - groups.leave(data.groupName, data.uid, callback); + async.waterfall([ + function (next) { + groups.isMember(data.uid, data.groupName, next); + }, + function (isMember, next) { + if (!isMember) { + return next(new Error('[[error:group-not-member]]')); + } + groups.leave(data.groupName, data.uid, next); + } + ], callback); }; Groups.update = function(socket, data, callback) { diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 4f6e5c14c0..d43d0c8171 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -15,7 +15,25 @@ SocketCategories.getRecentReplies = function(socket, cid, callback) { }; SocketCategories.get = function(socket, data, callback) { - categories.getCategoriesByPrivilege('categories:cid', socket.uid, 'find', callback); + async.parallel({ + isAdmin: async.apply(user.isAdministrator, socket.uid), + categories: function(next) { + async.waterfall([ + async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), + async.apply(categories.getCategoriesData), + ], next); + } + }, function(err, results) { + if (err) { + return callback(err); + } + + results.categories = results.categories.filter(function(category) { + return category && (!category.disabled || results.isAdmin); + }); + + callback(null, results.categories); + }); }; SocketCategories.getWatchedCategories = function(socket, data, callback) { diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index b75e6ba3df..1a07a90c26 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -62,8 +62,11 @@ SocketGroups.leave = function(socket, data, callback) { function isOwner(next) { return function (socket, data, callback) { - groups.ownership.isOwner(socket.uid, data.groupName, function(err, isOwner) { - if (err || !isOwner) { + async.parallel({ + isAdmin: async.apply(user.isAdmin, socket.uid), + isOwner: async.apply(groups.ownership.isOwner, socket.uid, data.groupName) + }, function(err, results) { + if (err || (!isOwner && !results.isAdmin)) { return callback(err || new Error('[[error:no-privileges]]')); } next(socket, data, callback); diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index c538aca14d..5b6a2a2e67 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -3,6 +3,7 @@ var async = require('async'); var winston = require('winston'); var nconf = require('nconf'); +var validator = require('validator'); var websockets = require('./index'); var user = require('../user'); @@ -86,14 +87,14 @@ SocketHelpers.sendNotificationToTopicOwner = function(tid, fromuid, notification async.parallel({ username: async.apply(user.getUserField, fromuid, 'username'), - topicData: async.apply(topics.getTopicFields, tid, ['uid', 'slug']), + topicData: async.apply(topics.getTopicFields, tid, ['uid', 'slug', 'title']), }, function(err, results) { if (err || fromuid === parseInt(results.topicData.uid, 10)) { return; } notifications.create({ - bodyShort: '[[' + notification + ', ' + results.username + ']]', + bodyShort: '[[' + notification + ', ' + results.username + ', ' + results.topicData.title + ']]', path: nconf.get('relative_path') + '/topic/' + results.topicData.slug, nid: 'tid:' + tid + ':uid:' + fromuid, from: fromuid diff --git a/src/socket.io/index.js b/src/socket.io/index.js index b0075c99d2..16188cb266 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -3,7 +3,6 @@ var SocketIO = require('socket.io'), socketioWildcard = require('socketio-wildcard')(), async = require('async'), - fs = require('fs'), nconf = require('nconf'), cookieParser = require('cookie-parser')(nconf.get('secret')), winston = require('winston'), diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js index 0381569472..c304833ef5 100644 --- a/src/socket.io/meta.js +++ b/src/socket.io/meta.js @@ -1,15 +1,10 @@ 'use strict'; -var nconf = require('nconf'), - winston = require('winston'), - validator = require('validator'), +var validator = require('validator'), - db = require('../database'), meta = require('../meta'), user = require('../user'), topics = require('../topics'), - logger = require('../logger'), - plugins = require('../plugins'), emitter = require('../emitter'), rooms = require('./rooms'), @@ -52,13 +47,7 @@ SocketMeta.rooms.enter = function(socket, data, callback) { return callback(new Error('[[error:not-allowed]]')); } - if (socket.currentRoom) { - rooms.leave(socket, socket.currentRoom); - if (socket.currentRoom.indexOf('topic') !== -1) { - websockets.in(socket.currentRoom).emit('event:user_leave', socket.uid); - } - socket.currentRoom = ''; - } + leaveCurrentRoom(socket); if (data.enter) { rooms.enter(socket, data.enter); @@ -75,6 +64,24 @@ SocketMeta.rooms.enter = function(socket, data, callback) { callback(); }; +SocketMeta.rooms.leaveCurrent = function(socket, data, callback) { + if (!socket.uid || !socket.currentRoom) { + return callback(); + } + leaveCurrentRoom(socket); + callback(); +}; + +function leaveCurrentRoom(socket) { + if (socket.currentRoom) { + rooms.leave(socket, socket.currentRoom); + if (socket.currentRoom.indexOf('topic') !== -1) { + websockets.in(socket.currentRoom).emit('event:user_leave', socket.uid); + } + socket.currentRoom = ''; + } +} + SocketMeta.rooms.getAll = function(socket, data, callback) { var roomClients = rooms.roomClients(); var socketData = { diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index b0dde46c5c..d7b2c7c03b 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -147,4 +147,11 @@ SocketModules.sounds.getMapping = function(socket, data, callback) { meta.sounds.getMapping(callback); }; +SocketModules.sounds.getData = function(socket, data, callback) { + async.parallel({ + mapping: async.apply(meta.sounds.getMapping), + files: async.apply(meta.sounds.getFiles) + }, callback); +}; + module.exports = SocketModules; diff --git a/src/socket.io/notifications.js b/src/socket.io/notifications.js index c957fff1a7..c540c338bb 100644 --- a/src/socket.io/notifications.js +++ b/src/socket.io/notifications.js @@ -12,6 +12,23 @@ SocketNotifs.get = function(socket, data, callback) { } }; +SocketNotifs.loadMore = function(socket, data, callback) { + if (!data || !parseInt(data.after, 10)) { + return callback(new Error('[[error:invalid-data]]')); + } + if (!socket.uid) { + return; + } + var start = parseInt(data.after, 10); + var stop = start + 20; + user.notifications.getAll(socket.uid, start, stop, function(err, notifications) { + if (err) { + return callback(err); + } + callback(null, {notifications: notifications, nextStart: stop}); + }); +}; + SocketNotifs.getCount = function(socket, data, callback) { user.notifications.getUnreadCount(socket.uid, callback); }; diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js index 201ec350a5..3e5230db6b 100644 --- a/src/socket.io/posts/flag.js +++ b/src/socket.io/posts/flag.js @@ -13,17 +13,21 @@ var meta = require('../../meta'); module.exports = function(SocketPosts) { - SocketPosts.flag = function(socket, pid, callback) { + SocketPosts.flag = function(socket, data, callback) { if (!socket.uid) { return callback(new Error('[[error:not-logged-in]]')); } + if (!data || !data.pid || !data.reason) { + return callback(new Error('[[error:invalid-data]]')); + } + var flaggingUser = {}, post; async.waterfall([ function (next) { - posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next); + posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next); }, function (postData, next) { if (parseInt(postData.deleted, 10) === 1) { @@ -55,7 +59,7 @@ module.exports = function(SocketPosts) { flaggingUser = user.userData; flaggingUser.uid = socket.uid; - posts.flag(post, socket.uid, next); + posts.flag(post, socket.uid, data.reason, next); }, function (next) { async.parallel({ @@ -74,8 +78,8 @@ module.exports = function(SocketPosts) { notifications.create({ bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + post.topic.title + ']]', bodyLong: post.content, - pid: pid, - nid: 'post_flag:' + pid + ':uid:' + socket.uid, + pid: data.pid, + nid: 'post_flag:' + data.pid + ':uid:' + socket.uid, from: socket.uid }, function(err, notification) { if (err || !notification) { diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 99b4299be1..08b5f11d0a 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -116,19 +116,6 @@ SocketUser.reset.commit = function(socket, data, callback) { }); }; - -SocketUser.isAdminOrSelf = function(socket, uid, callback) { - if (socket.uid === parseInt(uid, 10)) { - return callback(); - } - user.isAdministrator(socket.uid, function(err, isAdmin) { - if (err || !isAdmin) { - return callback(err || new Error('[[error:no-privileges]]')); - } - callback(); - }); -}; - SocketUser.follow = function(socket, data, callback) { if (!socket.uid || !data) { return; @@ -182,7 +169,7 @@ SocketUser.saveSettings = function(socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - SocketUser.isAdminOrSelf(socket, data.uid, function(err) { + user.isAdminOrSelf(socket.uid, data.uid, function(err) { if (err) { return callback(err); } @@ -220,6 +207,17 @@ SocketUser.getUnreadChatCount = function(socket, data, callback) { messaging.getUnreadCount(socket.uid, callback); }; +SocketUser.getUnreadCounts = function(socket, data, callback) { + if (!socket.uid) { + return callback(null, {}); + } + async.parallel({ + unreadTopicCount: async.apply(topics.getTotalUnread, socket.uid), + unreadChatCount: async.apply(messaging.getUnreadCount, socket.uid), + unreadNotificationCount: async.apply(user.notifications.getUnreadCount, socket.uid) + }, callback); +}; + SocketUser.loadMore = function(socket, data, callback) { if (!data || !data.set || parseInt(data.after, 10) < 0) { return callback(new Error('[[error:invalid-data]]')); diff --git a/src/socket.io/user/picture.js b/src/socket.io/user/picture.js index f7c4d61739..d46ce9284c 100644 --- a/src/socket.io/user/picture.js +++ b/src/socket.io/user/picture.js @@ -23,12 +23,12 @@ module.exports = function(SocketUser) { } else if (type === 'uploaded') { type = 'uploadedpicture'; } else { - return callback(new Error('[[error:invalid-image-type, ' + ['default', 'uploadedpicture'].join(', ') + ']]')); + return callback(new Error('[[error:invalid-image-type, ' + ['default', 'uploadedpicture'].join(', ') + ']]')); } async.waterfall([ function (next) { - SocketUser.isAdminOrSelf(socket, data.uid, next); + user.isAdminOrSelf(socket.uid, data.uid, next); }, function (next) { if (!type) { @@ -47,7 +47,7 @@ module.exports = function(SocketUser) { return; } - SocketUser.isAdminOrSelf(socket, data.uid, function(err) { + user.isAdminOrSelf(socket.uid, data.uid, function(err) { if (err) { return callback(err); } @@ -64,7 +64,7 @@ module.exports = function(SocketUser) { async.waterfall([ function (next) { - SocketUser.isAdminOrSelf(socket, data.uid, next); + user.isAdminOrSelf(socket.uid, data.uid, next); }, function (next) { user.getUserField(data.uid, 'uploadedpicture', next); diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index d966396a30..4e0be0acc5 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -8,6 +8,44 @@ var events = require('../../events'); module.exports = function(SocketUser) { + SocketUser.changeUsernameEmail = function(socket, data, callback) { + if (!data || !data.uid || !socket.uid) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + isAdminOrSelfAndPasswordMatch(socket.uid, data, next); + }, + function (next) { + SocketUser.updateProfile(socket, data, next); + } + ], callback); + }; + + function isAdminOrSelfAndPasswordMatch(uid, data, callback) { + async.parallel({ + isAdmin: async.apply(user.isAdministrator, uid), + hasPassword: async.apply(user.hasPassword, data.uid), + passwordMatch: async.apply(user.isPasswordCorrect, data.uid, data.password) + }, function(err, results) { + if (err) { + return callback(err); + } + var self = parseInt(uid, 10) === parseInt(data.uid, 10); + + if (!results.isAdmin && !self) { + return callback(new Error('[[error:no-privileges]]')); + } + + if (self && results.hasPassword && !results.passwordMatch) { + return callback(new Error('[[error:invalid-password]]')); + } + + callback(); + }); + } + SocketUser.changePassword = function(socket, data, callback) { if (!data || !data.uid || data.newPassword.length < meta.config.minimumPasswordLength) { return callback(new Error('[[error:invalid-data]]')); @@ -31,7 +69,6 @@ module.exports = function(SocketUser) { }); }; - SocketUser.updateProfile = function(socket, data, callback) { if (!socket.uid) { return callback('[[error:invalid-uid]]'); @@ -55,7 +92,7 @@ module.exports = function(SocketUser) { if (parseInt(meta.config['username:disableEdit'], 10) === 1) { data.username = oldUserData.username; } - SocketUser.isAdminOrSelf(socket, data.uid, next); + user.isAdminOrSelf(socket.uid, data.uid, next); }, function (next) { user.updateProfile(data.uid, data, next); diff --git a/src/topics/create.js b/src/topics/create.js index ae60a8c191..6ccc06de12 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -101,6 +101,9 @@ module.exports = function(Topics) { check(data.tags, meta.config.minimumTagsPerTopic, meta.config.maximumTagsPerTopic, 'not-enough-tags', 'too-many-tags', next); }, function(next) { + if (data.content) { + data.content = data.content.rtrim(); + } check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); }, function(next) { @@ -228,7 +231,7 @@ module.exports = function(Topics) { function(filteredData, next) { content = filteredData.content || data.content; if (content) { - content = content.trim(); + content = content.rtrim(); } check(content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); diff --git a/src/topics/tags.js b/src/topics/tags.js index fe2e8d8ea2..54514ddc42 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -2,12 +2,12 @@ 'use strict'; var async = require('async'), - winston = require('winston'), + db = require('../database'), meta = require('../meta'), _ = require('underscore'), - plugins = require('../plugins'), - utils = require('../../public/src/utils'); + plugins = require('../plugins'); + module.exports = function(Topics) { @@ -248,7 +248,7 @@ module.exports = function(Topics) { }; Topics.searchTags = function(data, callback) { - if (!data) { + if (!data || !data.query) { return callback(null, []); } @@ -256,9 +256,7 @@ module.exports = function(Topics) { if (err) { return callback(null, []); } - if (data.query === '') { - return callback(null, tags); - } + data.query = data.query.toLowerCase(); var matches = []; @@ -279,8 +277,14 @@ module.exports = function(Topics) { }; Topics.searchAndLoadTags = function(data, callback) { + var searchResult = { + tags: [], + matchCount: 0, + pageCount: 1 + }; + if (!data.query || !data.query.length) { - return callback(null, []); + return callback(null, searchResult); } Topics.searchTags(data, function(err, tags) { if (err) { @@ -307,8 +311,10 @@ module.exports = function(Topics) { results.tagData.sort(function(a, b) { return b.score - a.score; }); - - callback(null, results.tagData); + searchResult.tags = results.tagData; + searchResult.matchCount = results.tagData.length; + searchResult.pageCount = 1; + callback(null, searchResult); }); }); }; diff --git a/src/topics/unread.js b/src/topics/unread.js index d70915a7b7..294ac3cf17 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -142,7 +142,7 @@ module.exports = function(Topics) { if (err) { return callback(err); } - require('../socket.io').in('uid_' + uid).emit('event:unread.updateCount', null, count); + require('../socket.io').in('uid_' + uid).emit('event:unread.updateCount', count); callback(); }); }; diff --git a/src/user.js b/src/user.js index 74c3a75cd6..4d6593ab20 100644 --- a/src/user.js +++ b/src/user.js @@ -3,14 +3,11 @@ var async = require('async'), nconf = require('nconf'), gravatar = require('gravatar'), - validator = require('validator'), plugins = require('./plugins'), db = require('./database'), meta = require('./meta'), topics = require('./topics'), - groups = require('./groups'), - Password = require('./password'), privileges = require('./privileges'), utils = require('../public/src/utils'); @@ -37,6 +34,7 @@ var async = require('async'), require('./user/approval')(User); require('./user/invite')(User); require('./user/icon')(User); + require('./user/password')(User); User.updateLastOnlineTime = function(uid, callback) { callback = callback || function() {}; @@ -158,7 +156,7 @@ var async = require('async'), User.getUidByUserslug(userslug, function(err, exists) { callback(err, !! exists); }); - } + }; User.getUidByUsername = function(username, callback) { if (!username) { @@ -224,6 +222,18 @@ var async = require('async'), privileges.users.isAdministrator(uid, callback); }; + User.isAdminOrSelf = function(callerUid, uid, callback) { + if (parseInt(callerUid, 10) === parseInt(uid, 10)) { + return callback(); + } + User.isAdministrator(callerUid, function(err, isAdmin) { + if (err || !isAdmin) { + return callback(err || new Error('[[error:no-privileges]]')); + } + callback(); + }); + }; + }(exports)); diff --git a/src/user/delete.js b/src/user/delete.js index 1eba9939a2..778d270ecc 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -4,6 +4,7 @@ var async = require('async'), db = require('../database'), posts = require('../posts'), topics = require('../topics'), + favourites = require('../favourites'), groups = require('../groups'), plugins = require('../plugins'), batch = require('../batch'); @@ -21,12 +22,35 @@ module.exports = function(User) { function(next) { deleteTopics(uid, next); }, + function(next) { + deleteVotes(uid, next); + }, function(next) { User.deleteAccount(uid, next); } ], callback); }; + function deleteVotes(uid, callback) { + async.waterfall([ + function (next) { + async.parallel({ + upvotedPids: async.apply(db.getSortedSetRange, 'uid:' + uid + ':upvote', 0, -1), + downvotedPids: async.apply(db.getSortedSetRange, 'uid:' + uid + ':downvote', 0, -1) + }, next); + }, + function (pids, next) { + pids = pids.upvotedPids.concat(pids.downvotedPids).filter(function(pid, index, array) { + return pid && array.indexOf(pid) === index; + }); + + async.eachLimit(pids, 50, function(pid, next) { + favourites.unvote(pid, uid, next); + }, next); + } + ], callback); + } + function deletePosts(uid, callback) { deleteSortedSetElements('uid:' + uid + ':posts', posts.purge, callback); } @@ -125,20 +149,20 @@ module.exports = function(User) { }; function deleteUserIps(uid, callback) { - db.getSortedSetRange('uid:' + uid + ':ip', 0, -1, function(err, ips) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.getSortedSetRange('uid:' + uid + ':ip', 0, -1, next); + }, + function (ips, next) { + var keys = ips.map(function(ip) { + return 'ip:' + ip + ':uid'; + }); + db.sortedSetsRemove(keys, uid, next); + }, + function (next) { + db.delete('uid:' + uid + ':ip', next); } - - async.each(ips, function(ip, next) { - db.sortedSetRemove('ip:' + ip + ':uid', uid, next); - }, function(err) { - if (err) { - return callback(err); - } - db.delete('uid:' + uid + ':ip', callback); - }); - }); + ], callback); } function deleteUserFromFollowers(uid, callback) { diff --git a/src/user/notifications.js b/src/user/notifications.js index 1a93a6ba4b..7cc309d1a8 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -21,7 +21,7 @@ var async = require('async'), if (!parseInt(uid, 10)) { return callback(null , {read: [], unread: []}); } - getNotifications(uid, 10, function(err, notifications) { + getNotifications(uid, 0, 9, function(err, notifications) { if (err) { return callback(err); } @@ -38,8 +38,8 @@ var async = require('async'), }); }; - UserNotifications.getAll = function(uid, count, callback) { - getNotifications(uid, count, function(err, notifs) { + UserNotifications.getAll = function(uid, start, stop, callback) { + getNotifications(uid, start, stop, function(err, notifs) { if (err) { return callback(err); } @@ -52,13 +52,13 @@ var async = require('async'), }); }; - function getNotifications(uid, count, callback) { + function getNotifications(uid, start, stop, callback) { async.parallel({ unread: function(next) { - getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, 0, count - 1, next); + getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next); }, read: function(next) { - getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, 0, count - 1, next); + getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next); } }, callback); } diff --git a/src/user/password.js b/src/user/password.js new file mode 100644 index 0000000000..b326313be4 --- /dev/null +++ b/src/user/password.js @@ -0,0 +1,34 @@ +'use strict'; + +var nconf = require('nconf'); + +var db = require('../database'); +var Password = require('../password'); + +module.exports = function(User) { + + User.hashPassword = function(password, callback) { + if (!password) { + return callback(null, password); + } + + Password.hash(nconf.get('bcrypt_rounds') || 12, password, callback); + }; + + User.isPasswordCorrect = function(uid, password, callback) { + db.getObjectField('user:' + uid, 'password', function(err, hashedPassword) { + if (err || !hashedPassword) { + return callback(err); + } + + Password.compare(password || '', hashedPassword, callback); + }); + }; + + User.hasPassword = function(uid, callback) { + db.getObjectField('user:' + uid, 'password', function(err, hashedPassword) { + callback(err, !!hashedPassword); + }); + }; + +}; \ No newline at end of file diff --git a/src/user/profile.js b/src/user/profile.js index 0230e930a8..a516b30074 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -262,30 +262,21 @@ module.exports = function(User) { return callback(new Error('[[user:change_password_error]]')); } - if(parseInt(uid, 10) !== parseInt(data.uid, 10)) { + if (parseInt(uid, 10) !== parseInt(data.uid, 10)) { User.isAdministrator(uid, function(err, isAdmin) { - if(err || !isAdmin) { + if (err || !isAdmin) { return callback(err || new Error('[[user:change_password_error_privileges')); } hashAndSetPassword(callback); }); } else { - db.getObjectField('user:' + uid, 'password', function(err, currentPassword) { - if(err) { - return callback(err); + User.isPasswordCorrect(uid, data.currentPassword, function(err, correct) { + if (err || !correct) { + return callback(err || new Error('[[user:change_password_error_wrong_current]]')); } - if (!currentPassword) { - return hashAndSetPassword(callback); - } - - Password.compare(data.currentPassword, currentPassword, function(err, res) { - if (err || !res) { - return callback(err || new Error('[[user:change_password_error_wrong_current]]')); - } - hashAndSetPassword(callback); - }); + hashAndSetPassword(callback); }); } }; diff --git a/src/user/search.js b/src/user/search.js index 60f125096a..ae91ba1615 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -45,7 +45,6 @@ module.exports = function(User) { var pagination = User.paginate(page, uids); uids = pagination.data; searchResult.pagination = pagination.pagination; - searchResult.pageCount = pagination.pageCount; } User.getUsers(uids, uid, next); diff --git a/src/views/admin/advanced/database.tpl b/src/views/admin/advanced/database.tpl index b868b322a9..4f928b1e52 100644 --- a/src/views/admin/advanced/database.tpl +++ b/src/views/admin/advanced/database.tpl @@ -5,6 +5,10 @@
    Mongo
    + MongoDB Version {mongo.version}
    +
    + Uptime in Seconds {mongo.uptime}
    + Storage Engine {mongo.storageEngine}
    Collections {mongo.collections}
    Objects {mongo.objects}
    Avg. Object Size {mongo.avgObjSize} kb
    @@ -12,7 +16,9 @@ Data Size {mongo.dataSize} mb
    Storage Size {mongo.storageSize} mb
    Index Size {mongo.indexSize} mb
    + File Size {mongo.fileSize} mb
    +
    Resident Memory {mongo.mem.resident} mb
    Virtual Memory {mongo.mem.virtual} mb
    diff --git a/src/views/admin/development/info.tpl b/src/views/admin/development/info.tpl new file mode 100644 index 0000000000..563d12c11f --- /dev/null +++ b/src/views/admin/development/info.tpl @@ -0,0 +1,13 @@ +
    +
    +
    +

    Info

    +
    + +
    +
    +
    {info}
    +
    +
    +
    +
    \ No newline at end of file diff --git a/src/views/admin/general/navigation.tpl b/src/views/admin/general/navigation.tpl index ac6ec94515..ba7f5942e5 100644 --- a/src/views/admin/general/navigation.tpl +++ b/src/views/admin/general/navigation.tpl @@ -31,32 +31,32 @@
    -
    - +
    +
    -
    - +
    +
    - +
    -
    +
    -
    diff --git a/src/views/admin/partials/categories/category-rows.tpl b/src/views/admin/partials/categories/category-rows.tpl index 715427c34e..3f16bedd42 100644 --- a/src/views/admin/partials/categories/category-rows.tpl +++ b/src/views/admin/partials/categories/category-rows.tpl @@ -16,7 +16,7 @@
    - Edit diff --git a/tests/user.js b/tests/user.js index 06e756d50e..99a677afce 100644 --- a/tests/user.js +++ b/tests/user.js @@ -55,12 +55,12 @@ describe('User', function() { }); }); - it('should have a valid email, if using an email', function() { - assert.throws( - User.create({username: userData.username, password: userData.password, email: 'fakeMail'},function(){}), - Error, - 'does not validate email' - ); + it('should have a valid email, if using an email', function(done) { + User.create({username: userData.username, password: userData.password, email: 'fakeMail'},function(err) { + assert(err); + assert.equal(err.message, '[[error:invalid-email]]'); + done(); + }); }); });