Merge branch 'develop'

v1.18.x
Julian Lam
commit c93f943bef

@ -1,30 +1,33 @@
services:
- mongodb
- redis-server
before_install:
- "sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6"
- "echo 'deb [ arch=amd64 ] http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.4 multiverse' | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list"
- "sudo apt-get update"
- "sudo apt-get install -y mongodb-org"
- "sudo service mongod start"
- cp install/package.json package.json
cache:
directories:
- node_modules
before_script:
- sleep 15 # wait for mongodb to be ready
- cp install/package.json package.json
- npm install
- sh -c "if [ '$DB' = 'mongodb' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"mongo\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":27017,\\\"database\\\":0}\"; fi"
- sh -c "if [ '$DB' = 'redis' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"redis\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":6379,\\\"database\\\":0}\"; fi"
after_success:
- "npm run coveralls"
language: node_js
sudo: false
dist: trusty
env:
- CXX=g++-4.8 DB=mongodb
- CXX=g++-4.8 DB=redis
addons:
apt:
sources:
- ubuntu-toolchain-r-test
- ubuntu-toolchain-r-test
- mongodb-3.4-trusty
packages:
- g++-4.8
- g++-4.8
- mongodb-org-server
node_js:
- "9"
- "8"
- "7"
- "6"

@ -39,6 +39,7 @@
"cropperjs": "^1.2.2",
"csurf": "^1.9.0",
"daemon": "^1.1.0",
"diff": "^3.4.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"express-useragent": "1.0.8",
@ -60,6 +61,7 @@
"mongodb": "2.2.33",
"morgan": "^1.9.0",
"mousetrap": "^1.6.1",
"mubsub": "^1.4.0",
"nconf": "^0.9.1",
"nodebb-plugin-composer-default": "6.0.16",
"nodebb-plugin-dbsearch": "2.0.9",
@ -91,10 +93,12 @@
"serve-favicon": "^2.4.5",
"sitemap": "^1.13.0",
"socket.io": "2.0.4",
"socket.io-adapter-mongo": "^2.0.1",
"socket.io-client": "2.0.4",
"socket.io-redis": "5.2.0",
"socketio-wildcard": "2.0.0",
"spdx-license-list": "^3.0.1",
"spider-detector": "1.0.18",
"toobusy-js": "^0.5.1",
"uglify-js": "^3.3.4",
"validator": "9.2.0",

@ -140,6 +140,8 @@
"cant-delete-chat-message": "You are not allowed to delete this message",
"chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting",
"chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting",
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored'already": "This chat message has already been restored.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Reputation system is disabled.",

@ -134,5 +134,7 @@
"edited": "Edited",
"disabled": "Disabled",
"select": "Select"
"select": "Select",
"user-search-prompt": "Type something here to find users..."
}

@ -1,5 +1,5 @@
{
"chat.chatting_with": "Chat with <span id=\"chat-with-name\"></span>",
"chat.chatting_with": "Chat with",
"chat.placeholder": "Type chat message here, press enter to send",
"chat.send": "Send",
"chat.no_active": "You have no active chats.",
@ -12,6 +12,7 @@
"chat.recent-chats": "Recent Chats",
"chat.contacts": "Contacts",
"chat.message-history": "Message History",
"chat.options": "Chat options",
"chat.pop-out": "Pop out chat",
"chat.minimize": "Minimize",
"chat.maximize": "Maximize",
@ -20,7 +21,17 @@
"chat.three_months": "3 Months",
"chat.delete_message_confirm": "Are you sure you wish to delete this message?",
"chat.add-users-to-room": "Add users to room",
"chat.retrieving-users": "Retrieving users...",
"chat.manage-room": "Manage Chat Room",
"chat.add-user-help": "Search for users here. When selected, the user will be added to the chat. The new user will not be able to see chat messages written before they were added to the conversation.",
"chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?",
"chat.rename-room": "Rename room",
"chat.rename-placeholder": "Enter your room name here",
"chat.rename-help": "The room name set here will be viewable by all participants in the room.",
"chat.leave": "Leave Chat",
"chat.leave-prompt": "Are you sure you wish to leave this chat?",
"chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.",
"chat.in-room": "In this room",
"composer.compose": "Compose",
"composer.show_preview": "Show Preview",

@ -35,6 +35,7 @@
"moved": "Moved",
"copy-ip": "Copy IP",
"ban-ip": "Ban IP",
"view-history": "Edit History",
"bookmark_instructions" : "Click here to return to the last read post in this thread.",
@ -143,5 +144,9 @@
"stale.create": "Create a new topic",
"stale.reply_anyway": "Reply to this topic anyway",
"link_back": "Re: [%1](%2)\n\n"
"link_back": "Re: [%1](%2)\n\n",
"diffs.title": "Post Edit History",
"diffs.description": "This post has <strong>%1</strong> revisions. Click one of the revisions below to see the post content at that point in time.",
"diffs.no-revisions-description": "This post has <strong>%1</strong> revisions."
}

@ -1,6 +1,7 @@
{
"banned": "Banned",
"offline": "Offline",
"deleted": "Deleted",
"username": "User Name",
"joindate": "Join Date",
"postcount": "Post Count",

@ -27,21 +27,19 @@ define('forum/chats', [
recentChats.init();
Chats.addEventListeners();
Chats.createTagsInput($('[component="chat/messages"] .users-tag-input'), ajaxify.data);
Chats.createAutoComplete($('[component="chat/input"]'));
components.get('expanded-chat/controlsToggle').on('click', function () {
components.get('expanded-chat/controls').toggleClass('hide');
});
Chats.resizeMainWindow();
if (env === 'md' || env === 'lg') {
Chats.resizeMainWindow();
Chats.addHotkeys();
}
messages.scrollToBottom($('.expanded-chat ul'));
$(document).ready(function () {
$(window).trigger('action:chat.loaded', $('.chats-full'));
});
Chats.initialised = true;
messages.scrollToBottom($('.expanded-chat ul.chat-content'));
search.init();
@ -52,7 +50,16 @@ define('forum/chats', [
Chats.addEventListeners = function () {
Chats.addSendHandlers(ajaxify.data.roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]'));
Chats.addPopoutHandler();
Chats.addActionHandlers(components.get('chat/messages'), ajaxify.data.roomId);
Chats.addMemberHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="members"]'));
Chats.addRenameHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="rename"]'));
Chats.addLeaveHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="leave"]'));
Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content'));
Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]'));
};
Chats.addPopoutHandler = function () {
$('[data-action="pop-out"]').on('click', function () {
var text = components.get('chat/input').val();
var roomId = ajaxify.data.roomId;
@ -70,12 +77,6 @@ define('forum/chats', [
components.get('chat/input').val(text);
});
});
Chats.addEditDeleteHandler(components.get('chat/messages'), ajaxify.data.roomId);
Chats.addRenameHandler(ajaxify.data.roomId, $('[component="chat/room/name"]'));
Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content'));
Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]'));
};
Chats.addScrollHandler = function (roomId, uid, el) {
@ -120,23 +121,35 @@ define('forum/chats', [
var element = parent.find('[component="chat/input"]');
element.on('keyup', function () {
parent.find('[component="chat/message/length"]').text(element.val().length);
parent.find('[component="chat/message/remaining"]').text(config.maximumChatMessageLength - element.val().length);
});
};
Chats.addEditDeleteHandler = function (element, roomId) {
element.on('click', '[data-action="edit"]', function () {
var messageId = $(this).parents('[data-mid]').attr('data-mid');
var inputEl = $('[data-roomid="' + roomId + '"] [component="chat/input"]');
messages.prepEdit(inputEl, messageId, roomId);
}).on('click', '[data-action="delete"]', function () {
Chats.addActionHandlers = function (element, roomId) {
element.on('click', '[data-action]', function () {
var messageId = $(this).parents('[data-mid]').attr('data-mid');
messages.delete(messageId, roomId);
var action = this.getAttribute('data-action');
switch (action) {
case 'edit':
var inputEl = $('[data-roomid="' + roomId + '"] [component="chat/input"]');
messages.prepEdit(inputEl, messageId, roomId);
break;
case 'delete':
messages.delete(messageId, roomId);
break;
case 'restore':
messages.restore(messageId, roomId);
break;
}
});
};
Chats.addHotkeys = function () {
mousetrap.bind('ctrl+up', function () {
var activeContact = $('.chats-list .bg-primary');
var activeContact = $('.chats-list .bg-info');
var prev = activeContact.prev();
if (prev.length) {
@ -144,7 +157,7 @@ define('forum/chats', [
}
});
mousetrap.bind('ctrl+down', function () {
var activeContact = $('.chats-list .bg-primary');
var activeContact = $('.chats-list .bg-info');
var next = activeContact.next();
if (next.length) {
@ -163,28 +176,126 @@ define('forum/chats', [
});
};
Chats.addRenameHandler = function (roomId, inputEl) {
var oldName = inputEl.val();
inputEl.on('blur keypress', function (ev) {
if (ev.type === 'keypress' && ev.keyCode !== 13) {
return;
}
var newName = inputEl.val();
Chats.addMemberHandler = function (roomId, buttonEl) {
var modal;
if (oldName === newName) {
return;
buttonEl.on('click', function () {
Benchpress.parse('partials/modals/manage_room', {}, function (html) {
translator.translate(html, function (html) {
modal = bootbox.dialog({
title: '[[modules:chat.manage-room]]',
message: html,
});
modal.attr('component', 'chat/manage-modal');
Chats.refreshParticipantsList(roomId, modal);
var searchInput = modal.find('input');
var errorEl = modal.find('.text-danger');
require(['autocomplete', 'translator'], function (autocomplete, translator) {
autocomplete.user(searchInput, function (event, selected) {
errorEl.text('');
socket.emit('modules.chats.addUserToRoom', {
roomId: roomId,
username: selected.item.user.name,
}, function (err) {
if (err) {
translator.translate(err.message, function (translated) {
errorEl.text(translated);
});
}
Chats.refreshParticipantsList(roomId, modal);
searchInput.val('');
});
});
});
});
});
});
};
Chats.addLeaveHandler = function (roomId, buttonEl) {
buttonEl.on('click', function () {
bootbox.confirm({
size: 'small',
title: '[[modules:chat.leave]]',
message: '<p>[[modules:chat.leave-prompt]]</p><p class="help-block">[[modules:chat.leave-help]]</p>',
callback: function (ok) {
if (ok) {
socket.emit('modules.chats.leave', roomId, function (err) {
if (err) {
app.alertError(err.message);
}
// Return user to chats page. If modal, close modal.
var modal = buttonEl.parents('.chat-modal');
if (modal.length) {
require(['chat'], function (chatLib) {
chatLib.close(modal);
});
} else {
ajaxify.go('chats');
}
});
}
},
});
});
};
Chats.refreshParticipantsList = function (roomId, modal) {
socket.emit('modules.chats.getUsersInRoom', { roomId: roomId }, function (err, users) {
var listEl = modal.find('.list-group');
if (err) {
return translator.translate('[[error:invalid-data]]', function (translated) {
listEl.find('li').text(translated);
});
}
Benchpress.parse('partials/modals/manage_room_users', {
users: users,
}, function (html) {
listEl.html(html);
});
});
};
Chats.addRenameHandler = function (roomId, buttonEl, roomName) {
var modal;
buttonEl.on('click', function () {
Benchpress.parse('partials/modals/rename_room', {
name: roomName || ajaxify.data.roomName,
}, function (html) {
translator.translate(html, function (html) {
modal = bootbox.dialog({
title: '[[modules:chat.rename-room]]',
message: html,
buttons: {
save: {
label: '[[global:save]]',
className: 'btn-primary',
callback: submit,
},
},
});
});
});
});
function submit() {
socket.emit('modules.chats.renameRoom', {
roomId: roomId,
newName: newName,
newName: modal.find('#roomName').val(),
}, function (err) {
if (err) {
return app.alertError(err.message);
}
oldName = newName;
inputEl.blur();
});
});
}
};
Chats.addSendHandlers = function (roomId, inputEl, sendEl) {
@ -222,75 +333,6 @@ define('forum/chats', [
}
};
Chats.createTagsInput = function (tagEl, data) {
tagEl.tagsinput({
confirmKeys: [13, 44],
trimValue: true,
});
if (data.users && data.users.length) {
data.users.forEach(function (user) {
tagEl.tagsinput('add', $('<div/>').html(user.username).text());
});
}
tagEl.on('beforeItemAdd', function (event) {
event.cancel = event.item === app.user.username;
});
tagEl.on('itemAdded', function (event) {
if (event.item === app.user.username) {
return;
}
socket.emit('modules.chats.addUserToRoom', {
roomId: data.roomId,
username: event.item,
}, function (err) {
if (err) {
app.alertError(err.message);
tagEl.tagsinput('remove', event.item, {
nouser: true,
});
}
});
});
tagEl.on('beforeItemRemove', function (event) {
if (event.options && event.options.nouser) {
return;
}
event.cancel = !data.isOwner || tagEl.tagsinput('items').length < 2;
if (!data.owner) {
return app.alertError('[[error:not-allowed]]');
}
if (tagEl.tagsinput('items').length < 2) {
return app.alertError('[[error:cant-remove-last-user]]');
}
});
tagEl.on('itemRemoved', function (event) {
if (event.options && event.options.nouser) {
return;
}
socket.emit('modules.chats.removeUserFromRoom', {
roomId: data.roomId,
username: event.item,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
});
});
var input = $('.users-tag-container').find('.bootstrap-tagsinput input');
require(['autocomplete'], function (autocomplete) {
autocomplete.user(input);
});
};
Chats.leave = function (el) {
var roomId = el.attr('data-roomid');
socket.emit('modules.chats.leave', roomId, function (err) {
@ -320,11 +362,13 @@ define('forum/chats', [
response.json().then(function (payload) {
app.parseAndTranslate('partials/chats/message-window', payload, function (html) {
components.get('chat/main-wrapper').html(html);
html.find('.timeago').timeago();
Chats.resizeMainWindow();
ajaxify.data = payload;
Chats.setActive();
Chats.addEventListeners();
messages.scrollToBottom($('.expanded-chat ul'));
$(window).trigger('action:chat.loaded', $('.chats-full'));
messages.scrollToBottom($('.expanded-chat ul.chat-content'));
if (history.pushState) {
history.pushState({
url: 'user/' + payload.userslug + '/chats/' + payload.roomId,
@ -361,7 +405,7 @@ define('forum/chats', [
data.message.self = data.self;
messages.appendChatMessage($('.expanded-chat .chat-content'), data.message);
} else if (ajaxify.currentPage.startsWith('chats')) {
} else if (ajaxify.data.template.chats) {
var roomEl = $('[data-roomid=' + data.roomId + ']');
if (roomEl.length > 0) {
@ -391,26 +435,18 @@ define('forum/chats', [
messages.onChatMessageEdit();
socket.on('event:chats.roomRename', function (data) {
$('[component="chat/room/name"]').val($('<div/>').html(data.newName).text());
var roomEl = components.get('chat/recent/room', data.roomId);
var titleEl = roomEl.find('[component="chat/title"]');
ajaxify.data.roomName = data.newName;
titleEl.text(data.newName);
});
};
Chats.resizeMainWindow = function () {
var messagesList = $('.expanded-chat .chat-content');
var searchHeight = $('.chat-search').height();
var searchListHeight = $('[component="chat/search/list"]').outerHeight(true) - $('[component="chat/search/list"]').height();
var fromTop = components.get('chat/recent').offset().top;
if (messagesList.length) {
var margin = $('.expanded-chat ul').outerHeight(true) - $('.expanded-chat ul').height();
var inputHeight = $('.chat-input').outerHeight(true);
messagesList.height($(window).height() - (fromTop + inputHeight + (margin * 4)));
components.get('chat/recent').height($('.expanded-chat').height() - (searchHeight + searchListHeight));
$('[component="chat/search/list"]').css('max-height', (components.get('chat/recent').height() / 2) + 'px');
} else {
components.get('chat/recent').height($(window).height() - (fromTop + searchHeight + searchListHeight));
}
var viewportHeight = $(window).height();
var fromTop = components.get('chat/main-wrapper').offset().top || components.get('chat/nav-wrapper').offset().top;
$('.chats-full').height(viewportHeight - fromTop - 1);
Chats.setActive();
};
@ -418,10 +454,13 @@ define('forum/chats', [
Chats.setActive = function () {
if (ajaxify.data.roomId) {
socket.emit('modules.chats.markRead', ajaxify.data.roomId);
$('[data-roomid="' + ajaxify.data.roomId + '"]').toggleClass('unread', false);
$('.expanded-chat input').focus();
}
$('.chats-list li').removeClass('bg-primary');
$('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-primary');
$('.chats-list li').removeClass('bg-info');
$('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-info');
components.get('chat/nav-wrapper').attr('data-loaded', ajaxify.data.roomId ? '1' : '0');
};

@ -82,6 +82,10 @@ define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpres
newMessage.find('.timeago').timeago();
newMessage.find('img:not(.not-responsive)').addClass('img-responsive');
messages.scrollToBottom(chatContentEl);
$(window).trigger('action:chat.received', {
messageEl: newMessage,
});
}
@ -146,13 +150,24 @@ define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpres
return app.alertError(err.message);
}
components.get('chat/message', messageId).slideUp('slow', function () {
$(this).remove();
});
components.get('chat/message', messageId).toggleClass('deleted', true);
});
});
});
};
messages.restore = function (messageId, roomId) {
socket.emit('modules.chats.restore', {
messageId: messageId,
roomId: roomId,
}, function (err) {
if (err) {
return app.alertError(err.message);
}
components.get('chat/message', messageId).toggleClass('deleted', false);
});
};
return messages;
});

@ -6,18 +6,8 @@ define('forum/chats/recent', function () {
recent.init = function () {
require(['forum/chats'], function (Chats) {
$('[component="chat/recent"]').on('click', '[component="chat/leave"]', function () {
Chats.leave($(this).parents('[data-roomid]'));
return false;
});
$('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function () {
var env = utils.findBootstrapEnvironment();
if (env === 'xs' || env === 'sm') {
app.openChat($(this).attr('data-roomid'));
} else {
Chats.switchChat($(this).attr('data-roomid'));
}
Chats.switchChat($(this).attr('data-roomid'));
});
$('[component="chat/recent"]').on('scroll', function () {

@ -52,6 +52,8 @@ define('forum/chats/search', ['components'], function (components) {
var chatEl = displayUser(chatsListEl, userObj);
onUserClick(chatEl, userObj);
});
chatsListEl.parent().toggleClass('open', true);
}
function displayUser(chatsListEl, userObj) {

@ -0,0 +1,65 @@
'use strict';
define('forum/topic/diffs', ['benchpress', 'translator'], function (Benchpress, translator) {
var Diffs = {};
Diffs.open = function (pid) {
var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' };
socket.emit('posts.getDiffs', { pid: pid }, function (err, timestamps) {
if (err) {
return app.alertError(err.message);
}
Benchpress.parse('partials/modals/post_history', {
diffs: timestamps.map(function (timestamp) {
return {
timestamp: timestamp,
pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts),
};
}),
numDiffs: timestamps.length,
}, function (html) {
translator.translate(html, function (html) {
var modal = bootbox.dialog({
title: '[[topic:diffs.title]]',
message: html,
});
if (!timestamps.length) {
return;
}
var selectEl = modal.find('select');
var postContainer = modal.find('ul.posts-list');
selectEl.on('change', function () {
Diffs.load(pid, this.value, postContainer);
});
modal.on('shown.bs.modal', function () {
Diffs.load(pid, selectEl.val(), postContainer);
});
});
});
});
};
Diffs.load = function (pid, since, postContainer) {
socket.emit('posts.showPostAt', { pid: pid, since: since }, function (err, data) {
if (err) {
return app.alertError(err.message);
}
data.deleted = !!parseInt(data.deleted, 10);
app.parseAndTranslate('partials/posts_list', 'posts', {
posts: [data],
}, function (html) {
postContainer.empty().append(html);
});
});
};
return Diffs;
});

@ -8,7 +8,8 @@ define('forum/topic/postTools', [
'translator',
'forum/topic/votes',
'forum/topic/move-post',
], function (share, navigator, components, translator, votes, movePost) {
'forum/topic/diffs',
], function (share, navigator, components, translator, votes, movePost, diffs) {
var PostTools = {};
var staleReplyAnyway = false;
@ -139,6 +140,11 @@ define('forum/topic/postTools', [
}
});
postContainer.on('click', '[component="post/view-history"], [component="post/edit-indicator"]', function () {
var btn = $(this);
diffs.open(getData(btn, 'data-pid'));
});
postContainer.on('click', '[component="post/delete"]', function () {
var btn = $(this);
var timestamp = parseInt(getData(btn, 'data-timestamp'), 10);

@ -10,7 +10,7 @@ define('autocomplete', function () {
input.autocomplete({
delay: 200,
open: function () {
$(this).autocomplete('widget').css('z-index', 20000);
$(this).autocomplete('widget').css('z-index', 100005);
},
select: onselect,
source: function (request, response) {

@ -61,7 +61,7 @@ define('chat', [
if (modal.is(':visible')) {
taskbar.updateActive(modal.attr('UUID'));
ChatsMessages.scrollToBottom(modal.find('.chat-content'));
} else {
} else if (!ajaxify.data.template.chats) {
module.toggleNew(modal.attr('UUID'), true, true);
}
@ -75,7 +75,7 @@ define('chat', [
roomId: data.roomId,
});
}
} else {
} else if (!ajaxify.data.template.chats) {
socket.emit('modules.chats.loadRoom', {
roomId: data.roomId,
}, function (err, roomData) {
@ -106,7 +106,7 @@ define('chat', [
socket.on('event:chats.roomRename', function (data) {
var newTitle = $('<div/>').html(data.newName).text();
var modal = module.getModal(data.roomId);
modal.find('[component="chat/room/name"]').val(newTitle);
modal.find('[component="chat/room/name"]').text(newTitle);
taskbar.updateTitle('chat', modal.attr('UUID'), newTitle);
});
@ -190,7 +190,7 @@ define('chat', [
return;
}
chatModal.find('.chat-content').css('height', module.calculateChatListHeight(chatModal));
chatModal.find('.modal-body').css('height', module.calculateChatListHeight(chatModal));
});
chatModal.draggable({
@ -249,20 +249,11 @@ define('chat', [
}
});
Chats.addEditDeleteHandler(chatModal.find('[component="chat/messages"]'), data.roomId);
chatModal.find('[component="chat/controlsToggle"]').on('click', function () {
var messagesEl = chatModal.find('[component="chat/messages"]');
chatModal.find('[component="chat/controls"]').toggle();
messagesEl.css('height', module.calculateChatListHeight(chatModal));
});
Chats.addActionHandlers(chatModal.find('[component="chat/messages"]'), data.roomId);
Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="rename"]'), chatModal.attr('data-name'));
Chats.addLeaveHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="leave"]'));
Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('.chat-input'), chatModal.find('[data-action="send"]'));
Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[component="chat/room/name"]'));
Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn'));
Chats.createTagsInput(chatModal.find('.users-tag-input'), data);
Chats.createAutoComplete(chatModal.find('[component="chat/input"]'));
Chats.addScrollHandler(chatModal.attr('data-roomid'), data.uid, chatModal.find('.chat-content'));
@ -333,7 +324,7 @@ define('chat', [
module.enableMobileBehaviour = function (modalEl) {
app.toggleNavbar(false);
modalEl.attr('data-mobile', '1');
var messagesEl = modalEl.find('.chat-content');
var messagesEl = modalEl.find('.modal-body');
messagesEl.css('height', module.calculateChatListHeight(modalEl));
$(window).on('resize', function () {
@ -346,12 +337,8 @@ define('chat', [
};
module.calculateChatListHeight = function (modalEl) {
var totalHeight = modalEl.find('.modal-content').outerHeight() - modalEl.find('.modal-header').outerHeight();
var padding = parseInt(modalEl.find('.modal-body').css('padding-top'), 10) + parseInt(modalEl.find('.modal-body').css('padding-bottom'), 10);
var contentMargin = parseInt(modalEl.find('.chat-content').css('margin-top'), 10) + parseInt(modalEl.find('.chat-content').css('margin-bottom'), 10);
var inputGroupHeight = modalEl.find('.input-group').outerHeight();
return totalHeight - padding - contentMargin - inputGroupHeight;
// Formula: modal height minus header height. Simple(tm).
return modalEl.find('.modal-content').outerHeight() - modalEl.find('.modal-header').outerHeight();
};
module.minimize = function (uuid) {

@ -50,9 +50,14 @@ define('components', function () {
'chat/message': function (messageId) {
return $('[component="chat/message"][data-mid="' + messageId + '"]');
},
'chat/message/body': function (messageId) {
return $('[component="chat/message"][data-mid="' + messageId + '"] [component="chat/message/body"]');
},
'chat/recent/room': function (roomid) {
return $('[component="chat/recent/room"][data-roomid="' + roomid + '"]');
},
};
components.get = function () {

@ -457,19 +457,6 @@ define('settings', function () {
return callback(err);
}
// Parse all values. If they are json, return json
for (var key in values) {
if (values.hasOwnProperty(key)) {
try {
if (!utils.isNumber(values[key])) {
values[key] = JSON.parse(values[key]);
}
} catch (e) {
// Leave the value as is
}
}
}
// Save loaded settings into ajaxify.data for use client-side
ajaxify.data.settings = values;
@ -500,12 +487,6 @@ define('settings', function () {
}
});
// Normalizing value of multiple selects
formEl.find('select[multiple]').each(function (idx, selectEl) {
selectEl = $(selectEl);
values[selectEl.attr('name')] = JSON.stringify(selectEl.val());
});
socket.emit('admin.settings.set', {
hash: hash,
values: values,

@ -97,7 +97,7 @@ chatsController.get = function (req, res, callback) {
chatsController.redirectToChat = function (req, res, next) {
var roomid = parseInt(req.params.roomid, 10);
if (!req.uid) {
if (!req.loggedIn) {
return next();
}
async.waterfall([

@ -50,11 +50,26 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
ips: function (next) {
user.getIPs(uid, 4, next);
},
profile_links: function (next) {
profile_links: function (next) { // DEPRECATED, do not use
plugins.fireHook('filter:user.profileLinks', [], next);
},
profile_menu: function (next) {
plugins.fireHook('filter:user.profileMenu', { uid: uid, callerUID: callerUID, links: [] }, next);
plugins.fireHook('filter:user.profileMenu', {
uid: uid,
callerUID: callerUID,
links: [{
id: 'info',
route: 'info',
name: '[[user:account_info]]',
visibility: {
self: false,
other: false,
moderator: true,
globalMod: true,
admin: true,
},
}],
}, next);
},
groups: function (next) {
groups.getUserGroups([uid], next);

@ -13,7 +13,7 @@ categoriesController.get = function (req, res, callback) {
async.waterfall([
function (next) {
async.parallel({
category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid),
category: async.apply(categories.getCategories, [req.params.category_id], req.uid),
allCategories: async.apply(categories.buildForSelect, req.uid, 'read'),
}, next);
},

@ -253,7 +253,7 @@ function uploadImage(filename, folder, uploadedFile, req, res, next) {
async.waterfall([
function (next) {
if (plugins.hasListeners('filter:uploadImage')) {
plugins.fireHook('filter:uploadImage', { image: uploadedFile, uid: req.user.uid }, next);
plugins.fireHook('filter:uploadImage', { image: uploadedFile, uid: req.uid }, next);
} else {
file.saveFileToLocal(filename, folder, uploadedFile.path, next);
}

@ -191,7 +191,7 @@ usersController.getCSV = function (req, res, next) {
}
events.log({
type: 'getUsersCSV',
uid: req.user.uid,
uid: req.uid,
ip: req.ip,
});
async.waterfall([

@ -18,6 +18,7 @@ var apiController = module.exports;
apiController.loadConfig = function (req, callback) {
var config = {};
config.relative_path = nconf.get('relative_path');
config.upload_url = nconf.get('upload_url');
config.siteTitle = validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB'));
config.browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB'));
config.titleLayout = (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '&#123;').replace(/}/g, '&#125;');
@ -39,6 +40,7 @@ apiController.loadConfig = function (req, callback) {
config.usePagination = parseInt(meta.config.usePagination, 10) === 1;
config.disableChat = parseInt(meta.config.disableChat, 10) === 1;
config.disableChatMessageEditing = parseInt(meta.config.disableChatMessageEditing, 10) === 1;
config.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000;
config.socketioTransports = nconf.get('socket.io:transports') || ['polling', 'websocket'];
config.websocketAddress = nconf.get('socket.io:address') || '';
config.maxReconnectionAttempts = meta.config.maxReconnectionAttempts || 5;
@ -76,7 +78,7 @@ apiController.loadConfig = function (req, callback) {
async.waterfall([
function (next) {
if (!req.uid) {
if (!req.loggedIn) {
return next(null, config);
}
user.getSettings(req.uid, next);

@ -417,7 +417,7 @@ authenticationController.localLogin = function (req, username, password, next) {
};
authenticationController.logout = function (req, res, next) {
if (!req.uid || !req.sessionID) {
if (!req.loggedIn || !req.sessionID) {
return res.status(200).send('not-logged-in');
}

@ -135,6 +135,7 @@ categoryController.get = function (req, res, callback) {
addTags(categoryData, res);
categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
categoryData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
categoryData.title = translator.escape(categoryData.name);
pageCount = Math.max(1, Math.ceil(categoryData.topic_count / settings.topicsPerPage));
categoryData.pagination = pagination.create(currentPage, pageCount, req.query);

@ -24,7 +24,7 @@ helpers.noScriptErrors = function (req, res, error, httpStatus) {
middleware.buildHeader(req, res, function () {
res.status(httpStatus).render(httpStatusString, {
path: req.path,
loggedIn: true,
loggedIn: req.loggedIn,
error: error,
returnLink: true,
title: '[[global:' + httpStatusString + '.title]]',
@ -67,11 +67,11 @@ helpers.notAllowed = function (req, res, error) {
if (err) {
return winston.error(err);
}
if (req.uid) {
if (req.loggedIn) {
if (res.locals.isAPI) {
res.status(403).json({
path: req.path.replace(/^\/api/, ''),
loggedIn: !!req.uid,
loggedIn: req.loggedIn,
error: error,
title: '[[global:403.title]]',
});
@ -79,7 +79,7 @@ helpers.notAllowed = function (req, res, error) {
middleware.buildHeader(req, res, function () {
res.status(403).render('403', {
path: req.path,
loggedIn: !!req.uid,
loggedIn: req.loggedIn,
error: error,
title: '[[global:403.title]]',
});

@ -113,7 +113,7 @@ Controllers.login = function (req, res, next) {
}
return res.redirect(nconf.get('relative_path') + data.authentication[0].url);
}
if (req.uid) {
if (req.loggedIn) {
user.getUserFields(req.uid, ['username', 'email'], function (err, user) {
if (err) {
return next(err);

@ -37,7 +37,7 @@ popularController.get = function (req, res, next) {
alltime: '[[global:header.popular]]',
};
if (!req.uid) {
if (!req.loggedIn) {
if (anonCache[term] && (Date.now() - lastUpdateTime) < 60 * 60 * 1000) {
return res.render('popular', anonCache[term]);
}
@ -73,7 +73,7 @@ popularController.get = function (req, res, next) {
data.breadcrumbs = helpers.buildBreadcrumbs(breadcrumbs);
}
if (!req.uid) {
if (!req.loggedIn) {
anonCache[term] = data;
lastUpdateTime = Date.now();
}

@ -58,7 +58,7 @@ recentController.get = function (req, res, next) {
data.set = 'topics:recent';
data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss';
if (req.uid) {
if (req.loggedIn) {
data.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken;
}
data.title = meta.config.homePageTitle || '[[pages:home]]';

@ -11,15 +11,14 @@ var categories = require('../categories');
var pagination = require('../pagination');
var helpers = require('./helpers');
var searchController = {};
var searchController = module.exports;
searchController.search = function (req, res, next) {
if (!plugins.hasListeners('filter:search.query')) {
return next();
}
if (!req.user && parseInt(meta.config.allowGuestSearching, 10) !== 1) {
if (!req.loggedIn && parseInt(meta.config.allowGuestSearching, 10) !== 1) {
return helpers.notAllowed(req, res);
}
@ -78,5 +77,3 @@ searchController.search = function (req, res, next) {
res.render('search', searchData);
});
};
module.exports = searchController;

@ -58,7 +58,7 @@ topController.get = function (req, res, next) {
data.set = 'topics:votes';
data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
data.rssFeedUrl = nconf.get('relative_path') + '/top.rss';
if (req.uid) {
if (req.loggedIn) {
data.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken;
}
data.title = meta.config.homePageTitle || '[[pages:home]]';

@ -146,7 +146,7 @@ topicsController.get = function (req, res, callback) {
topicData.postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10) || 0;
topicData.scrollToMyPost = settings.scrollToMyPost;
topicData.rssFeedUrl = nconf.get('relative_path') + '/topic/' + topicData.tid + '.rss';
if (req.uid) {
if (req.loggedIn) {
topicData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken;
}
@ -165,7 +165,7 @@ topicsController.get = function (req, res, callback) {
req.session.tids_viewed[tid] = Date.now();
}
if (req.uid) {
if (req.loggedIn) {
topics.markAsRead([tid], req.uid, function (err, markedRead) {
if (err) {
return callback(err);

@ -9,7 +9,7 @@ var accountHelpers = require('./accounts/helpers');
var userController = module.exports;
userController.getCurrentUser = function (req, res, next) {
if (!req.uid) {
if (!req.loggedIn) {
return res.status(401).json('not-authorized');
}
async.waterfall([

@ -61,11 +61,7 @@ mongoModule.questions = [
mongoModule.helpers = mongoModule.helpers || {};
mongoModule.helpers.mongo = require('./mongo/helpers');
mongoModule.init = function (callback) {
callback = callback || function () { };
var mongoClient = require('mongodb').MongoClient;
function getConnectionString() {
var usernamePassword = '';
if (nconf.get('mongo:username') && nconf.get('mongo:password')) {
usernamePassword = nconf.get('mongo:username') + ':' + encodeURIComponent(nconf.get('mongo:password')) + '@';
@ -92,7 +88,15 @@ mongoModule.init = function (callback) {
servers.push(hosts[i] + ':' + ports[i]);
}
var connString = nconf.get('mongo:uri') || 'mongodb://' + usernamePassword + servers.join() + '/' + nconf.get('mongo:database');
return nconf.get('mongo:uri') || 'mongodb://' + usernamePassword + servers.join() + '/' + nconf.get('mongo:database');
}
mongoModule.init = function (callback) {
callback = callback || function () { };
var mongoClient = require('mongodb').MongoClient;
var connString = getConnectionString();
var connOptions = {
poolSize: 10,
@ -262,3 +266,8 @@ mongoModule.close = function (callback) {
callback = callback || function () {};
db.close(callback);
};
mongoModule.socketAdapter = function () {
var mongoAdapter = require('socket.io-adapter-mongo');
return mongoAdapter(getConnectionString());
};

@ -1,11 +1,12 @@
'use strict';
var pubsub = require('../../pubsub');
module.exports = function (db, module) {
var helpers = module.helpers.mongo;
var LRU = require('lru-cache');
var _ = require('lodash');
var pubsub = require('../../pubsub');
var cache = LRU({
max: 10000,
@ -35,6 +36,7 @@ module.exports = function (db, module) {
cache.reset();
};
module.setObject = function (key, data, callback) {
callback = callback || helpers.noop;
if (!key || !data) {

@ -0,0 +1,8 @@
'use strict';
var mubsub = require('mubsub');
var db = require('../mongo');
var client = mubsub(db.client);
module.exports = client.channel('pubsub');

@ -203,25 +203,36 @@ module.exports = function (db, module) {
};
module.sortedSetRank = function (key, value, callback) {
getSortedSetRank(module.getSortedSetRange, key, value, callback);
getSortedSetRank(false, key, value, callback);
};
module.sortedSetRevRank = function (key, value, callback) {
getSortedSetRank(module.getSortedSetRevRange, key, value, callback);
getSortedSetRank(true, key, value, callback);
};
function getSortedSetRank(method, key, value, callback) {
function getSortedSetRank(reverse, key, value, callback) {
if (!key) {
return callback();
}
value = helpers.valueToString(value);
method(key, 0, -1, function (err, result) {
if (err) {
return callback(err);
module.sortedSetScore(key, value, function (err, score) {
if (err || score === null) {
return callback(err, null);
}
var rank = result.indexOf(value);
callback(null, rank !== -1 ? rank : null);
db.collection('objects').count({
$or: [
{
_key: key,
score: reverse ? { $gt: score } : { $lt: score },
},
{
_key: key,
score: score,
value: reverse ? { $gt: value } : { $lt: value },
},
],
}, function (err, rank) { callback(err, rank); });
});
}
@ -235,7 +246,7 @@ module.exports = function (db, module) {
}
async.map(data, function (item, next) {
getSortedSetRank(module.getSortedSetRange, item.key, item.value, next);
getSortedSetRank(false, item.key, item.value, next);
}, callback);
};

@ -169,5 +169,16 @@ redisModule.info = function (cxn, callback) {
], callback);
};
redisModule.socketAdapter = function () {
var redisAdapter = require('socket.io-redis');
var pub = redisModule.connect();
var sub = redisModule.connect();
return redisAdapter({
key: 'db:' + nconf.get('redis:database') + ':adapter_key',
pubClient: pub,
subClient: sub,
});
};
redisModule.helpers = redisModule.helpers || {};
redisModule.helpers.redis = require('./redis/helpers');

@ -0,0 +1,39 @@
'use strict';
var nconf = require('nconf');
var util = require('util');
var winston = require('winston');
var EventEmitter = require('events').EventEmitter;
var channelName;
var PubSub = function () {
var self = this;
var db = require('../redis');
var subClient = db.connect();
this.pubClient = db.connect();
channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel';
subClient.subscribe(channelName);
subClient.on('message', function (channel, message) {
if (channel !== channelName) {
return;
}
try {
var msg = JSON.parse(message);
self.emit(msg.event, msg.data);
} catch (err) {
winston.error(err.stack);
}
});
};
util.inherits(PubSub, EventEmitter);
PubSub.prototype.publish = function (event, data) {
this.pubClient.publish(channelName, JSON.stringify({ event: event, data: data }));
};
module.exports = new PubSub();

@ -22,7 +22,7 @@ require('./groups/posts')(Groups);
require('./groups/user')(Groups);
Groups.ephemeralGroups = ['guests'];
Groups.ephemeralGroups = ['guests', 'spiders'];
Groups.getEphemeralGroup = function (groupName) {
return {

@ -56,6 +56,16 @@ Messaging.getMessages = function (params, callback) {
messageData.forEach(function (messageData) {
messageData.index = indices[messageData.messageId.toString()];
});
// Filter out deleted messages unless you're the sender of said message
messageData = messageData.filter(function (messageData) {
if (messageData.deleted && parseInt(messageData.fromuid, 10) !== parseInt(params.uid, 10)) {
return false;
}
return true;
});
next(null, messageData);
},
], callback);
@ -72,9 +82,6 @@ function canGet(hook, callerUid, uid, callback) {
}
Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) {
message = utils.decodeHTMLEntities(utils.stripHTMLTags(message));
message = validator.escape(String(message));
plugins.fireHook('filter:parse.raw', message, function (err, parsed) {
if (err) {
return callback(err);

@ -58,6 +58,7 @@ module.exports = function (Messaging) {
timestamp: timestamp,
fromuid: fromuid,
roomId: roomId,
deleted: 0,
};
plugins.fireHook('filter:messaging.save', message, next);

@ -51,11 +51,14 @@ module.exports = function (Messaging) {
return msg && msg.fromuid;
});
user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status'], next);
user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'banned'], next);
},
function (users, next) {
messages.forEach(function (message, index) {
message.fromUser = users[index];
message.fromUser.banned = !!parseInt(message.fromUser.banned, 10);
message.fromUser.deleted = parseInt(message.fromuid, 10) !== message.fromUser.uid && message.fromUser.uid === 0;
var self = parseInt(message.fromuid, 10) === parseInt(uid, 10);
message.self = self ? 1 : 0;
message.timestampISO = utils.toISOString(message.timestamp);
@ -64,6 +67,8 @@ module.exports = function (Messaging) {
if (message.hasOwnProperty('edited')) {
message.editedISO = new Date(parseInt(message.edited, 10)).toISOString();
}
message.deleted = !!parseInt(message.deleted, 10);
});
async.map(messages, function (message, next) {

@ -1,25 +1,30 @@
'use strict';
var async = require('async');
var db = require('../database');
module.exports = function (Messaging) {
Messaging.deleteMessage = function (mid, roomId, callback) {
async.waterfall([
function (next) {
Messaging.getUidsInRoom(roomId, 0, -1, next);
},
function (uids, next) {
if (!uids.length) {
return next();
async.apply(Messaging.getMessageField, mid, 'deleted'),
function (deleted, next) {
if (parseInt(deleted, 10)) {
return next(new Error('[[error:chat-deleted-already]]'));
}
var keys = uids.map(function (uid) {
return 'uid:' + uid + ':chat:room:' + roomId + ':mids';
});
db.sortedSetsRemove(keys, mid, next);
Messaging.setMessageField(mid, 'deleted', 1, next);
},
function (next) {
db.delete('message:' + mid, next);
], callback);
};
Messaging.restoreMessage = function (mid, roomId, callback) {
async.waterfall([
async.apply(Messaging.getMessageField, mid, 'deleted'),
function (deleted, next) {
if (!parseInt(deleted, 10)) {
return next(new Error('[[error:chat-restored-already]]'));
}
Messaging.setMessageField(mid, 'deleted', 0, next);
},
], callback);
};

@ -83,7 +83,7 @@ module.exports = function (middleware) {
},
user: function (next) {
var userData = {
uid: 0,
uid: req.uid,
username: '[[global:guest]]',
userslug: '',
fullname: '[[global:guest]]',
@ -93,7 +93,7 @@ module.exports = function (middleware) {
reputation: 0,
'email:confirmed': 0,
};
if (req.uid) {
if (req.loggedIn) {
user.getUserFields(req.uid, Object.keys(userData), next);
} else {
next(null, userData);

@ -59,12 +59,12 @@ middleware.pageView = function (req, res, next) {
plugins.fireHook('action:middleware.pageView', { req: req });
if (req.user) {
user.updateLastOnlineTime(req.user.uid);
if (req.loggedIn) {
user.updateLastOnlineTime(req.uid);
if (req.path.startsWith('/api/users') || req.path.startsWith('/users')) {
user.updateOnlineUsers(req.user.uid, next);
user.updateOnlineUsers(req.uid, next);
} else {
user.updateOnlineUsers(req.user.uid);
user.updateOnlineUsers(req.uid);
next();
}
} else {
@ -112,7 +112,7 @@ middleware.routeTouchIcon = function (req, res) {
};
middleware.privateTagListing = function (req, res, next) {
if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) {
if (!req.loggedIn && parseInt(meta.config.privateTagListing, 10) === 1) {
controllers.helpers.notAllowed(req, res);
} else {
next();
@ -143,7 +143,7 @@ function expose(exposedField, method, field, req, res, next) {
}
middleware.privateUploads = function (req, res, next) {
if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) {
if (req.loggedIn || parseInt(meta.config.privateUploads, 10) !== 1) {
return next();
}
if (req.path.startsWith(nconf.get('relative_path') + '/assets/uploads/files')) {

@ -14,7 +14,7 @@ var controllers = {
module.exports = function (middleware) {
middleware.authenticate = function (req, res, next) {
if (req.uid) {
if (req.loggedIn) {
return next();
}
@ -44,7 +44,7 @@ module.exports = function (middleware) {
*/
async.waterfall([
function (next) {
if (!req.uid) {
if (!req.loggedIn) {
return setImmediate(next, null, false);
}
@ -64,7 +64,7 @@ module.exports = function (middleware) {
}
middleware.checkGlobalPrivacySettings = function (req, res, next) {
if (!req.uid && !!parseInt(meta.config.privateUserInfo, 10)) {
if (!req.loggedIn && !!parseInt(meta.config.privateUserInfo, 10)) {
return middleware.authenticate(req, res, next);
}
@ -202,7 +202,7 @@ module.exports = function (middleware) {
};
middleware.requireUser = function (req, res, next) {
if (req.uid) {
if (req.loggedIn) {
return next();
}

@ -25,6 +25,7 @@ require('./posts/tools')(Posts);
require('./posts/votes')(Posts);
require('./posts/bookmarks')(Posts);
require('./posts/queue')(Posts);
require('./posts/diffs')(Posts);
Posts.exists = function (pid, callback) {
db.isSortedSetMember('posts:pid', pid, callback);

@ -0,0 +1,77 @@
'use strict';
var async = require('async');
var validator = require('validator');
var diff = require('diff');
var db = require('../database');
var plugins = require('../plugins');
var translator = require('../translator');
module.exports = function (Posts) {
Posts.diffs = {};
Posts.diffs.exists = function (pid, callback) {
db.sortedSetCard('post:' + pid + ':diffs', function (err, numDiffs) {
return callback(err, numDiffs > 0);
});
};
Posts.diffs.list = function (pid, callback) {
db.getSortedSetRangeWithScores('post:' + pid + ':diffs', 0, -1, function (err, diffs) {
callback(err, diffs ? diffs.map(function (diffObj) {
return diffObj.score;
}).reverse() : null);
});
};
Posts.diffs.save = function (pid, oldContent, newContent, callback) {
db.sortedSetAdd('post:' + pid + ':diffs', Date.now(), diff.createPatch('', newContent, oldContent), callback);
};
Posts.diffs.load = function (pid, since, uid, callback) {
// Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since`
since = parseInt(since, 10);
if (isNaN(since) || since > Date.now()) {
return callback(new Error('[[error:invalid-data]]'));
}
async.parallel({
post: async.apply(Posts.getPostSummaryByPids, [pid], uid, {
parse: false,
}),
diffs: async.apply(db.getSortedSetRangeByScore.bind(db), 'post:' + pid + ':diffs', 0, -1, since, Date.now()),
}, function (err, data) {
if (err) {
return callback(err);
}
data.post = data.post[0];
data.post.content = validator.unescape(data.post.content);
// Replace content with re-constructed content from that point in time
data.post.content = data.diffs.reverse().reduce(function (content, diffString) {
return diff.applyPatch(content, diffString, {
fuzzFactor: 1,
});
}, data.post.content);
// Clear editor data (as it is outdated for this content)
delete data.post.edited;
data.post.editor = null;
data.post.content = String(data.post.content || '');
async.waterfall([
function (next) {
plugins.fireHook('filter:parse.post', { postData: data.post }, next);
},
function (data, next) {
data.postData.content = translator.escape(data.postData.content);
next(null, data.postData);
},
], callback);
});
};
};

@ -20,6 +20,7 @@ module.exports = function (Posts) {
});
Posts.edit = function (data, callback) {
var oldContent; // for diffing purposes
var postData;
var results;
@ -39,6 +40,7 @@ module.exports = function (Posts) {
}
postData = _postData;
oldContent = postData.content;
postData.content = data.content;
postData.edited = Date.now();
postData.editor = data.uid;
@ -63,6 +65,9 @@ module.exports = function (Posts) {
results = _results;
Posts.setPostFields(data.pid, postData, next);
},
function (next) {
Posts.diffs.save(data.pid, oldContent, data.content, next);
},
function (next) {
postData.cid = results.topic.cid;
postData.topic = results.topic;

@ -10,6 +10,11 @@ var plugins = require('../plugins');
var helpers = module.exports;
var uidToSystemGroup = {
0: 'guests',
'-1': 'spiders',
};
helpers.some = function (tasks, callback) {
async.some(tasks, function (task, next) {
task(next);
@ -27,8 +32,8 @@ helpers.isUserAllowedTo = function (privilege, uid, cid, callback) {
};
function isUserAllowedToCids(privilege, uid, cids, callback) {
if (parseInt(uid, 10) === 0) {
return isGuestAllowedToCids(privilege, cids, callback);
if (parseInt(uid, 10) <= 0) {
return isSystemGroupAllowedToCids(privilege, uid, cids, callback);
}
var userKeys = [];
@ -42,8 +47,8 @@ function isUserAllowedToCids(privilege, uid, cids, callback) {
}
function isUserAllowedToPrivileges(privileges, uid, cid, callback) {
if (parseInt(uid, 10) === 0) {
return isGuestAllowedToPrivileges(privileges, cid, callback);
if (parseInt(uid, 10) <= 0) {
return isSystemGroupAllowedToPrivileges(privileges, uid, cid, callback);
}
var userKeys = [];
@ -100,20 +105,20 @@ helpers.isUsersAllowedTo = function (privilege, uids, cid, callback) {
], callback);
};
function isGuestAllowedToCids(privilege, cids, callback) {
function isSystemGroupAllowedToCids(privilege, uid, cids, callback) {
var groupKeys = cids.map(function (cid) {
return 'cid:' + cid + ':privileges:groups:' + privilege;
});
groups.isMemberOfGroups('guests', groupKeys, callback);
groups.isMemberOfGroups(uidToSystemGroup[uid], groupKeys, callback);
}
function isGuestAllowedToPrivileges(privileges, cid, callback) {
function isSystemGroupAllowedToPrivileges(privileges, uid, cid, callback) {
var groupKeys = privileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:groups:' + privilege;
});
groups.isMemberOfGroups('guests', groupKeys, callback);
groups.isMemberOfGroups(uidToSystemGroup[uid], groupKeys, callback);
}
helpers.getUserPrivileges = function (cid, hookName, userPrivilegeList, callback) {

@ -1,48 +1,70 @@
'use strict';
var nconf = require('nconf');
var util = require('util');
var winston = require('winston');
var EventEmitter = require('events').EventEmitter;
var channelName;
var PubSub = function () {
var self = this;
if (nconf.get('redis')) {
var redis = require('./database/redis');
var subClient = redis.connect();
this.pubClient = redis.connect();
channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel';
subClient.subscribe(channelName);
subClient.on('message', function (channel, message) {
if (channel !== channelName) {
return;
}
try {
var msg = JSON.parse(message);
self.emit(msg.event, msg.data);
} catch (err) {
winston.error(err.stack);
}
});
}
var real;
var fake = {
publishQueue: [],
publish: function (event, data) {
fake.publishQueue.push({ event: event, data: data });
},
listenQueue: {},
on: function (event, callback) {
if (!Object.prototype.hasOwnProperty.call(fake.listenQueue, event)) {
fake.listenQueue[event] = [];
}
fake.listenQueue[event].push(callback);
},
removeAllListeners: function (event) {
delete fake.listenQueue[event];
},
};
util.inherits(PubSub, EventEmitter);
function get() {
if (real) {
return real;
}
var pubsub;
PubSub.prototype.publish = function (event, data) {
if (this.pubClient) {
this.pubClient.publish(channelName, JSON.stringify({ event: event, data: data }));
} else {
this.emit(event, data);
if (nconf.get('isCluster') === 'false') {
var EventEmitter = require('events');
pubsub = new EventEmitter();
pubsub.publish = pubsub.emit.bind(pubsub);
} else if (nconf.get('redis')) {
pubsub = require('./database/redis/pubsub');
} else if (nconf.get('mongo')) {
pubsub = require('./database/mongo/pubsub');
}
};
var pubsub = new PubSub();
if (!pubsub) {
return fake;
}
Object.keys(fake.listenQueue).forEach(function (event) {
fake.listenQueue[event].forEach(function (callback) {
pubsub.on(event, callback);
});
});
fake.publishQueue.forEach(function (msg) {
pubsub.publish(msg.event, msg.data);
});
module.exports = pubsub;
real = pubsub;
fake = null;
return pubsub;
}
module.exports = {
publish: function (event, data) {
get().publish(event, data);
},
on: function (event, callback) {
get().on(event, callback);
},
removeAllListeners: function (event) {
get().removeAllListeners(event);
},
};

@ -20,7 +20,15 @@ Auth.initialize = function (app, middleware) {
app.use(passport.session());
app.use(function (req, res, next) {
req.uid = req.user ? parseInt(req.user.uid, 10) : 0;
var isSpider = req.isSpider();
req.loggedIn = !isSpider && !!req.user;
if (isSpider) {
req.uid = -1;
} else if (req.user) {
req.uid = parseInt(req.user.uid, 10);
} else {
req.uid = 0;
}
next();
});

@ -27,7 +27,7 @@ Sockets.init = function (server) {
path: nconf.get('relative_path') + '/socket.io',
});
addRedisAdapter(io);
io.adapter(nconf.get('redis') ? require('../database/redis').socketAdapter() : db.socketAdapter());
io.use(socketioWildcard);
io.use(authorize);
@ -214,22 +214,6 @@ function authorize(socket, callback) {
], callback);
}
function addRedisAdapter(io) {
if (nconf.get('redis')) {
var redisAdapter = require('socket.io-redis');
var redis = require('../database/redis');
var pub = redis.connect();
var sub = redis.connect();
io.adapter(redisAdapter({
key: 'db:' + nconf.get('redis:database') + ':adapter_key',
pubClient: pub,
subClient: sub,
}));
} else if (nconf.get('isCluster') === 'true') {
winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.');
}
}
Sockets.in = function (room) {
return io.in(room);
};

@ -188,6 +188,14 @@ SocketModules.chats.loadRoom = function (socket, data, callback) {
], callback);
};
SocketModules.chats.getUsersInRoom = function (socket, data, callback) {
if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
Messaging.getUsersInRoom(data.roomId, 0, -1, callback);
};
SocketModules.chats.addUserToRoom = function (socket, data, callback) {
if (!data || !data.roomId || !data.username) {
return callback(new Error('[[error:invalid-data]]'));
@ -220,7 +228,7 @@ SocketModules.chats.addUserToRoom = function (socket, data, callback) {
return next(new Error('[[error:no-user]]'));
}
if (socket.uid === parseInt(uid, 10)) {
return next(new Error('[[error:cant-add-self-to-chat-room]]'));
return next(new Error('[[error:cant-chat-with-yourself]]'));
}
async.parallel({
settings: async.apply(user.getSettings, uid),
@ -295,6 +303,21 @@ SocketModules.chats.delete = function (socket, data, callback) {
], callback);
};
SocketModules.chats.restore = function (socket, data, callback) {
if (!data || !data.roomId || !data.messageId) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
Messaging.canDelete(data.messageId, socket.uid, next);
},
function (next) {
Messaging.restoreMessage(data.messageId, data.roomId, next);
},
], callback);
};
SocketModules.chats.canMessage = function (socket, roomId, callback) {
Messaging.canMessageRoom(socket.uid, roomId, callback);
};

@ -20,6 +20,7 @@ require('./posts/move')(SocketPosts);
require('./posts/votes')(SocketPosts);
require('./posts/bookmarks')(SocketPosts);
require('./posts/tools')(SocketPosts);
require('./posts/diffs')(SocketPosts);
SocketPosts.reply = function (socket, data, callback) {
if (!data || !data.tid || (parseInt(meta.config.minimumPostLength, 10) !== 0 && !data.content)) {

@ -0,0 +1,13 @@
'use strict';
var posts = require('../../posts');
module.exports = function (SocketPosts) {
SocketPosts.getDiffs = function (socket, data, callback) {
posts.diffs.list(data.pid, callback);
};
SocketPosts.showPostAt = function (socket, data, callback) {
posts.diffs.load(data.pid, data.since, socket.uid, callback);
};
};

@ -51,6 +51,7 @@ module.exports = function (SocketPosts) {
postSharing: function (next) {
social.getActivePostSharing(next);
},
history: async.apply(posts.diffs.exists, data.pid),
}, next);
},
function (results, next) {
@ -64,6 +65,7 @@ module.exports = function (SocketPosts) {
results.posts.display_moderator_tools = results.posts.display_edit_tools || results.posts.display_delete_tools;
results.posts.display_move_tools = results.isAdmin || results.isModerator;
results.posts.display_ip_ban = (results.isAdmin || results.isGlobalMod) && !results.posts.selfPost;
results.posts.display_history = results.history;
if (!results.isAdmin && !results.isGlobalMod && !results.isModerator) {
results.posts.ip = undefined;

@ -0,0 +1,49 @@
'use strict';
var async = require('async');
var groups = require('../../groups');
var privileges = require('../../privileges');
var db = require('../../database');
module.exports = {
name: 'Give category access privileges to spiders system group',
timestamp: Date.UTC(2018, 0, 31),
method: function (callback) {
db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
if (err) {
return callback(err);
}
async.eachSeries(cids, function (cid, next) {
getGroupPrivileges(cid, function (err, groupPrivileges) {
if (err) {
return next(err);
}
var privs = [];
if (groupPrivileges['groups:find']) {
privs.push('find');
}
if (groupPrivileges['groups:read']) {
privs.push('read');
}
if (groupPrivileges['groups:topics:read']) {
privs.push('topics:read');
}
privileges.categories.give(privs, cid, 'spiders', next);
});
}, callback);
});
},
};
function getGroupPrivileges(cid, callback) {
var tasks = {};
['groups:find', 'groups:read', 'groups:topics:read'].forEach(function (privilege) {
tasks[privilege] = async.apply(groups.isMember, 'guests', 'cid:' + cid + ':privileges:' + privilege);
});
async.parallel(tasks, callback);
}

@ -27,7 +27,7 @@
</td>
<!-- ENDIF files.isFile -->
<td class="col-md-2 text-right"><!-- IF files.size -->{files.sizeHumanReadable}<!-- ELSE -->[[admin/manage/uploads:filecount, {files.fileCount}]]<!-- ENDIF files.size --></td>
<td class="col-md-2 text-right"><!-- IF files.isFile -->{files.sizeHumanReadable}<!-- ELSE -->[[admin/manage/uploads:filecount, {files.fileCount}]]<!-- ENDIF files.isFile --></td>
<td role="button" class="col-md-1 text-right"><i class="delete fa fa-fw fa-trash-o <!-- IF !files.isFile --> hidden<!-- ENDIF !files.isFile -->"></i></td>
</tr>

@ -17,6 +17,7 @@ var cookieParser = require('cookie-parser');
var session = require('express-session');
var useragent = require('express-useragent');
var favicon = require('serve-favicon');
var detector = require('spider-detector');
var helmet = require('helmet');
var db = require('./database');
@ -162,6 +163,7 @@ function setupExpressApp(app, callback) {
app.use(bodyParser.json());
app.use(cookieParser());
app.use(useragent.express());
app.use(detector.middleware());
app.use(session({
store: db.sessionStore,

@ -17,6 +17,9 @@ describe('Sorted Set methods', function () {
function (next) {
db.sortedSetAdd('sortedSetTest3', [2, 4], ['value2', 'value4'], next);
},
function (next) {
db.sortedSetAdd('sortedSetTest4', [1, 1, 2, 3, 5], ['b', 'a', 'd', 'e', 'c'], next);
},
function (next) {
db.sortedSetAdd('sortedSetLex', [0, 0, 0, 0], ['a', 'b', 'c', 'd'], next);
},
@ -305,6 +308,33 @@ describe('Sorted Set methods', function () {
done();
});
});
it('should return the rank sorted by the score and then the value (a)', function (done) {
db.sortedSetRank('sortedSetTest4', 'a', function (err, rank) {
assert.equal(err, null);
assert.equal(arguments.length, 2);
assert.equal(rank, 0);
done();
});
});
it('should return the rank sorted by the score and then the value (b)', function (done) {
db.sortedSetRank('sortedSetTest4', 'b', function (err, rank) {
assert.equal(err, null);
assert.equal(arguments.length, 2);
assert.equal(rank, 1);
done();
});
});
it('should return the rank sorted by the score and then the value (c)', function (done) {
db.sortedSetRank('sortedSetTest4', 'c', function (err, rank) {
assert.equal(err, null);
assert.equal(arguments.length, 2);
assert.equal(rank, 4);
done();
});
});
});
describe('sortedSetRevRank()', function () {

@ -14,8 +14,8 @@ var helpers = require('./helpers');
var socketModules = require('../src/socket.io/modules');
describe('Messaging Library', function () {
var fooUid;
var bazUid;
var fooUid; // the admin
var bazUid; // the user with chat restriction enabled
var herpUid;
var roomId;
@ -156,7 +156,7 @@ describe('Messaging Library', function () {
it('should fail to add self to room', function (done) {
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'foo' }, function (err) {
assert.equal(err.message, '[[error:cant-add-self-to-chat-room]]');
assert.equal(err.message, '[[error:cant-chat-with-yourself]]');
done();
});
});
@ -593,21 +593,71 @@ describe('Messaging Library', function () {
});
});
it('should mark the message as deleted', function (done) {
socketModules.chats.delete({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) {
assert.ifError(err);
db.getObjectField('message:' + mid, 'deleted', function (err, value) {
assert.ifError(err);
assert.strictEqual(1, parseInt(value, 10));
done();
});
});
});
it('should show deleted message to original users', function (done) {
socketModules.chats.getMessages({ uid: fooUid }, { uid: fooUid, roomId: roomId, start: 0 }, function (err, messages) {
assert.ifError(err);
// Reduce messages to their mids
var mids = messages.reduce(function (mids, cur) {
mids.push(cur.messageId);
return mids;
}, []);
assert(mids.includes(mid));
done();
});
});
it('should not show deleted message to other users', function (done) {
socketModules.chats.getMessages({ uid: herpUid }, { uid: herpUid, roomId: roomId, start: 0 }, function (err, messages) {
assert.ifError(err);
// Reduce messages to their mids
var mids = messages.reduce(function (mids, cur) {
mids.push(cur.messageId);
return mids;
}, []);
it('should delete message', function (done) {
assert(!mids.includes(mid));
done();
});
});
it('should error out if a message is deleted again', function (done) {
socketModules.chats.delete({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) {
assert.strictEqual('[[error:chat-deleted-already]]', err.message);
done();
});
});
it('should restore the message', function (done) {
socketModules.chats.restore({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) {
assert.ifError(err);
db.exists('message:' + mid, function (err, exists) {
db.getObjectField('message:' + mid, 'deleted', function (err, value) {
assert.ifError(err);
assert(!exists);
db.isSortedSetMember('uid:' + fooUid + ':chat:room:' + roomId + ':mids', mid, function (err, isMember) {
assert.ifError(err);
assert(!isMember);
done();
});
assert.strictEqual(0, parseInt(value, 10));
done();
});
});
});
it('should error out if a message is restored again', function (done) {
socketModules.chats.restore({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) {
assert.strictEqual('[[error:chat-restored-already]]', err.message);
done();
});
});
});
describe('controller', function () {

Loading…
Cancel
Save