Merge branch 'develop'

This commit is contained in:
Julian Lam
2018-03-02 13:15:39 -05:00
63 changed files with 847 additions and 344 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 () {
Chats.addActionHandlers = function (element, roomId) {
element.on('click', '[data-action]', 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 () {
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');
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

77
src/posts/diffs.js Normal file
View File

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

View File

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

View File

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

View File

@@ -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 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];
},
};
var PubSub = function () {
var self = this;
if (nconf.get('redis')) {
var redis = require('./database/redis');
var subClient = redis.connect();
this.pubClient = redis.connect();
function get() {
if (real) {
return real;
}
channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel';
subClient.subscribe(channelName);
var pubsub;
subClient.on('message', function (channel, message) {
if (channel !== channelName) {
return;
}
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');
}
try {
var msg = JSON.parse(message);
self.emit(msg.event, msg.data);
} catch (err) {
winston.error(err.stack);
}
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);
});
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);
},
};
util.inherits(PubSub, EventEmitter);
PubSub.prototype.publish = function (event, data) {
if (this.pubClient) {
this.pubClient.publish(channelName, JSON.stringify({ event: event, data: data }));
} else {
this.emit(event, data);
}
};
var pubsub = new PubSub();
module.exports = pubsub;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 delete message', function (done) {
it('should mark the message as deleted', function (done) {
socketModules.chats.delete({ 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(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;
}, []);
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.getObjectField('message:' + mid, 'deleted', function (err, value) {
assert.ifError(err);
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 () {