diff --git a/.travis.yml b/.travis.yml index 10f03d19ef..99f8a599e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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" diff --git a/install/package.json b/install/package.json index f53c0db483..1b17355286 100644 --- a/install/package.json +++ b/install/package.json @@ -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", diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index f206a9b882..14842a1507 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -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.", diff --git a/public/language/en-GB/global.json b/public/language/en-GB/global.json index b61d8f27b9..1195d792a0 100644 --- a/public/language/en-GB/global.json +++ b/public/language/en-GB/global.json @@ -134,5 +134,7 @@ "edited": "Edited", "disabled": "Disabled", - "select": "Select" + "select": "Select", + + "user-search-prompt": "Type something here to find users..." } diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index 8f8e4ad1ed..5104549457 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -1,5 +1,5 @@ { - "chat.chatting_with": "Chat with ", + "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", diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index e084a9f24f..4b30d84356 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -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 %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions." } diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json index 3e5d51c4aa..093179d80f 100644 --- a/public/language/en-GB/user.json +++ b/public/language/en-GB/user.json @@ -1,6 +1,7 @@ { "banned": "Banned", "offline": "Offline", + "deleted": "Deleted", "username": "User Name", "joindate": "Join Date", "postcount": "Post Count", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 2da1735a36..76ca7e2e34 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -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: '

[[modules:chat.leave-prompt]]

[[modules:chat.leave-help]]

', + 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', $('
').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($('
').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'); }; diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index fc4a327021..6a772dd6e5 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -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; }); diff --git a/public/src/client/chats/recent.js b/public/src/client/chats/recent.js index 51650c8795..32156cfc8f 100644 --- a/public/src/client/chats/recent.js +++ b/public/src/client/chats/recent.js @@ -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 () { diff --git a/public/src/client/chats/search.js b/public/src/client/chats/search.js index 1b76d665d8..e40551ed05 100644 --- a/public/src/client/chats/search.js +++ b/public/src/client/chats/search.js @@ -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) { diff --git a/public/src/client/topic/diffs.js b/public/src/client/topic/diffs.js new file mode 100644 index 0000000000..63dd2b7b28 --- /dev/null +++ b/public/src/client/topic/diffs.js @@ -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; +}); diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 9cbfdbb366..a164f97ee2 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -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); diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js index 6c32cb36f2..738252b038 100644 --- a/public/src/modules/autocomplete.js +++ b/public/src/modules/autocomplete.js @@ -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) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 9a2ca3e7c4..5791a7828f 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -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 = $('
').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) { diff --git a/public/src/modules/components.js b/public/src/modules/components.js index 29d9e883a1..08454f21bc 100644 --- a/public/src/modules/components.js +++ b/public/src/modules/components.js @@ -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 () { diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index 5dfa5def2e..f891ea2ae4 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -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, diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index ff5a07d157..103af601ce 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -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([ diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index ae73b817e8..bc43213de0 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -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); diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js index e03b51745c..78108caca1 100644 --- a/src/controllers/admin/categories.js +++ b/src/controllers/admin/categories.js @@ -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); }, diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index db26284205..7256e35b7b 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -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); } diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index 5c71c19da7..f33435c325 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -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([ diff --git a/src/controllers/api.js b/src/controllers/api.js index 7321694107..c64d054595 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -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, '{').replace(/}/g, '}'); @@ -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); diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index b2d46dcd70..88d0ec5b49 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -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'); } diff --git a/src/controllers/category.js b/src/controllers/category.js index 89f924e479..f3e90897cf 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -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); diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 319f10e841..21dccd0044 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -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]]', }); diff --git a/src/controllers/index.js b/src/controllers/index.js index 665be289c6..9dccc94aa3 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -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); diff --git a/src/controllers/popular.js b/src/controllers/popular.js index 6c7c2e6011..fae1bbaa16 100644 --- a/src/controllers/popular.js +++ b/src/controllers/popular.js @@ -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(); } diff --git a/src/controllers/recent.js b/src/controllers/recent.js index 7b997894c1..1ef041604f 100644 --- a/src/controllers/recent.js +++ b/src/controllers/recent.js @@ -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]]'; diff --git a/src/controllers/search.js b/src/controllers/search.js index 49e762407a..4c37b6258d 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -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; diff --git a/src/controllers/top.js b/src/controllers/top.js index 7b500533d5..3be4fb261d 100644 --- a/src/controllers/top.js +++ b/src/controllers/top.js @@ -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]]'; diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 1936c99481..9e89a1e929 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -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); diff --git a/src/controllers/user.js b/src/controllers/user.js index ea027b9406..3405c1ea1d 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -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([ diff --git a/src/database/mongo.js b/src/database/mongo.js index 5bcab76a6f..be6789389b 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -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()); +}; diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index 263ad79849..5eca32fd4e 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -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) { diff --git a/src/database/mongo/pubsub.js b/src/database/mongo/pubsub.js new file mode 100644 index 0000000000..83ff676294 --- /dev/null +++ b/src/database/mongo/pubsub.js @@ -0,0 +1,8 @@ +'use strict'; + +var mubsub = require('mubsub'); + +var db = require('../mongo'); +var client = mubsub(db.client); + +module.exports = client.channel('pubsub'); diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index 22c7e44196..ca1d53f63b 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -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); }; diff --git a/src/database/redis.js b/src/database/redis.js index 70a8ffbd53..e88663cf70 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -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'); diff --git a/src/database/redis/pubsub.js b/src/database/redis/pubsub.js new file mode 100644 index 0000000000..cb989207a6 --- /dev/null +++ b/src/database/redis/pubsub.js @@ -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(); diff --git a/src/groups.js b/src/groups.js index 3e6c36d1c8..780eda46c4 100644 --- a/src/groups.js +++ b/src/groups.js @@ -22,7 +22,7 @@ require('./groups/posts')(Groups); require('./groups/user')(Groups); -Groups.ephemeralGroups = ['guests']; +Groups.ephemeralGroups = ['guests', 'spiders']; Groups.getEphemeralGroup = function (groupName) { return { diff --git a/src/messaging.js b/src/messaging.js index 1c8289b5b8..6b0462eb0e 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -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); diff --git a/src/messaging/create.js b/src/messaging/create.js index f4fe421eac..6f355a9ff8 100644 --- a/src/messaging/create.js +++ b/src/messaging/create.js @@ -58,6 +58,7 @@ module.exports = function (Messaging) { timestamp: timestamp, fromuid: fromuid, roomId: roomId, + deleted: 0, }; plugins.fireHook('filter:messaging.save', message, next); diff --git a/src/messaging/data.js b/src/messaging/data.js index 88b6683a11..bae9c6deac 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -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) { diff --git a/src/messaging/delete.js b/src/messaging/delete.js index e3058c0ba9..55907491f2 100644 --- a/src/messaging/delete.js +++ b/src/messaging/delete.js @@ -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); }; diff --git a/src/middleware/header.js b/src/middleware/header.js index 0d2b916965..c769991753 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -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); diff --git a/src/middleware/index.js b/src/middleware/index.js index f6bee01a53..659724153e 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -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')) { diff --git a/src/middleware/user.js b/src/middleware/user.js index 2b29aa5ca1..9ddbbf086d 100644 --- a/src/middleware/user.js +++ b/src/middleware/user.js @@ -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(); } diff --git a/src/posts.js b/src/posts.js index cf30bd4c64..6061017b89 100644 --- a/src/posts.js +++ b/src/posts.js @@ -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); diff --git a/src/posts/diffs.js b/src/posts/diffs.js new file mode 100644 index 0000000000..1472e913e4 --- /dev/null +++ b/src/posts/diffs.js @@ -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); + }); + }; +}; diff --git a/src/posts/edit.js b/src/posts/edit.js index 8780fa6016..2ca8e3b534 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -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; diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index c3452c495e..3c0d5834ae 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -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) { diff --git a/src/pubsub.js b/src/pubsub.js index b33a03b6bf..270ae37bf8 100644 --- a/src/pubsub.js +++ b/src/pubsub.js @@ -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); + }, +}; diff --git a/src/routes/authentication.js b/src/routes/authentication.js index c85e03fe27..212c2b1302 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -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(); }); diff --git a/src/socket.io/index.js b/src/socket.io/index.js index c22f96154f..af37273fda 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -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); }; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index f5d2e8143a..e3e24654a4 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -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); }; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index 39dd1fe64e..e0b7884ece 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -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)) { diff --git a/src/socket.io/posts/diffs.js b/src/socket.io/posts/diffs.js new file mode 100644 index 0000000000..7f208dc71f --- /dev/null +++ b/src/socket.io/posts/diffs.js @@ -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); + }; +}; diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js index a61e50ec6c..b8533937ce 100644 --- a/src/socket.io/posts/tools.js +++ b/src/socket.io/posts/tools.js @@ -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; diff --git a/src/upgrades/1.8.0/give_spiders_privileges.js b/src/upgrades/1.8.0/give_spiders_privileges.js new file mode 100644 index 0000000000..da3a3b2792 --- /dev/null +++ b/src/upgrades/1.8.0/give_spiders_privileges.js @@ -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); +} diff --git a/src/views/admin/manage/uploads.tpl b/src/views/admin/manage/uploads.tpl index 0af5bd43c8..a18b233aa6 100644 --- a/src/views/admin/manage/uploads.tpl +++ b/src/views/admin/manage/uploads.tpl @@ -27,7 +27,7 @@ - {files.sizeHumanReadable}[[admin/manage/uploads:filecount, {files.fileCount}]] + {files.sizeHumanReadable}[[admin/manage/uploads:filecount, {files.fileCount}]] diff --git a/src/webserver.js b/src/webserver.js index 639f2af667..23c3315f3d 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -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, diff --git a/test/database/sorted.js b/test/database/sorted.js index 703d3e93b4..08799513b2 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -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 () { diff --git a/test/messaging.js b/test/messaging.js index 80ec29d09b..d026ece475 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -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 () {