Merge remote-tracking branch 'origin/master' into user-icons

Conflicts:
	public/src/client/account/edit.js
	src/middleware/middleware.js
	src/socket.io/meta.js
	src/socket.io/user/picture.js
	src/user.js
	src/views/admin/manage/group.tpl
This commit is contained in:
Julian Lam
2015-10-25 07:33:59 -04:00
112 changed files with 1769 additions and 1104 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -14,8 +14,8 @@
"new_message_from": "New message from <strong>%1</strong>",
"upvoted_your_post_in": "<strong>%1</strong> has upvoted your post in <strong>%2</strong>.",
"moved_your_post": "<strong>%1</strong> has moved your post.",
"moved_your_topic": "<strong>%1</strong> has moved your topic.",
"moved_your_post": "<strong>%1</strong> has moved your post to <strong>%2</strong>",
"moved_your_topic": "<strong>%1</strong> has moved <strong>%2</strong>",
"favourited_your_post_in": "<strong>%1</strong> has favourited your post in <strong>%2</strong>.",
"user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>",
"user_posted_to" : "<strong>%1</strong> has posted a reply to: <strong>%2</strong>",

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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') {

View File

@@ -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);
}

View File

@@ -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', {

View File

@@ -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 = $('<li />');
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($('<img />').attr('src', results.users[x].picture))
.append($('<span />').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);
});

View File

@@ -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;

View File

@@ -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') {

View File

@@ -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');

View File

@@ -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;
});
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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();
}
}

View File

@@ -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 + '"]');

View File

@@ -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();
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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);
};

View File

@@ -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;
});
});

View File

@@ -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,

View File

@@ -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();

View File

@@ -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;
});

View File

@@ -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]');

View File

@@ -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('<a href="' + $this.attr('src') + '" target="_blank">');
}
});
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() {

View File

@@ -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 '<meta ' + name + property + content + '/>';
return '<meta ' + name + property + content + '/>\n\t';
};
helpers.buildLinkTag = function(tag) {
@@ -47,7 +50,7 @@
href = tag.href ? 'href="' + tag.href + '" ' : '',
sizes = tag.sizes ? 'sizes="' + tag.sizes + '" ' : '';
return '<link ' + link + rel + type + sizes + href + '/>';
return '<link ' + link + rel + type + sizes + href + '/>\n\t';
};
helpers.stringify = function(obj) {

View File

@@ -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);
});

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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: []
};

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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);
});
}
};

View File

@@ -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({

View File

@@ -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];

View File

@@ -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]]'}])
});

View File

@@ -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')
};

View File

@@ -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});
});
};

View File

@@ -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;

View File

@@ -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('&#44; ') + ']]'});
return false;
}

View File

@@ -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();
});

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
});
}

View File

@@ -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);

View File

@@ -6,6 +6,7 @@ helpers.toMap = function(data) {
var map = {};
for (var i = 0; i<data.length; ++i) {
map[data[i]._key] = data[i];
data[i]._key = undefined;
}
return map;
};

View File

@@ -53,7 +53,7 @@ module.exports = function(db, module) {
}
}
db.collection('search' + key).find(searchQuery, {limit: limit}).toArray(function(err, results) {
db.collection('search' + key).find(searchQuery, {limit: limit, fields:{_id: 0, id: 1}}).toArray(function(err, results) {
if (err) {
return callback(err);
}

View File

@@ -125,7 +125,7 @@ module.exports = function(db, module) {
if (withScores) {
fields.score = 1;
}
db.collection('objects').find({_key:key}, {fields: fields})
db.collection('objects').find({_key: key}, {fields: fields})
.limit(stop - start + 1)
.skip(start)
.sort({score: sort})

View File

@@ -26,13 +26,13 @@ var async = require('async'),
var now = Date.now();
if(type === 'upvote' && !unvote) {
if (type === 'upvote' && !unvote) {
db.sortedSetAdd('uid:' + uid + ':upvote', now, pid);
} else {
db.sortedSetRemove('uid:' + uid + ':upvote', pid);
}
if(type === 'upvote' || unvote) {
if (type === 'upvote' || unvote) {
db.sortedSetRemove('uid:' + uid + ':downvote', pid);
} else {
db.sortedSetAdd('uid:' + uid + ':downvote', now, pid);
@@ -213,7 +213,7 @@ var async = require('async'),
if (voteStatus.upvoted && command === 'downvote' || voteStatus.downvoted && command === 'upvote') { // e.g. User *has* upvoted, and clicks downvote
hook = command;
} else if (voteStatus.upvoted || voteStatus.downvoted) { // e.g. User *has* upvotes, clicks upvote (so we "unvote")
} else if (voteStatus.upvoted || voteStatus.downvoted) { // e.g. User *has* upvoted, clicks upvote (so we "unvote")
hook = 'unvote';
} else { // e.g. User *has not* voted, clicks upvote
hook = command;

View File

@@ -54,7 +54,7 @@ file.isFileTypeAllowed = function(path, allowedExtensions, callback) {
var uploadedFileExtension = mime.extension(mimeType);
if (allowedExtensions.indexOf(uploadedFileExtension) === -1) {
return callback(new Error('[[error:invalid-file-type, ' + allowedExtensions.join(', ') + ']]'));
return callback(new Error('[[error:invalid-file-type, ' + allowedExtensions.join('&#44; ') + ']]'));
}
callback();

View File

@@ -13,7 +13,7 @@ var async = require('async'),
"dependencies": ["redis@~0.10.1", "connect-redis@~2.0.0"]
},
"mongo": {
"dependencies": ["mongodb@~2.0.0", "connect-mongo"]
"dependencies": ["mongodb@~2.0.0", "connect-mongo@~0.8.2"]
}
};

View File

@@ -111,7 +111,8 @@ var db = require('./database'),
touid = params.touid,
since = params.since,
isNew = params.isNew,
count = params.count || parseInt(meta.config.chatMessageInboxSize, 10) || 250;
count = params.count || parseInt(meta.config.chatMessageInboxSize, 10) || 250,
markRead = params.markRead || true;
var uids = sortUids(fromuid, touid),
min = params.count ? 0 : Date.now() - (terms[since] || terms.day);
@@ -135,13 +136,15 @@ var db = require('./database'),
getMessages(mids, fromuid, touid, isNew, callback);
});
notifications.markRead('chat_' + touid + '_' + fromuid, fromuid, function(err) {
if (err) {
winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message);
}
if (markRead) {
notifications.markRead('chat_' + touid + '_' + fromuid, fromuid, function(err) {
if (err) {
winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message);
}
userNotifications.pushCount(fromuid);
});
userNotifications.pushCount(fromuid);
});
}
};
function getMessages(mids, fromuid, touid, isNew, callback) {
@@ -271,7 +274,8 @@ var db = require('./database'),
fromuid: fromuid,
touid: uid,
isNew: false,
count: 1
count: 1,
markRead: false
}, function(err, teaser) {
var teaser = teaser[0];
teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s;
@@ -286,7 +290,6 @@ var db = require('./database'),
results.users.forEach(function(user, index) {
if (user && parseInt(user.uid, 10)) {
Messaging.markRead(uid, uids[index]);
user.unread = results.unread[index];
user.status = sockets.isUserOnline(user.uid) ? user.status : 'offline';
user.teaser = results.teasers[index];
@@ -311,7 +314,7 @@ var db = require('./database'),
if (err) {
return;
}
sockets.in('uid_' + uid).emit('event:unread.updateChatCount', null, unreadCount);
sockets.in('uid_' + uid).emit('event:unread.updateChatCount', unreadCount);
});
};

View File

@@ -6,7 +6,7 @@ var path = require('path'),
semver = require('semver'),
winston = require('winston'),
pkg = require.main.require('./package.json');
pkg = require('../../package.json');
module.exports = function(Meta) {
Meta.dependencies = {};

View File

@@ -5,7 +5,6 @@ var winston = require('winston'),
path = require('path'),
async = require('async'),
_ = require('underscore'),
os = require('os'),
nconf = require('nconf'),
fs = require('fs'),
file = require('../file'),
@@ -50,15 +49,32 @@ module.exports = function(Meta) {
'public/src/client/chats.js',
'public/src/client/infinitescroll.js',
'public/src/client/pagination.js',
'public/src/client/recent.js',
'public/src/client/unread.js',
'public/src/client/topic.js',
'public/src/client/topic/browsing.js',
'public/src/client/topic/events.js',
'public/src/client/topic/flag.js',
'public/src/client/topic/fork.js',
'public/src/client/topic/move.js',
'public/src/client/topic/posts.js',
'public/src/client/topic/postTools.js',
'public/src/client/topic/threadTools.js',
'public/src/client/categories.js',
'public/src/client/category.js',
'public/src/client/categoryTools.js',
'public/src/modules/csrf.js',
'public/src/modules/translator.js',
'public/src/modules/notifications.js',
'public/src/modules/chat.js',
'public/src/modules/components.js',
'public/src/modules/composer/formatting.js',
'public/src/modules/composer/controls.js',
'public/src/modules/composer/preview.js',
'public/src/modules/categories.js',
'public/src/modules/sort.js',
'public/src/modules/navigator.js',
'public/src/modules/topicSelect.js',
'public/src/modules/share.js',
'public/src/modules/search.js',
'public/src/modules/alerts.js',
'public/src/modules/taskbar.js',
'public/src/modules/helpers.js',
'public/src/modules/sounds.js',
@@ -72,8 +88,7 @@ module.exports = function(Meta) {
async.apply(getPluginScripts), // plugin scripts via filter:scripts.get
function(next) { // client scripts via "scripts" config in plugin.json
var pluginsScripts = [],
pluginDirectories = [],
clientScripts = [];
pluginDirectories = [];
pluginsScripts = plugins.clientScripts.filter(function(path) {
if (path.endsWith('.js')) {
@@ -268,4 +283,4 @@ module.exports = function(Meta) {
callback();
});
}
};
};

View File

@@ -20,14 +20,16 @@ module.exports = function(Meta) {
Meta.settings.set = function(hash, values, callback) {
var key = 'settings:' + hash;
db.setObject(key, values, function(err) {
if (!err) {
plugins.fireHook('action:settings.set', {
plugin: hash,
settings: values
});
if (err) {
return callback(err);
}
callback.apply(this, arguments);
plugins.fireHook('action:settings.set', {
plugin: hash,
settings: values
});
callback();
});
};

View File

@@ -99,7 +99,7 @@ module.exports = function(Meta) {
return callback(null, defaults);
}
callback.apply(null, arguments);
callback(null, sounds);
});
};
};

157
src/middleware/header.js Normal file
View File

@@ -0,0 +1,157 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var user = require('../user');
var meta = require('../meta');
var plugins = require('../plugins');
var navigation = require('../navigation');
var translator = require('../../public/src/modules/translator');
var controllers = {
api: require('../controllers/api'),
helpers: require('../controllers/helpers')
};
module.exports = function(app, middleware) {
middleware.buildHeader = function(req, res, next) {
res.locals.renderHeader = true;
res.locals.isAPI = false;
middleware.applyCSRF(req, res, function() {
async.parallel({
config: function(next) {
controllers.api.getConfig(req, res, next);
},
footer: function(next) {
app.render('footer', {loggedIn: (req.user ? parseInt(req.user.uid, 10) !== 0 : false)}, next);
},
plugins: function(next) {
plugins.fireHook('filter:middleware.buildHeader', {req: req, locals: res.locals}, next);
}
}, function(err, results) {
if (err) {
return next(err);
}
res.locals.config = results.config;
translator.translate(results.footer, results.config.defaultLang, function(parsedTemplate) {
res.locals.footer = parsedTemplate;
next();
});
});
});
};
middleware.renderHeader = function(req, res, data, callback) {
var registrationType = meta.config.registrationType || 'normal';
var templateValues = {
bootswatchCSS: meta.config['theme:src'],
title: meta.config.title || '',
description: meta.config.description || '',
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
'brand:logo': meta.config['brand:logo'] || '',
'brand:logo:url': meta.config['brand:logo:url'] || '',
'brand:logo:alt': meta.config['brand:logo:alt'] || '',
'brand:logo:display': meta.config['brand:logo']?'':'hide',
allowRegistration: registrationType === 'normal' || registrationType === 'admin-approval',
searchEnabled: plugins.hasListeners('filter:search.query'),
config: res.locals.config,
relative_path: nconf.get('relative_path'),
bodyClass: data.bodyClass
};
templateValues.configJSON = JSON.stringify(res.locals.config);
async.parallel({
isAdmin: function(next) {
user.isAdministrator(req.uid, next);
},
user: function(next) {
if (req.uid) {
user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'icon:bgColor', 'icon:text', 'status', 'email:confirmed', 'banned'], next);
} else {
next(null, {
username: '[[global:guest]]',
userslug: '',
picture: meta.config.defaultAvatar,
status: 'offline',
banned: false,
uid: 0
});
}
},
navigation: async.apply(navigation.get),
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags)
}, function(err, results) {
if (err) {
return callback(err);
}
if (results.user && parseInt(results.user.banned, 10) === 1) {
req.logout();
return res.redirect('/');
}
results.user.isAdmin = results.isAdmin;
results.user.uid = parseInt(results.user.uid, 10);
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
if (res.locals.config.bootswatchSkin !== 'default') {
templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + res.locals.config.bootswatchSkin + '/bootstrap.min.css';
}
templateValues.browserTitle = controllers.helpers.buildTitle(data.title);
templateValues.navigation = results.navigation;
templateValues.metaTags = results.tags.meta;
templateValues.linkTags = results.tags.link;
templateValues.isAdmin = results.user.isAdmin;
templateValues.user = results.user;
templateValues.userJSON = JSON.stringify(results.user);
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS;
templateValues.customCSS = templateValues.useCustomCSS ? (meta.config.renderedCustomCSS || '') : '';
templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1;
templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : '';
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
templateValues.defaultLang = meta.config.defaultLang || 'en_GB';
templateValues.template = {name: res.locals.template};
templateValues.template[res.locals.template] = true;
if (req.route && req.route.path === '/') {
modifyTitle(templateValues);
}
plugins.fireHook('filter:middleware.renderHeader', {templateValues: templateValues, req: req, res: res}, function(err, data) {
if (err) {
return callback(err);
}
app.render('header', data.templateValues, callback);
});
});
};
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;
}
});
}
return title;
}
};

View File

@@ -7,21 +7,16 @@ var app,
async = require('async'),
path = require('path'),
csrf = require('csurf'),
winston = require('winston'),
validator = require('validator'),
nconf = require('nconf'),
ensureLoggedIn = require('connect-ensure-login'),
plugins = require('../plugins'),
navigation = require('../navigation'),
meta = require('../meta'),
translator = require('../../public/src/modules/translator'),
user = require('../user'),
groups = require('../groups'),
db = require('../database'),
categories = require('../categories'),
topics = require('../topics'),
messaging = require('../messaging'),
analytics = require('../analytics'),
@@ -69,7 +64,7 @@ middleware.pageView = function(req, res, next) {
middleware.pluginHooks = function(req, res, next) {
async.each(plugins.loadedHooks['filter:router.page'] || [], function(hookObj, next) {
hookObj.method(req, res, next);
}, function(req, res) {
}, function() {
// If it got here, then none of the subscribed hooks did anything, or there were no hooks
next();
});
@@ -87,14 +82,6 @@ middleware.redirectToAccountIfLoggedIn = function(req, res, next) {
});
};
middleware.redirectToLoginIfGuest = function(req, res, next) {
if (!req.user || parseInt(req.user.uid, 10) === 0) {
return redirectToLogin(req, res);
}
next();
};
middleware.validateFiles = function(req, res, next) {
if (!Array.isArray(req.files.files) || !req.files.files.length) {
return next(new Error(['[[error:invalid-files]]']));
@@ -108,14 +95,6 @@ middleware.prepareAPI = function(req, res, next) {
next();
};
middleware.guestSearchingAllowed = function(req, res, next) {
if (!req.user && parseInt(meta.config.allowGuestSearching, 10) !== 1) {
return controllers.helpers.notAllowed(req, res);
}
next();
};
middleware.checkGlobalPrivacySettings = function(req, res, next) {
if (!req.user && !!parseInt(meta.config.privateUserInfo, 10)) {
return controllers.helpers.notAllowed(req, res);
@@ -149,11 +128,11 @@ middleware.checkAccountPermissions = function(req, res, next) {
};
middleware.isAdmin = function(req, res, next) {
if (!req.user) {
return redirectToLogin(req, res);
if (!req.uid) {
return controllers.helpers.notAllowed(req, res);
}
user.isAdministrator((req.user && req.user.uid) ? req.user.uid : 0, function (err, isAdmin) {
user.isAdministrator(req.uid, function (err, isAdmin) {
if (err || isAdmin) {
return next(err);
}
@@ -168,219 +147,6 @@ middleware.isAdmin = function(req, res, next) {
});
};
middleware.buildHeader = function(req, res, next) {
res.locals.renderHeader = true;
res.locals.isAPI = false;
middleware.applyCSRF(req, res, function() {
async.parallel({
config: function(next) {
controllers.api.getConfig(req, res, next);
},
footer: function(next) {
app.render('footer', {loggedIn: (req.user ? parseInt(req.user.uid, 10) !== 0 : false)}, next);
},
plugins: function(next) {
plugins.fireHook('filter:middleware.buildHeader', {req: req, locals: res.locals}, next);
}
}, function(err, results) {
if (err) {
return next(err);
}
res.locals.config = results.config;
translator.translate(results.footer, results.config.defaultLang, function(parsedTemplate) {
res.locals.footer = parsedTemplate;
next();
});
});
});
};
middleware.renderHeader = function(req, res, data, callback) {
var registrationType = meta.config.registrationType || 'normal';
var templateValues = {
bootswatchCSS: meta.config['theme:src'],
title: meta.config.title || '',
description: meta.config.description || '',
'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '',
'brand:logo': meta.config['brand:logo'] || '',
'brand:logo:url': meta.config['brand:logo:url'] || '',
'brand:logo:alt': meta.config['brand:logo:alt'] || '',
'brand:logo:display': meta.config['brand:logo']?'':'hide',
allowRegistration: registrationType === 'normal' || registrationType === 'admin-approval',
searchEnabled: plugins.hasListeners('filter:search.query'),
config: res.locals.config,
relative_path: nconf.get('relative_path')
};
templateValues.configJSON = JSON.stringify(res.locals.config);
async.parallel({
customCSS: function(next) {
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1;
if (!templateValues.useCustomCSS || !meta.config.customCSS || !meta.config.renderedCustomCSS) {
return next(null, '');
}
next(null, meta.config.renderedCustomCSS);
},
customJS: function(next) {
templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1;
next(null, templateValues.useCustomJS ? meta.config.customJS : '');
},
settings: function(next) {
if (req.uid) {
user.getSettings(req.uid, next);
} else {
next();
}
},
title: function(next) {
next(null, controllers.helpers.buildTitle(data.title));
},
isAdmin: function(next) {
user.isAdministrator(req.uid, next);
},
user: function(next) {
if (req.uid) {
user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'icon:bgColor', 'icon:text', 'status', 'email:confirmed', 'banned'], next);
} else {
next(null, {
username: '[[global:guest]]',
userslug: '',
picture: meta.config.defaultAvatar,
status: 'offline',
banned: false,
uid: 0
});
}
},
navigation: async.apply(navigation.get),
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags)
}, function(err, results) {
if (err) {
return callback(err);
}
if (results.user && parseInt(results.user.banned, 10) === 1) {
req.logout();
return res.redirect('/');
}
results.user.isAdmin = results.isAdmin || false;
results.user.uid = parseInt(results.user.uid, 10);
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
if (results.settings && results.settings.bootswatchSkin && results.settings.bootswatchSkin !== 'default') {
templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + results.settings.bootswatchSkin + '/bootstrap.min.css';
}
templateValues.browserTitle = results.title;
templateValues.navigation = results.navigation;
templateValues.metaTags = results.tags.meta;
templateValues.linkTags = results.tags.link;
templateValues.isAdmin = results.user.isAdmin;
templateValues.user = results.user;
templateValues.userJSON = JSON.stringify(results.user);
templateValues.customCSS = results.customCSS;
templateValues.customJS = results.customJS;
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
templateValues.defaultLang = meta.config.defaultLang || 'en_GB';
templateValues.template = {name: res.locals.template};
templateValues.template[res.locals.template] = true;
if (req.route && req.route.path === '/') {
modifyTitle(templateValues);
}
plugins.fireHook('filter:middleware.renderHeader', {templateValues: templateValues, req: req, res: res}, function(err, data) {
if (err) {
return callback(err);
}
app.render('header', data.templateValues, callback);
});
});
};
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,
app = req.app,
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;
res.locals.template = template;
if ('function' !== typeof fn) {
fn = defaultFn;
}
if (res.locals.isAPI) {
if (req.route && req.route.path === '/api/') {
options.title = '[[pages:home]]';
}
return res.json(options);
}
var ajaxifyData = encodeURIComponent(JSON.stringify(options));
render.call(self, template, options, function(err, str) {
if (err) {
winston.error(err);
return fn(err);
}
str = str + '<input type="hidden" ajaxify-data="' + ajaxifyData + '" />';
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;

95
src/middleware/render.js Normal file
View File

@@ -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 + '<input type="hidden" ajaxify-data="' + ajaxifyData + '" />';
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(' ');
}
};

View File

@@ -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);
});
};

View File

@@ -2,8 +2,6 @@
var navigation = {},
plugins = require('../plugins'),
db = require('../database'),
admin = require('./admin'),
translator = require('../../public/src/modules/translator');

View File

@@ -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]]'));
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
});
};

View File

@@ -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);
};

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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);

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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

View File

@@ -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'),

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -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) {

View File

@@ -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]]'));

View File

@@ -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('&#44; ') + ']]'));
}
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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
});
});
};

View File

@@ -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();
});
};

View File

@@ -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));

View File

@@ -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) {

View File

@@ -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);
}

Some files were not shown because too many files have changed in this diff Show More