diff --git a/package.json b/package.json index 5aee89a816..add7cde955 100644 --- a/package.json +++ b/package.json @@ -52,17 +52,17 @@ "morgan": "^1.3.2", "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "4.4.1", + "nodebb-plugin-composer-default": "4.4.2", "nodebb-plugin-dbsearch": "2.0.2", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.1.5", "nodebb-plugin-markdown": "7.1.1", "nodebb-plugin-mentions": "2.0.1", "nodebb-plugin-soundpack-default": "1.0.0", - "nodebb-plugin-spam-be-gone": "0.4.10", + "nodebb-plugin-spam-be-gone": "0.4.13", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "3.0.15", - "nodebb-theme-persona": "4.2.4", + "nodebb-theme-persona": "4.2.6", "nodebb-theme-vanilla": "5.2.0", "nodebb-widget-essentials": "2.0.13", "nodemailer": "2.6.4", @@ -94,6 +94,7 @@ "underscore.deep": "^0.5.1", "validator": "^6.1.0", "winston": "^2.1.0", + "xml": "^1.0.1", "xregexp": "~3.1.0" }, "devDependencies": { diff --git a/public/language/en-GB/admin/settings/general.json b/public/language/en-GB/admin/settings/general.json index 72ecfe641f..3f2814bd88 100644 --- a/public/language/en-GB/admin/settings/general.json +++ b/public/language/en-GB/admin/settings/general.json @@ -27,5 +27,6 @@ "touch-icon.help": "Recommended size and format: 192x192, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", - "search-default-sort-by": "Search default sort by" + "search-default-sort-by": "Search default sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page" } \ No newline at end of file diff --git a/public/language/en-GB/groups.json b/public/language/en-GB/groups.json index a55cc8603f..08c8d4d1f5 100644 --- a/public/language/en-GB/groups.json +++ b/public/language/en-GB/groups.json @@ -32,6 +32,7 @@ "details.disableJoinRequests": "Disable join requests", "details.grant": "Grant/Rescind Ownership", "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", "details.owner_options": "Group Administration", "details.group_name": "Group Name", diff --git a/public/language/pl/admin/settings/group.json b/public/language/pl/admin/settings/group.json index 6db8cb32b4..c5900c2a39 100644 --- a/public/language/pl/admin/settings/group.json +++ b/public/language/pl/admin/settings/group.json @@ -1,5 +1,5 @@ { - "general": "General", + "general": "Ogólne", "private-groups": "Prywatne Grupy", "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", diff --git a/public/language/pl/admin/settings/tags.json b/public/language/pl/admin/settings/tags.json index d67523c8e6..ef8efabbcd 100644 --- a/public/language/pl/admin/settings/tags.json +++ b/public/language/pl/admin/settings/tags.json @@ -1,6 +1,6 @@ { "tag": "Ustawienia Tagów", - "min-per-topic": "Minimum Tags per Topic", + "min-per-topic": "Minimalna ilość Tagów na Temat", "max-per-topic": "Maximum Tags per Topic", "min-length": "Minimum Tag Length", "max-length": "Maximum Tag Length", diff --git a/public/language/pl/admin/settings/uploads.json b/public/language/pl/admin/settings/uploads.json index 9a610fb576..05edbff41a 100644 --- a/public/language/pl/admin/settings/uploads.json +++ b/public/language/pl/admin/settings/uploads.json @@ -1,6 +1,6 @@ { "posts": "Posty", - "allow-files": "Allow users to upload regular files", + "allow-files": "Pozwolić użytkownikom wgrywać pliki", "private": "Make uploaded files private", "max-image-width": "Resize images down to specified width (in pixels)", "max-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", diff --git a/public/language/sr/user.json b/public/language/sr/user.json index 08c61c887c..62a64140a7 100644 --- a/public/language/sr/user.json +++ b/public/language/sr/user.json @@ -31,7 +31,7 @@ "signature": "Потпис", "birthday": "Рођендан", "chat": "Ђаскање", - "chat_with": "Настави ћаскање са %1", + "chat_with": "Ћаскај са %1", "new_chat_with": "Започни ново ћаскање са %1", "flag-profile": "Означи профил", "follow": "Прати", diff --git a/public/language/vi/admin/admin.json b/public/language/vi/admin/admin.json index 9c01f56006..726cef0e8b 100644 --- a/public/language/vi/admin/admin.json +++ b/public/language/vi/admin/admin.json @@ -1,7 +1,7 @@ { - "alert.confirm-reload": "Are you sure you wish to reload NodeBB?", - "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + "alert.confirm-reload": "Bạn có thật sự muốn tải lại NodeBB", + "alert.confirm-restart": "Bạn có thật sự muốn khởi động lại NodeBB", - "acp-title": "%1 | NodeBB Admin Control Panel", - "settings-header-contents": "Contents" + "acp-title": "%1 | Bảng điểu khiển", + "settings-header-contents": "Nội dung" } \ No newline at end of file diff --git a/public/language/vi/admin/advanced/cache.json b/public/language/vi/admin/advanced/cache.json index 5a954f1232..505b1a4510 100644 --- a/public/language/vi/admin/advanced/cache.json +++ b/public/language/vi/admin/advanced/cache.json @@ -1,11 +1,11 @@ { - "post-cache": "Post Cache", - "posts-in-cache": "Posts in Cache", - "average-post-size": "Average Post Size", - "length-to-max": "Length / Max", - "percent-full": "%1% Full", - "post-cache-size": "Post Cache Size", - "items-in-cache": "Items in Cache", - "control-panel": "Control Panel", - "update-settings": "Update Cache Settings" + "post-cache": "Cache bài viết", + "posts-in-cache": "Cache cho bài viết", + "average-post-size": "Kích thước bài viết", + "length-to-max": "Độ dài / Tối Đa", + "percent-full": "%1% Đầy", + "post-cache-size": "Kích thước cache bài viết", + "items-in-cache": "Thành phần trong Cache", + "control-panel": "Bảng điều khiển", + "update-settings": "Cập nhật thiết lập Cache" } \ No newline at end of file diff --git a/public/language/vi/admin/advanced/database.json b/public/language/vi/admin/advanced/database.json index f7db6220ee..fe446d1759 100644 --- a/public/language/vi/admin/advanced/database.json +++ b/public/language/vi/admin/advanced/database.json @@ -1,30 +1,30 @@ { "x-b": "%1 b", "x-mb": "%1 mb", - "uptime-seconds": "Uptime in Seconds", - "uptime-days": "Uptime in Days", + "uptime-seconds": "Thời gian hoạt động(giây)", + "uptime-days": "Thời gian hoạt động(Ngày)", "mongo": "Mongo", - "mongo.version": "MongoDB Version", - "mongo.storage-engine": "Storage Engine", - "mongo.collections": "Collections", - "mongo.objects": "Objects", - "mongo.avg-object-size": "Avg. Object Size", - "mongo.data-size": "Data Size", - "mongo.storage-size": "Storage Size", - "mongo.index-size": "Index Size", - "mongo.file-size": "File Size", + "mongo.version": "Phiên bản MongoDB ", + "mongo.storage-engine": "Lưu Trữ", + "mongo.collections": "Tập dữ liệu", + "mongo.objects": "Đối tượng", + "mongo.avg-object-size": "Kích thước trung bình", + "mongo.data-size": "Kích thước dữ liệu", + "mongo.storage-size": "Kích thước lưu trữ", + "mongo.index-size": "Kích thước chỉ mục", + "mongo.file-size": "Kích thước tập tin", "mongo.resident-memory": "Resident Memory", - "mongo.virtual-memory": "Virtual Memory", + "mongo.virtual-memory": "Bộ nhớ ảo", "mongo.mapped-memory": "Mapped Memory", - "mongo.raw-info": "MongoDB Raw Info", + "mongo.raw-info": "Thông tin MongoDB", "redis": "Redis", - "redis.version": "Redis Version", - "redis.connected-clients": "Connected Clients", + "redis.version": "Phiên bản Redis", + "redis.connected-clients": "Người dùng kết nối", "redis.connected-slaves": "Connected Slaves", - "redis.blocked-clients": "Blocked Clients", - "redis.used-memory": "Used Memory", + "redis.blocked-clients": "Người dùng vi phạm", + "redis.used-memory": "Bộ nhớ đã sử dụng", "redis.memory-frag-ratio": "Memory Fragmentation Ratio", "redis.total-connections-recieved": "Total Connections Received", "redis.total-commands-processed": "Total Commands Processed", diff --git a/public/language/vi/global.json b/public/language/vi/global.json index 5a4e6a1406..65db660c60 100644 --- a/public/language/vi/global.json +++ b/public/language/vi/global.json @@ -103,5 +103,5 @@ "cookies.message": "Trang web này sử dụng cookie để đảm bảo trải nghiệm tốt nhất cho người dùng", "cookies.accept": "Đã rõ!", "cookies.learn_more": "Xem thêm", - "edited": "Edited" + "edited": "Đã cập nhật" } \ No newline at end of file diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index aaa940bdc3..5c1cd238c7 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -13,8 +13,8 @@ "notify_me": "Được thông báo khi có trả lời mới trong chủ đề này", "quote": "Trích dẫn", "reply": "Trả lời", - "replies_to_this_post": "%1 Replies", - "last_reply_time": "Last reply", + "replies_to_this_post": "%1 trả lời", + "last_reply_time": "Trả lời cuối cùng", "reply-as-topic": "Trả lời dưới dạng chủ đề", "guest-login-reply": "Hãy đăng nhập để trả lời", "edit": "Chỉnh sửa", diff --git a/public/language/zh-CN/users.json b/public/language/zh-CN/users.json index d6795469f2..253d4db756 100644 --- a/public/language/zh-CN/users.json +++ b/public/language/zh-CN/users.json @@ -6,7 +6,7 @@ "search": "搜索", "enter_username": "输入用户名搜索", "load_more": "加载更多", - "users-found-search-took": "找到 %1 位用户!耗时 %2 毫秒。", + "users-found-search-took": "找到 %1 位用户!耗时 %2 秒。", "filter-by": "过滤选项", "online-only": "只看在线", "invite": "邀请注册", diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index 23f3bead8c..bcf6ddca58 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -272,4 +272,8 @@ body { border: 1px dashed @brand-success; background: lighten(@brand-success, 10%); opacity: 0.5; +} + +form small { + color: @gray-light; } \ No newline at end of file diff --git a/public/less/admin/settings.less b/public/less/admin/settings.less index 84572def68..decfc8b7ed 100644 --- a/public/less/admin/settings.less +++ b/public/less/admin/settings.less @@ -16,4 +16,20 @@ [data-action="upload"][type="text"] { width: 95%; } + + .bootstrap-tagsinput { + width: 100%; + border: 0; + box-shadow: none; + padding-left: 0; + + input { + width: 100%; + margin-left: 1px; + margin-top: 9px; + border-bottom: 1px dotted #ccc !important; + padding-bottom: 5px; + padding-left: 0; + } + } } \ No newline at end of file diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js index 041de4a40f..5495e3b71b 100644 --- a/public/src/admin/settings.js +++ b/public/src/admin/settings.js @@ -102,6 +102,7 @@ define('admin/settings', ['uploader'], function (uploader) { }); handleUploads(); + setupTagsInput(); $('#clear-sitemap-cache').off('click').on('click', function () { socket.emit('admin.settings.clearSitemapCache', function () { @@ -142,6 +143,14 @@ define('admin/settings', ['uploader'], function (uploader) { }); } + function setupTagsInput() { + $('[data-field-type="tagsinput"]').tagsinput({ + confirmKeys: [13, 44], + trimValue: true, + }); + app.flags._unsaved = false; + } + Settings.remove = function (key) { socket.emit('admin.config.remove', key); }; diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index 12ebff1540..93eb4791b0 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -99,10 +99,11 @@ $(document).ready(function () { }; ajaxify.handleRedirects = function (url) { - url = ajaxify.removeRelativePath(url.replace(/\/$/, '')).toLowerCase(); + url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, '')).toLowerCase(); var isClientToAdmin = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0; var isAdminToClient = !url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') === 0; - var uploadsOrApi = url.startsWith('uploads') || url.startsWith('api'); + var uploadsOrApi = url.startsWith('assets/uploads') || url.startsWith('uploads') || url.startsWith('api'); + if (isClientToAdmin || isAdminToClient || uploadsOrApi) { window.open(RELATIVE_PATH + '/' + url, '_top'); return true; @@ -365,8 +366,13 @@ $(document).ready(function () { window.open(this.href, '_blank'); e.preventDefault(); } else if (config.useOutgoingLinksPage) { - ajaxify.go('outgoing?url=' + encodeURIComponent(this.href)); - e.preventDefault(); + var safeUrls = config.outgoingLinksWhitelist.trim().split(/[\s,]+/g); + var href = this.href; + + if (!safeUrls.some(function (url) { return href.indexOf(url) !== -1; })) { + ajaxify.go('outgoing?url=' + encodeURIComponent(href)); + e.preventDefault(); + } } } } diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js index 20e85bb46b..13b84e3023 100644 --- a/public/src/client/account/edit.js +++ b/public/src/client/account/edit.js @@ -241,22 +241,29 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components' uploadModal.find('.upload-btn').on('click', function () { var url = uploadModal.find('#uploadFromUrl').val(); if (!url) { - return; + return false; } - - uploadModal.modal('hide'); - - pictureCropper.handleImageCrop({ + socket.emit('user.uploadProfileImageFromUrl', { + uid: ajaxify.data.uid, url: url, - socketMethod: 'user.uploadCroppedPicture', - aspectRatio: '1 / 1', - allowSkippingCrop: false, - restrictImageDimension: true, - imageDimension: ajaxify.data.profileImageDimension, - paramName: 'uid', - paramValue: ajaxify.data.theirid, - }, onUploadComplete); + }, function (err, url) { + if (err) { + return app.alertError(err); + } + uploadModal.modal('hide'); + + pictureCropper.handleImageCrop({ + url: url, + socketMethod: 'user.uploadCroppedPicture', + aspectRatio: '1 / 1', + allowSkippingCrop: false, + restrictImageDimension: true, + imageDimension: ajaxify.data.profileImageDimension, + paramName: 'uid', + paramValue: ajaxify.data.theirid, + }, onUploadComplete); + }); return false; }); }); diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js index 537baab5de..bffefc725f 100644 --- a/public/src/client/account/settings.js +++ b/public/src/client/account/settings.js @@ -4,6 +4,12 @@ define('forum/account/settings', ['forum/account/header', 'components', 'sounds'], function (header, components, sounds) { var AccountSettings = {}; + $(window).on('action:ajaxify.start', function () { + if (ajaxify.data.template.name === 'account/settings' && $('#bootswatchSkin').val() !== config.bootswatchSkin) { + changePageSkin(config.bootswatchSkin); + } + }); + AccountSettings.init = function () { header.init(); @@ -24,10 +30,7 @@ define('forum/account/settings', ['forum/account/header', 'components', 'sounds' }); $('#bootswatchSkin').on('change', function () { - var css = $('#bootswatchCSS'); - var val = $(this).val() === 'default' ? config['theme:src'] : '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + $(this).val() + '/bootstrap.min.css'; - - css.attr('href', val); + changePageSkin($(this).val()); }); $('[data-property="homePageRoute"]').on('change', toggleCustomRoute); @@ -44,6 +47,29 @@ define('forum/account/settings', ['forum/account/header', 'components', 'sounds' components.get('user/sessions').find('.timeago').timeago(); }; + function changePageSkin(skinName) { + var css = $('#bootswatchCSS'); + if (skinName === 'noskin' || (skinName === 'default' && config.defaultBootswatchSkin === 'noskin')) { + css.remove(); + } else { + if (skinName === 'default') { + skinName = config.defaultBootswatchSkin; + } + var cssSource = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + skinName + '/bootstrap.min.css'; + if (css.length) { + css.attr('href', cssSource); + } else { + css = $(''); + $('head').append(css); + } + } + + var currentSkinClassName = $('body').attr('class').split(/\s+/).filter(function (className) { + return className.startsWith('skin-'); + }); + $('body').removeClass(currentSkinClassName.join(' ')).addClass('skin-' + skinName); + } + function loadSettings() { var settings = {}; diff --git a/public/src/client/account/topics.js b/public/src/client/account/topics.js index 340c2e11b9..c16ffd913d 100644 --- a/public/src/client/account/topics.js +++ b/public/src/client/account/topics.js @@ -27,6 +27,7 @@ define('forum/account/topics', ['forum/account/header', 'forum/infinitescroll'], infinitescroll.loadMore('topics.loadMoreFromSet', { set: set, after: $('[component="category"]').attr('data-nextstart'), + count: config.topicsPerPage, }, function (data, done) { if (data.topics && data.topics.length) { onTopicsLoaded(data.topics, done); diff --git a/public/src/client/chats.js b/public/src/client/chats.js index c42a8e9330..582c578f65 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -282,11 +282,17 @@ define('forum/chats', [ if (err) { return app.alertError(err.message); } - if (parseInt(roomId, 10) === ajaxify.data.roomId) { + if (parseInt(roomId, 10) === parseInt(ajaxify.data.roomId, 10)) { ajaxify.go('user/' + ajaxify.data.userslug + '/chats'); } else { el.remove(); } + require(['chat'], function (chat) { + var modal = chat.getModal(roomId); + if (modal.length) { + chat.close(modal); + } + }); }); }; diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index c8300c485c..2a1889cf25 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -35,7 +35,14 @@ define('forum/chats/messages', ['components', 'sounds', 'translator'], function if (err.message === '[[error:email-not-confirmed-chat]]') { return app.showEmailConfirmWarning(err); } - return app.alertError(err.message); + + return app.alert({ + alert_id: 'chat_spam_error', + title: '[[global:alert.error]]', + message: err.message, + type: 'danger', + timeout: 10000, + }); } sounds.play('chat-outgoing'); @@ -57,8 +64,10 @@ define('forum/chats/messages', ['components', 'sounds', 'translator'], function messages.appendChatMessage = function (chatContentEl, data) { var lastSpeaker = parseInt(chatContentEl.find('.chat-message').last().attr('data-uid'), 10); + var lasttimestamp = parseInt(chatContentEl.find('.chat-message').last().attr('data-timestamp'), 10); if (!Array.isArray(data)) { - data.newSet = lastSpeaker !== data.fromuid; + data.newSet = lastSpeaker !== parseInt(data.fromuid, 10) || + parseInt(data.timestamp, 10) > parseInt(lasttimestamp, 10) + (1000 * 60 * 3); } messages.parseMessage(data, function (html) { diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js index 4c516eac38..0806195ac4 100644 --- a/public/src/client/flags/list.js +++ b/public/src/client/flags/list.js @@ -76,7 +76,6 @@ define('forum/flags/list', ['components', 'Chart'], function (components, Chart) yAxes: [{ ticks: { beginAtZero: true, - stepSize: 1, }, }], }, diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 4f7a11a892..e7225bb16c 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -75,15 +75,23 @@ define('forum/groups/details', [ break; case 'kick': - socket.emit('groups.kick', { - uid: uid, - groupName: groupName, - }, function (err) { - if (!err) { - userRow.slideUp().remove(); - } else { - app.alertError(err.message); - } + translator.translate('[[groups:details.kick_confirm]]', function (translated) { + bootbox.confirm(translated, function (confirm) { + if (!confirm) { + return; + } + + socket.emit('groups.kick', { + uid: uid, + groupName: groupName, + }, function (err) { + if (!err) { + userRow.slideUp().remove(); + } else { + app.alertError(err.message); + } + }); + }); }); break; diff --git a/public/src/client/recent.js b/public/src/client/recent.js index 981baabd12..b8f9380198 100644 --- a/public/src/client/recent.js +++ b/public/src/client/recent.js @@ -132,6 +132,7 @@ define('forum/recent', ['forum/infinitescroll', 'components'], function (infinit infinitescroll.loadMore('topics.loadMoreRecentTopics', { after: $('[component="category"]').attr('data-nextstart'), + count: config.topicsPerPage, cid: utils.params().cid, filter: ajaxify.data.selectedFilter.filter, set: $('[component="category"]').attr('data-set') ? $('[component="category"]').attr('data-set') : 'topics:recent', diff --git a/public/src/client/tag.js b/public/src/client/tag.js index 2b74c17193..e07126cb2e 100644 --- a/public/src/client/tag.js +++ b/public/src/client/tag.js @@ -27,6 +27,7 @@ define('forum/tag', ['forum/recent', 'forum/infinitescroll'], function (recent, infinitescroll.loadMore('topics.loadMoreFromSet', { set: 'tag:' + ajaxify.data.tag + ':topics', after: $('[component="category"]').attr('data-nextstart'), + count: config.topicsPerPage, }, function (data, done) { if (data.topics && data.topics.length) { recent.onTopicsLoaded('tag', data.topics, false, done); diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 9088830554..d620da1e70 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -7,11 +7,12 @@ define('forum/topic', [ 'forum/topic/postTools', 'forum/topic/events', 'forum/topic/posts', + 'forum/topic/images', 'forum/topic/replies', 'navigator', 'sort', 'components', -], function (infinitescroll, threadTools, postTools, events, posts, replies, navigator, sort, components) { +], function (infinitescroll, threadTools, postTools, events, posts, images, replies, navigator, sort, components) { var Topic = {}; var currentUrl = ''; @@ -238,7 +239,7 @@ define('forum/topic', [ return; } - posts.loadImages(threshold); + images.loadImages(threshold); var newUrl = 'topic/' + ajaxify.data.slug + (index > 1 ? ('/' + index) : ''); diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index f3e2d925c6..22a6c69d6c 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -6,9 +6,10 @@ define('forum/topic/events', [ 'forum/topic/postTools', 'forum/topic/threadTools', 'forum/topic/posts', + 'forum/topic/images', 'components', 'translator', -], function (postTools, threadTools, posts, components, translator) { +], function (postTools, threadTools, posts, images, components, translator) { var Events = {}; var events = { @@ -128,9 +129,9 @@ define('forum/topic/events', [ editedPostEl.html(translator.unescape(data.post.content)); editedPostEl.find('img:not(.not-responsive)').addClass('img-responsive'); app.replaceSelfLinks(editedPostEl.find('a')); - posts.wrapImagesInLinks(editedPostEl.parent()); - posts.unloadImages(editedPostEl.parent()); - posts.loadImages(); + images.wrapImagesInLinks(editedPostEl.parent()); + images.unloadImages(editedPostEl.parent()); + images.loadImages(); editedPostEl.fadeIn(250); var editData = { diff --git a/public/src/client/topic/images.js b/public/src/client/topic/images.js new file mode 100644 index 0000000000..cd44dd0914 --- /dev/null +++ b/public/src/client/topic/images.js @@ -0,0 +1,119 @@ +'use strict'; + + +define('forum/topic/images', [ + 'forum/topic/postTools', + 'navigator', + 'components', +], function (postTools, navigator, components) { + var Images = { + _imageLoaderTimeout: undefined, + }; + + Images.unloadImages = function (posts) { + var images = posts.find('[component="post/content"] img:not(.not-responsive)'); + + if (config.delayImageLoading) { + images.each(function () { + $(this).attr('data-src', $(this).attr('src')); + }).attr('data-state', 'unloaded').attr('src', 'about:blank'); + } else { + images.attr('data-state', 'loaded'); + Images.wrapImagesInLinks(posts); + } + }; + + Images.loadImages = function (threshold) { + if (Images._imageLoaderTimeout) { + clearTimeout(Images._imageLoaderTimeout); + } + + Images._imageLoaderTimeout = setTimeout(function () { + /* + If threshold is defined, images loaded above this threshold will modify + the user's scroll position so they are not scrolled away from content + they were reading. Images loaded below this threshold will push down content. + + If no threshold is defined, loaded images will push down content, as per + default + */ + + var images = components.get('post/content').find('img[data-state="unloaded"]'); + var visible = images.filter(function () { + return utils.isElementInViewport(this); + }); + var posts = $.unique(visible.map(function () { + return $(this).parents('[component="post"]').get(0); + })); + var scrollTop = $(window).scrollTop(); + var adjusting = false; + var adjustQueue = []; + var oldHeight; + var newHeight; + + function adjustPosition() { + adjusting = true; + oldHeight = document.body.clientHeight; + + // Display the image + $(this).attr('data-state', 'loaded'); + newHeight = document.body.clientHeight; + + var imageRect = this.getBoundingClientRect(); + if (imageRect.top < threshold) { + scrollTop += newHeight - oldHeight; + $(window).scrollTop(scrollTop); + } + + if (adjustQueue.length) { + adjustQueue.pop()(); + } else { + adjusting = false; + + Images.wrapImagesInLinks(posts); + posts.length = 0; + } + } + + // For each image, reset the source and adjust scrollTop when loaded + visible.attr('data-state', 'loading'); + visible.each(function (index, image) { + image = $(image); + + image.on('load', function () { + if (!adjusting) { + adjustPosition.call(this); + } else { + adjustQueue.push(adjustPosition.bind(this)); + } + }); + + image.attr('src', image.attr('data-src')); + image.removeAttr('data-src'); + }); + }, 250); + }; + + Images.wrapImagesInLinks = function (posts) { + posts.find('[component="post/content"] img:not(.emoji)').each(function () { + var $this = $(this); + var src = $this.attr('src'); + var suffixRegex = /-resized(\.[\w]+)?$/; + + if (src === 'about:blank') { + return; + } + + if (utils.isRelativeUrl(src) && suffixRegex.test(src)) { + src = src.replace(suffixRegex, '$1'); + } + + if (!$this.parent().is('a')) { + $this.wrap(''); + } + }); + }; + + + return Images; +}); diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index 689d443ad3..fb9074b3be 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -5,19 +5,14 @@ define('forum/topic/posts', [ 'forum/pagination', 'forum/infinitescroll', 'forum/topic/postTools', + 'forum/topic/images', 'navigator', 'components', -], function (pagination, infinitescroll, postTools, navigator, components) { - var Posts = { - _imageLoaderTimeout: undefined, - }; +], function (pagination, infinitescroll, postTools, images, navigator, components) { + var Posts = { }; Posts.onNewPost = function (data) { - if (!data || !data.posts || !data.posts.length) { - return; - } - - if (parseInt(data.posts[0].tid, 10) !== parseInt(ajaxify.data.tid, 10)) { + if (!data || !data.posts || !data.posts.length || parseInt(data.posts[0].tid, 10) !== parseInt(ajaxify.data.tid, 10)) { return; } @@ -63,7 +58,7 @@ define('forum/topic/posts', [ function onNewPostPagination(data) { function scrollToPost() { scrollToPostIfSelf(data.posts[0]); - Posts.loadImages(); + images.loadImages(); } var posts = data.posts; @@ -107,7 +102,7 @@ define('forum/topic/posts', [ html.addClass('new'); } scrollToPostIfSelf(data.posts[0]); - Posts.loadImages(); + images.loadImages(); }); } @@ -232,6 +227,7 @@ define('forum/topic/posts', [ infinitescroll.loadMore('topics.loadMore', { tid: tid, after: after, + count: config.postsPerPage, direction: direction, topicPostSort: config.topicPostSort, }, function (data, done) { @@ -247,7 +243,7 @@ define('forum/topic/posts', [ }; Posts.processPage = function (posts) { - Posts.unloadImages(posts); + images.unloadImages(posts); Posts.showBottomPostBar(); posts.find('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); app.createUserTooltips(posts); @@ -260,110 +256,6 @@ define('forum/topic/posts', [ hidePostToolsForDeletedPosts(posts); }; - Posts.unloadImages = function (posts) { - var images = posts.find('[component="post/content"] img:not(.not-responsive)'); - - if (config.delayImageLoading) { - images.each(function () { - $(this).attr('data-src', $(this).attr('src')); - }).attr('data-state', 'unloaded').attr('src', 'about:blank'); - } else { - images.attr('data-state', 'loaded'); - Posts.wrapImagesInLinks(posts); - } - }; - - Posts.loadImages = function (threshold) { - if (Posts._imageLoaderTimeout) { - clearTimeout(Posts._imageLoaderTimeout); - } - - Posts._imageLoaderTimeout = setTimeout(function () { - /* - If threshold is defined, images loaded above this threshold will modify - the user's scroll position so they are not scrolled away from content - they were reading. Images loaded below this threshold will push down content. - - If no threshold is defined, loaded images will push down content, as per - default - */ - - var images = components.get('post/content').find('img[data-state="unloaded"]'); - var visible = images.filter(function () { - return utils.isElementInViewport(this); - }); - var posts = $.unique(visible.map(function () { - return $(this).parents('[component="post"]').get(0); - })); - var scrollTop = $(window).scrollTop(); - var adjusting = false; - var adjustQueue = []; - var oldHeight; - var newHeight; - - function adjustPosition() { - adjusting = true; - oldHeight = document.body.clientHeight; - - // Display the image - $(this).attr('data-state', 'loaded'); - newHeight = document.body.clientHeight; - - var imageRect = this.getBoundingClientRect(); - if (imageRect.top < threshold) { - scrollTop += newHeight - oldHeight; - $(window).scrollTop(scrollTop); - } - - if (adjustQueue.length) { - adjustQueue.pop()(); - } else { - adjusting = false; - - Posts.wrapImagesInLinks(posts); - posts.length = 0; - } - } - - // For each image, reset the source and adjust scrollTop when loaded - visible.attr('data-state', 'loading'); - visible.each(function (index, image) { - image = $(image); - - image.on('load', function () { - if (!adjusting) { - adjustPosition.call(this); - } else { - adjustQueue.push(adjustPosition.bind(this)); - } - }); - - image.attr('src', image.attr('data-src')); - image.removeAttr('data-src'); - }); - }, 250); - }; - - Posts.wrapImagesInLinks = function (posts) { - posts.find('[component="post/content"] img:not(.emoji)').each(function () { - var $this = $(this); - var src = $this.attr('src'); - var suffixRegex = /-resized(\.[\w]+)?$/; - - if (src === 'about:blank') { - return; - } - - if (utils.isRelativeUrl(src) && suffixRegex.test(src)) { - src = src.replace(suffixRegex, '$1'); - } - - if (!$this.parent().is('a')) { - $this.wrap(''); - } - }); - }; - Posts.showBottomPostBar = function () { var mainPost = components.get('post', 'index', 0); var placeHolder = $('.post-bar-placeholder'); diff --git a/public/src/client/unread.js b/public/src/client/unread.js index f5b1dd73ed..01ef9cff52 100644 --- a/public/src/client/unread.js +++ b/public/src/client/unread.js @@ -92,6 +92,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll', ' var cid = params.cid; infinitescroll.loadMore('topics.loadMoreUnreadTopics', { after: $('[component="category"]').attr('data-nextstart'), + count: config.topicsPerPage, cid: cid, filter: ajaxify.data.selectedFilter.filter, }, function (data, done) { diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index 7e93618cc7..3f02a86758 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -16,9 +16,9 @@ return false; } var properties = item.properties; - + var loggedIn = data.config ? data.config.loggedIn : false; if (properties) { - if ((properties.loggedIn && !data.config.loggedIn) || + if ((properties.loggedIn && !loggedIn) || (properties.globalMod && !data.isGlobalMod && !data.isAdmin) || (properties.adminOnly && !data.isAdmin) || (properties.searchInstalled && !data.searchEnabled)) { @@ -26,11 +26,11 @@ } } - if (item.route.match('/users') && data.privateUserInfo && !data.config.loggedIn) { + if (item.route.match('/users') && data.privateUserInfo && !loggedIn) { return false; } - if (item.route.match('/tags') && data.privateTagListing && !data.config.loggedIn) { + if (item.route.match('/tags') && data.privateTagListing && !loggedIn) { return false; } diff --git a/public/src/modules/pictureCropper.js b/public/src/modules/pictureCropper.js index 0ed62d3d8c..bbe0a76735 100644 --- a/public/src/modules/pictureCropper.js +++ b/public/src/modules/pictureCropper.js @@ -42,6 +42,7 @@ define('pictureCropper', ['translator', 'cropper'], function (translator, croppe var img = document.getElementById('cropped-image'); var cropperTool = new cropper.default(img, { aspectRatio: data.aspectRatio, + autoCropArea: 1, viewMode: 1, cropmove: function () { if (data.restrictImageDimension) { diff --git a/src/controllers/404.js b/src/controllers/404.js new file mode 100644 index 0000000000..bc4e2e1d00 --- /dev/null +++ b/src/controllers/404.js @@ -0,0 +1,47 @@ +'use strict'; + +var nconf = require('nconf'); +var winston = require('winston'); +var validator = require('validator'); + +var meta = require('../meta'); +var plugins = require('../plugins'); + +exports.handle404 = function (req, res) { + var relativePath = nconf.get('relative_path'); + var isClientScript = new RegExp('^' + relativePath + '\\/assets\\/src\\/.+\\.js'); + + if (plugins.hasListeners('action:meta.override404')) { + return plugins.fireHook('action:meta.override404', { + req: req, + res: res, + error: {}, + }); + } + + if (isClientScript.test(req.url)) { + res.type('text/javascript').status(200).send(''); + } else if (req.path.startsWith(relativePath + '/assets/uploads') || (req.get('accept') && req.get('accept').indexOf('text/html') === -1) || req.path === '/favicon.ico') { + meta.errors.log404(req.path || ''); + res.sendStatus(404); + } else if (req.accepts('html')) { + if (process.env.NODE_ENV === 'development') { + winston.warn('Route requested but not found: ' + req.url); + } + + meta.errors.log404(req.path.replace(/^\/api/, '') || ''); + res.status(404); + + var path = String(req.path || ''); + + if (res.locals.isAPI) { + return res.json({ path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]' }); + } + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('404', { path: validator.escape(path), title: '[[global:404.title]]' }); + }); + } else { + res.status(404).type('txt').send('Not found'); + } +}; diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js index edd5d58282..2f360f883c 100644 --- a/src/controllers/accounts/settings.js +++ b/src/controllers/accounts/settings.js @@ -108,6 +108,7 @@ settingsController.get = function (req, res, callback) { userData.bootswatchSkinOptions = [ + { name: 'No skin', value: 'noskin' }, { name: 'Default', value: 'default' }, { name: 'Cerulean', value: 'cerulean' }, { name: 'Cosmo', value: 'cosmo' }, diff --git a/src/controllers/api.js b/src/controllers/api.js index 66392e6160..f7158481e7 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -13,9 +13,8 @@ var privileges = require('../privileges'); var plugins = require('../plugins'); var widgets = require('../widgets'); var translator = require('../../public/src/modules/translator'); -var accountHelpers = require('../controllers/accounts/helpers'); -var apiController = {}; +var apiController = module.exports; apiController.getConfig = function (req, res, next) { var config = {}; @@ -62,7 +61,12 @@ apiController.getConfig = function (req, res, next) { config.categoryTopicSort = meta.config.categoryTopicSort || 'newest_to_oldest'; config.csrf_token = req.csrfToken(); config.searchEnabled = plugins.hasListeners('filter:search.query'); - config.bootswatchSkin = 'default'; + config.bootswatchSkin = meta.config.bootswatchSkin || 'noskin'; + config.defaultBootswatchSkin = meta.config.bootswatchSkin || 'noskin'; + + if (config.useOutgoingLinksPage) { + config.outgoingLinksWhitelist = meta.config['outgoingLinks:whitelist']; + } var timeagoCutoff = meta.config.timeagoCutoff === undefined ? 30 : meta.config.timeagoCutoff; config.timeagoCutoff = timeagoCutoff !== '' ? Math.max(0, parseInt(timeagoCutoff, 10)) : timeagoCutoff; @@ -91,7 +95,7 @@ apiController.getConfig = function (req, res, next) { config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; config.topicSearchEnabled = settings.topicSearchEnabled || false; config.delayImageLoading = settings.delayImageLoading !== undefined ? settings.delayImageLoading : true; - config.bootswatchSkin = settings.bootswatchSkin || config.bootswatchSkin; + config.bootswatchSkin = (settings.bootswatchSkin && settings.bootswatchSkin !== 'default') ? settings.bootswatchSkin : config.bootswatchSkin; plugins.fireHook('filter:config.get', config, next); }, ], function (err, config) { @@ -220,92 +224,6 @@ apiController.getObject = function (req, res, next) { }); }; -apiController.getCurrentUser = function (req, res, next) { - if (!req.uid) { - return res.status(401).json('not-authorized'); - } - async.waterfall([ - function (next) { - user.getUserField(req.uid, 'userslug', next); - }, - function (userslug, next) { - accountHelpers.getUserDataByUserSlug(userslug, req.uid, next); - }, - ], function (err, userData) { - if (err) { - return next(err); - } - res.json(userData); - }); -}; - -apiController.getUserByUID = function (req, res, next) { - byType('uid', req, res, next); -}; - -apiController.getUserByUsername = function (req, res, next) { - byType('username', req, res, next); -}; - -apiController.getUserByEmail = function (req, res, next) { - byType('email', req, res, next); -}; - -function byType(type, req, res, next) { - apiController.getUserDataByField(req.uid, type, req.params[type], function (err, data) { - if (err || !data) { - return next(err); - } - res.json(data); - }); -} - -apiController.getUserDataByField = function (callerUid, field, fieldValue, callback) { - async.waterfall([ - function (next) { - if (field === 'uid') { - next(null, fieldValue); - } else if (field === 'username') { - user.getUidByUsername(fieldValue, next); - } else if (field === 'email') { - user.getUidByEmail(fieldValue, next); - } else { - next(); - } - }, - function (uid, next) { - if (!uid) { - return next(); - } - apiController.getUserDataByUID(callerUid, uid, next); - }, - ], callback); -}; - -apiController.getUserDataByUID = function (callerUid, uid, callback) { - if (!parseInt(callerUid, 10) && parseInt(meta.config.privateUserInfo, 10) === 1) { - return callback(new Error('[[error:no-privileges]]')); - } - - if (!parseInt(uid, 10)) { - return callback(new Error('[[error:no-user]]')); - } - - async.parallel({ - userData: async.apply(user.getUserData, uid), - settings: async.apply(user.getSettings, uid), - }, function (err, results) { - if (err || !results.userData) { - return callback(err || new Error('[[error:no-user]]')); - } - - results.userData.email = results.settings.showemail ? results.userData.email : undefined; - results.userData.fullname = results.settings.showfullname ? results.userData.fullname : undefined; - - callback(null, results.userData); - }); -}; - apiController.getModerators = function (req, res, next) { categories.getModerators(req.params.cid, function (err, moderators) { if (err) { @@ -314,16 +232,3 @@ apiController.getModerators = function (req, res, next) { res.json({ moderators: moderators }); }); }; - - -apiController.getRecentPosts = function (req, res, next) { - posts.getRecentPosts(req.uid, 0, 19, req.params.term, function (err, data) { - if (err) { - return next(err); - } - - res.json(data); - }); -}; - -module.exports = apiController; diff --git a/src/controllers/categories.js b/src/controllers/categories.js index d5cd04b504..e02f107b4a 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -13,9 +13,6 @@ categoriesController.list = function (req, res, next) { res.locals.metaTags = [{ name: 'title', content: String(meta.config.title || 'NodeBB'), - }, { - name: 'description', - content: String(meta.config.description || ''), }, { property: 'og:title', content: '[[pages:categories]]', diff --git a/src/controllers/errors.js b/src/controllers/errors.js new file mode 100644 index 0000000000..6ab0dc6471 --- /dev/null +++ b/src/controllers/errors.js @@ -0,0 +1,63 @@ +'use strict'; + +var nconf = require('nconf'); +var winston = require('winston'); +var validator = require('validator'); + +exports.handleURIErrors = function (err, req, res, next) { + // Handle cases where malformed URIs are passed in + if (err instanceof URIError) { + var tidMatch = req.path.match(/^\/topic\/(\d+)\//); + var cidMatch = req.path.match(/^\/category\/(\d+)\//); + + if (tidMatch) { + res.redirect(nconf.get('relative_path') + tidMatch[0]); + } else if (cidMatch) { + res.redirect(nconf.get('relative_path') + cidMatch[0]); + } else { + winston.warn('[controller] Bad request: ' + req.path); + if (res.locals.isAPI) { + res.status(400).json({ + error: '[[global:400.title]]', + }); + } else { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('400', { error: validator.escape(String(err.message)) }); + }); + } + } + } else { + next(err); + } +}; + +// this needs to have four arguments or express treats it as `(req, res, next)` +// don't remove `next`! +exports.handleErrors = function (err, req, res, next) { // eslint-disable-line no-unused-vars + switch (err.code) { + case 'EBADCSRFTOKEN': + winston.error(req.path + '\n', err.message); + return res.sendStatus(403); + case 'blacklisted-ip': + return res.status(403).type('text/plain').send(err.message); + } + + if (parseInt(err.status, 10) === 302 && err.path) { + return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path); + } + + winston.error(req.path + '\n', err.stack); + + res.status(err.status || 500); + + var path = String(req.path || ''); + if (res.locals.isAPI) { + res.json({ path: validator.escape(path), error: err.message }); + } else { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) }); + }); + } +}; diff --git a/src/controllers/index.js b/src/controllers/index.js index 59f5527ef4..93b8e9a383 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -3,34 +3,36 @@ var async = require('async'); var nconf = require('nconf'); var validator = require('validator'); -var winston = require('winston'); var meta = require('../meta'); var user = require('../user'); var plugins = require('../plugins'); var helpers = require('./helpers'); -var Controllers = { - topics: require('./topics'), - posts: require('./posts'), - categories: require('./categories'), - category: require('./category'), - unread: require('./unread'), - recent: require('./recent'), - popular: require('./popular'), - tags: require('./tags'), - search: require('./search'), - users: require('./users'), - groups: require('./groups'), - accounts: require('./accounts'), - authentication: require('./authentication'), - api: require('./api'), - admin: require('./admin'), - globalMods: require('./globalmods'), - mods: require('./mods'), - sitemap: require('./sitemap'), -}; - +var Controllers = module.exports; + +Controllers.topics = require('./topics'); +Controllers.posts = require('./posts'); +Controllers.categories = require('./categories'); +Controllers.category = require('./category'); +Controllers.unread = require('./unread'); +Controllers.recent = require('./recent'); +Controllers.popular = require('./popular'); +Controllers.tags = require('./tags'); +Controllers.search = require('./search'); +Controllers.user = require('./user'); +Controllers.users = require('./users'); +Controllers.groups = require('./groups'); +Controllers.accounts = require('./accounts'); +Controllers.authentication = require('./authentication'); +Controllers.api = require('./api'); +Controllers.admin = require('./admin'); +Controllers.globalMods = require('./globalmods'); +Controllers.mods = require('./mods'); +Controllers.sitemap = require('./sitemap'); +Controllers.osd = require('./osd'); +Controllers['404'] = require('./404'); +Controllers.errors = require('./errors'); Controllers.home = function (req, res, next) { var route = meta.config.homePageRoute || (meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories'; @@ -129,7 +131,7 @@ Controllers.login = function (req, res, next) { if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) { if (res.locals.isAPI) { return helpers.redirect(res, { - external: data.authentication[0].url, + external: nconf.get('relative_path') + data.authentication[0].url, }); } return res.redirect(nconf.get('relative_path') + data.authentication[0].url); @@ -321,19 +323,18 @@ Controllers.manifest = function (req, res) { res.status(200).json(manifest); }; -Controllers.outgoing = function (req, res) { +Controllers.outgoing = function (req, res, next) { var url = req.query.url || ''; - var data = { + + if (!url) { + return next(); + } + + res.render('outgoing', { outgoing: validator.escape(String(url)), title: meta.config.title, breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[notifications:outgoing_link]]' }]), - }; - - if (url) { - res.render('outgoing', data); - } else { - res.status(404).redirect(nconf.get('relative_path') + '/404'); - } + }); }; Controllers.termsOfUse = function (req, res, next) { @@ -346,102 +347,3 @@ Controllers.termsOfUse = function (req, res, next) { Controllers.ping = function (req, res) { res.status(200).send(req.path === '/sping' ? 'healthy' : '200'); }; - -Controllers.handle404 = function (req, res) { - var relativePath = nconf.get('relative_path'); - var isClientScript = new RegExp('^' + relativePath + '\\/assets\\/src\\/.+\\.js'); - - if (plugins.hasListeners('action:meta.override404')) { - return plugins.fireHook('action:meta.override404', { - req: req, - res: res, - error: {}, - }); - } - - if (isClientScript.test(req.url)) { - res.type('text/javascript').status(200).send(''); - } else if (req.path.startsWith(relativePath + '/assets/uploads') || (req.get('accept') && req.get('accept').indexOf('text/html') === -1) || req.path === '/favicon.ico') { - meta.errors.log404(req.path || ''); - res.sendStatus(404); - } else if (req.accepts('html')) { - if (process.env.NODE_ENV === 'development') { - winston.warn('Route requested but not found: ' + req.url); - } - - meta.errors.log404(req.path.replace(/^\/api/, '') || ''); - res.status(404); - - var path = String(req.path || ''); - - if (res.locals.isAPI) { - return res.json({ path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]' }); - } - var middleware = require('../middleware'); - middleware.buildHeader(req, res, function () { - res.render('404', { path: validator.escape(path), title: '[[global:404.title]]' }); - }); - } else { - res.status(404).type('txt').send('Not found'); - } -}; - -Controllers.handleURIErrors = function (err, req, res, next) { - // Handle cases where malformed URIs are passed in - if (err instanceof URIError) { - var tidMatch = req.path.match(/^\/topic\/(\d+)\//); - var cidMatch = req.path.match(/^\/category\/(\d+)\//); - - if (tidMatch) { - res.redirect(nconf.get('relative_path') + tidMatch[0]); - } else if (cidMatch) { - res.redirect(nconf.get('relative_path') + cidMatch[0]); - } else { - winston.warn('[controller] Bad request: ' + req.path); - if (res.locals.isAPI) { - res.status(400).json({ - error: '[[global:400.title]]', - }); - } else { - var middleware = require('../middleware'); - middleware.buildHeader(req, res, function () { - res.render('400', { error: validator.escape(String(err.message)) }); - }); - } - } - } else { - next(err); - } -}; - -// this needs to have four arguments or express treats it as `(req, res, next)` -// don't remove `next`! -Controllers.handleErrors = function (err, req, res, next) { // eslint-disable-line no-unused-vars - switch (err.code) { - case 'EBADCSRFTOKEN': - winston.error(req.path + '\n', err.message); - return res.sendStatus(403); - case 'blacklisted-ip': - return res.status(403).type('text/plain').send(err.message); - } - - if (parseInt(err.status, 10) === 302 && err.path) { - return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path); - } - - winston.error(req.path + '\n', err.stack); - - res.status(err.status || 500); - - var path = String(req.path || ''); - if (res.locals.isAPI) { - res.json({ path: validator.escape(path), error: err.message }); - } else { - var middleware = require('../middleware'); - middleware.buildHeader(req, res, function () { - res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) }); - }); - } -}; - -module.exports = Controllers; diff --git a/src/controllers/osd.js b/src/controllers/osd.js new file mode 100644 index 0000000000..c83f7f142c --- /dev/null +++ b/src/controllers/osd.js @@ -0,0 +1,32 @@ +'use strict'; + +var xml = require('xml'); +var nconf = require('nconf'); + +var plugins = require('../plugins'); +var meta = require('../meta'); + +module.exports.handle = function (req, res, next) { + if (plugins.hasListeners('filter:search.query')) { + res.type('application/xml').send(generateXML()); + } else { + next(); + } +}; + +function generateXML() { + return xml([{ + OpenSearchDescription: [ + { _attr: { xmlns: 'http://a9.com/-/spec/opensearch/1.1/' } }, + { ShortName: String(meta.config.title || meta.config.browserTitle || 'NodeBB') }, + { Description: String(meta.config.description || '') }, + { Url: { + _attr: { + type: 'text/html', + method: 'get', + template: nconf.get('url') + '/search?term={searchTerms}&in=titlesposts', + }, + } }, + ], + }], { declaration: true, indent: '\t' }); +} diff --git a/src/controllers/posts.js b/src/controllers/posts.js index 90596d5b34..8afb3f5729 100644 --- a/src/controllers/posts.js +++ b/src/controllers/posts.js @@ -1,24 +1,38 @@ 'use strict'; +var async = require('async'); + var posts = require('../posts'); var helpers = require('./helpers'); -var postsController = {}; +var postsController = module.exports; -postsController.redirectToPost = function (req, res, callback) { +postsController.redirectToPost = function (req, res, next) { var pid = parseInt(req.params.pid, 10); if (!pid) { - return callback(); + return next(); } - posts.generatePostPath(pid, req.uid, function (err, path) { - if (err || !path) { - return callback(err); - } - - helpers.redirect(res, path); - }); + async.waterfall([ + function (next) { + posts.generatePostPath(pid, req.uid, next); + }, + function (path, next) { + if (!path) { + return next(); + } + helpers.redirect(res, path); + }, + ], next); }; - -module.exports = postsController; +postsController.getRecentPosts = function (req, res, next) { + async.waterfall([ + function (next) { + posts.getRecentPosts(req.uid, 0, 19, req.params.term, next); + }, + function (data) { + res.json(data); + }, + ], next); +}; diff --git a/src/controllers/search.js b/src/controllers/search.js index be98589c54..bf818a39d5 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -60,7 +60,7 @@ searchController.search = function (req, res, next) { var searchData = results.search; searchData.categories = categoriesData; - searchData.categoriesCount = results.categories.length; + searchData.categoriesCount = Math.max(10, Math.min(20, categoriesData.length)); searchData.pagination = pagination.create(page, searchData.pageCount, req.query); searchData.showAsPosts = !req.query.showAs || req.query.showAs === 'posts'; searchData.showAsTopics = req.query.showAs === 'topics'; diff --git a/src/controllers/sitemap.js b/src/controllers/sitemap.js index 453aed4f6e..846c6facb2 100644 --- a/src/controllers/sitemap.js +++ b/src/controllers/sitemap.js @@ -1,68 +1,57 @@ 'use strict'; +var async = require('async'); + var sitemap = require('../sitemap'); var meta = require('../meta'); -var sitemapController = {}; -sitemapController.render = function (req, res, next) { - sitemap.render(function (err, tplData) { - if (err) { - return next(err); - } +var sitemapController = module.exports; - req.app.render('sitemap', tplData, function (err, xml) { - if (err) { - return next(err); - } +sitemapController.render = function (req, res, next) { + async.waterfall([ + function (next) { + sitemap.render(next); + }, + function (tplData, next) { + req.app.render('sitemap', tplData, next); + }, + function (xml) { res.header('Content-Type', 'application/xml'); res.send(xml); - }); - }); + }, + ], next); }; sitemapController.getPages = function (req, res, next) { - if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); - } - - sitemap.getPages(function (err, xml) { - if (err) { - return next(err); - } - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); + sendSitemap(sitemap.getPages, res, next); }; sitemapController.getCategories = function (req, res, next) { - if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); - } - - sitemap.getCategories(function (err, xml) { - if (err) { - return next(err); - } - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); + sendSitemap(sitemap.getCategories, res, next); }; sitemapController.getTopicPage = function (req, res, next) { + sendSitemap(function (callback) { + sitemap.getTopicPage(parseInt(req.params[0], 10), callback); + }, res, next); +}; + +function sendSitemap(method, res, callback) { if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) { - return next(); + return callback(); } + async.waterfall([ + function (next) { + method(next); + }, + function (xml) { + if (!xml) { + return callback(); + } - sitemap.getTopicPage(parseInt(req.params[0], 10), function (err, xml) { - if (err) { - return next(err); - } else if (!xml) { - return next(); - } - - res.header('Content-Type', 'application/xml'); - res.send(xml); - }); -}; + res.header('Content-Type', 'application/xml'); + res.send(xml); + }, + ], callback); +} -module.exports = sitemapController; diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 25375e91a9..82556ee12c 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -66,7 +66,7 @@ function uploadAsImage(req, uploadedFile, callback) { file.isFileTypeAllowed(uploadedFile.path, next); }, function (next) { - uploadFile(req.uid, uploadedFile, next); + uploadsController.uploadFile(req.uid, uploadedFile, next); }, function (fileObj, next) { if (parseInt(meta.config.maximumImageWidth, 10) === 0) { @@ -90,7 +90,7 @@ function uploadAsFile(req, uploadedFile, callback) { if (parseInt(meta.config.allowFileUploads, 10) !== 1) { return next(new Error('[[error:uploads-are-disabled]]')); } - uploadFile(req.uid, uploadedFile, next); + uploadsController.uploadFile(req.uid, uploadedFile, next); }, ], callback); } @@ -122,7 +122,7 @@ function resizeImage(fileObj, callback) { var extname = path.extname(fileObj.url); var basename = path.basename(fileObj.url, extname); - fileObj.url = path.join(dirname, basename + '-resized' + extname); + fileObj.url = dirname + '/' + basename + '-resized' + extname; next(null, fileObj); }, @@ -161,7 +161,7 @@ uploadsController.uploadThumb = function (req, res, next) { }, next); } - uploadFile(req.uid, uploadedFile, next); + uploadsController.uploadFile(req.uid, uploadedFile, next); }, ], next); }, next); @@ -192,7 +192,7 @@ uploadsController.uploadGroupCover = function (uid, uploadedFile, callback) { ], callback); }; -function uploadFile(uid, uploadedFile, callback) { +uploadsController.uploadFile = function (uid, uploadedFile, callback) { if (plugins.hasListeners('filter:uploadFile')) { return plugins.fireHook('filter:uploadFile', { file: uploadedFile, @@ -217,7 +217,7 @@ function uploadFile(uid, uploadedFile, callback) { } saveFileToLocal(uploadedFile, callback); -} +}; function saveFileToLocal(uploadedFile, callback) { var extension = file.typeToExtension(uploadedFile.type); diff --git a/src/controllers/user.js b/src/controllers/user.js new file mode 100644 index 0000000000..0f93f549b3 --- /dev/null +++ b/src/controllers/user.js @@ -0,0 +1,99 @@ +'use strict'; + +var async = require('async'); + +var user = require('../user'); +var meta = require('../meta'); +var accountHelpers = require('./accounts/helpers'); + +var userController = module.exports; + +userController.getCurrentUser = function (req, res, next) { + if (!req.uid) { + return res.status(401).json('not-authorized'); + } + async.waterfall([ + function (next) { + user.getUserField(req.uid, 'userslug', next); + }, + function (userslug, next) { + accountHelpers.getUserDataByUserSlug(userslug, req.uid, next); + }, + function (userData) { + res.json(userData); + }, + ], next); +}; + + +userController.getUserByUID = function (req, res, next) { + byType('uid', req, res, next); +}; + +userController.getUserByUsername = function (req, res, next) { + byType('username', req, res, next); +}; + +userController.getUserByEmail = function (req, res, next) { + byType('email', req, res, next); +}; + +function byType(type, req, res, next) { + async.waterfall([ + function (next) { + userController.getUserDataByField(req.uid, type, req.params[type], next); + }, + function (data, next) { + if (!data) { + return next(); + } + res.json(data); + }, + ], next); +} + +userController.getUserDataByField = function (callerUid, field, fieldValue, callback) { + async.waterfall([ + function (next) { + if (field === 'uid') { + next(null, fieldValue); + } else if (field === 'username') { + user.getUidByUsername(fieldValue, next); + } else if (field === 'email') { + user.getUidByEmail(fieldValue, next); + } else { + next(null, null); + } + }, + function (uid, next) { + if (!uid) { + return next(null, null); + } + userController.getUserDataByUID(callerUid, uid, next); + }, + ], callback); +}; + +userController.getUserDataByUID = function (callerUid, uid, callback) { + if (!parseInt(callerUid, 10) && parseInt(meta.config.privateUserInfo, 10) === 1) { + return callback(new Error('[[error:no-privileges]]')); + } + + if (!parseInt(uid, 10)) { + return callback(new Error('[[error:no-user]]')); + } + + async.parallel({ + userData: async.apply(user.getUserData, uid), + settings: async.apply(user.getSettings, uid), + }, function (err, results) { + if (err || !results.userData) { + return callback(err || new Error('[[error:no-user]]')); + } + + results.userData.email = results.settings.showemail ? results.userData.email : undefined; + results.userData.fullname = results.settings.showfullname ? results.userData.fullname : undefined; + + callback(null, results.userData); + }); +}; diff --git a/src/controllers/users.js b/src/controllers/users.js index ae3643bb8d..45cb5f1e21 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -8,9 +8,7 @@ var pagination = require('../pagination'); var db = require('../database'); var helpers = require('./helpers'); - -var usersController = {}; - +var usersController = module.exports; usersController.index = function (req, res, next) { var section = req.query.section || 'joindate'; @@ -33,62 +31,65 @@ usersController.index = function (req, res, next) { }; usersController.search = function (req, res, next) { - async.parallel({ - search: function (next) { - user.search({ - query: req.query.term, - searchBy: req.query.searchBy || 'username', - page: req.query.page || 1, - sortBy: req.query.sortBy, - onlineOnly: req.query.onlineOnly === 'true', - bannedOnly: req.query.bannedOnly === 'true', - flaggedOnly: req.query.flaggedOnly === 'true', + async.waterfall([ + function (next) { + async.parallel({ + search: function (next) { + user.search({ + query: req.query.term, + searchBy: req.query.searchBy || 'username', + page: req.query.page || 1, + sortBy: req.query.sortBy, + onlineOnly: req.query.onlineOnly === 'true', + bannedOnly: req.query.bannedOnly === 'true', + flaggedOnly: req.query.flaggedOnly === 'true', + }, next); + }, + isAdminOrGlobalMod: function (next) { + user.isAdminOrGlobalMod(req.uid, next); + }, }, next); }, - isAdminOrGlobalMod: function (next) { - user.isAdminOrGlobalMod(req.uid, next); + function (results, next) { + var section = req.query.section || 'joindate'; + + results.search.isAdminOrGlobalMod = results.isAdminOrGlobalMod; + results.search.pagination = pagination.create(req.query.page, results.search.pageCount, req.query); + results.search['section_' + section] = true; + render(req, res, results.search, next); }, - }, function (err, results) { - if (err) { - return next(err); - } - - var section = req.query.section || 'joindate'; - - results.search.isAdminOrGlobalMod = results.isAdminOrGlobalMod; - results.search.pagination = pagination.create(req.query.page, results.search.pageCount, req.query); - results.search['section_' + section] = true; - render(req, res, results.search, next); - }); + ], next); }; usersController.getOnlineUsers = function (req, res, next) { - async.parallel({ - users: function (next) { - usersController.getUsers('users:online', req.uid, req.query, next); - }, - guests: function (next) { - require('../socket.io/admin/rooms').getTotalGuestCount(next); + async.waterfall([ + function (next) { + async.parallel({ + users: function (next) { + usersController.getUsers('users:online', req.uid, req.query, next); + }, + guests: function (next) { + require('../socket.io/admin/rooms').getTotalGuestCount(next); + }, + }, next); }, - }, function (err, results) { - if (err) { - return next(err); - } - var userData = results.users; - var hiddenCount = 0; - if (!userData.isAdminOrGlobalMod) { - userData.users = userData.users.filter(function (user) { - if (user && user.status === 'offline') { - hiddenCount += 1; - } - return user && user.status !== 'offline'; - }); - } + function (results, next) { + var userData = results.users; + var hiddenCount = 0; + if (!userData.isAdminOrGlobalMod) { + userData.users = userData.users.filter(function (user) { + if (user && user.status === 'offline') { + hiddenCount += 1; + } + return user && user.status !== 'offline'; + }); + } - userData.anonymousUserCount = results.guests + hiddenCount; + userData.anonymousUserCount = results.guests + hiddenCount; - render(req, res, userData, next); - }); + render(req, res, userData, next); + }, + ], next); }; usersController.getUsersSortedByPosts = function (req, res, next) { @@ -107,41 +108,36 @@ usersController.getUsersSortedByJoinDate = function (req, res, next) { }; usersController.getBannedUsers = function (req, res, next) { - usersController.getUsers('users:banned', req.uid, req.query, function (err, userData) { - if (err) { - return next(err); - } - - if (!userData.isAdminOrGlobalMod) { - return next(); - } - - render(req, res, userData, next); - }); + renderIfAdminOrGlobalMod('users:banned', req, res, next); }; usersController.getFlaggedUsers = function (req, res, next) { - usersController.getUsers('users:flags', req.uid, req.query, function (err, userData) { - if (err) { - return next(err); - } - - if (!userData.isAdminOrGlobalMod) { - return next(); - } - - render(req, res, userData, next); - }); + renderIfAdminOrGlobalMod('users:flags', req, res, next); }; -usersController.renderUsersPage = function (set, req, res, next) { - usersController.getUsers(set, req.uid, req.query, function (err, userData) { - if (err) { - return next(err); - } +function renderIfAdminOrGlobalMod(set, req, res, next) { + async.waterfall([ + function (next) { + user.isAdminOrGlobalMod(req.uid, next); + }, + function (isAdminOrGlobalMod, next) { + if (!isAdminOrGlobalMod) { + return helpers.notAllowed(req, res); + } + usersController.renderUsersPage(set, req, res, next); + }, + ], next); +} - render(req, res, userData, next); - }); +usersController.renderUsersPage = function (set, req, res, next) { + async.waterfall([ + function (next) { + usersController.getUsers(set, req.uid, req.query, next); + }, + function (userData, next) { + render(req, res, userData, next); + }, + ], next); }; usersController.getUsers = function (set, uid, query, callback) { @@ -169,59 +165,62 @@ usersController.getUsers = function (set, uid, query, callback) { var start = Math.max(0, page - 1) * resultsPerPage; var stop = start + resultsPerPage - 1; - async.parallel({ - isAdminOrGlobalMod: function (next) { - user.isAdminOrGlobalMod(uid, next); + async.waterfall([ + function (next) { + async.parallel({ + isAdminOrGlobalMod: function (next) { + user.isAdminOrGlobalMod(uid, next); + }, + usersData: function (next) { + usersController.getUsersAndCount(set, uid, start, stop, next); + }, + }, next); }, - usersData: function (next) { - usersController.getUsersAndCount(set, uid, start, stop, next); + function (results, next) { + var pageCount = Math.ceil(results.usersData.count / resultsPerPage); + var userData = { + users: results.usersData.users, + pagination: pagination.create(page, pageCount, query), + userCount: results.usersData.count, + title: setToData[set].title || '[[pages:users/latest]]', + breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs), + isAdminOrGlobalMod: results.isAdminOrGlobalMod, + }; + userData['section_' + (query.section || 'joindate')] = true; + next(null, userData); }, - }, function (err, results) { - if (err) { - return callback(err); - } - - var pageCount = Math.ceil(results.usersData.count / resultsPerPage); - var userData = { - users: results.usersData.users, - pagination: pagination.create(page, pageCount, query), - userCount: results.usersData.count, - title: setToData[set].title || '[[pages:users/latest]]', - breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs), - isAdminOrGlobalMod: results.isAdminOrGlobalMod, - }; - userData['section_' + (query.section || 'joindate')] = true; - callback(null, userData); - }); + ], callback); }; usersController.getUsersAndCount = function (set, uid, start, stop, callback) { - async.parallel({ - users: function (next) { - user.getUsersFromSet(set, uid, start, stop, next); + async.waterfall([ + function (next) { + async.parallel({ + users: function (next) { + user.getUsersFromSet(set, uid, start, stop, next); + }, + count: function (next) { + if (set === 'users:online') { + var now = Date.now(); + db.sortedSetCount('users:online', now - 300000, '+inf', next); + } else if (set === 'users:banned') { + db.sortedSetCard('users:banned', next); + } else if (set === 'users:flags') { + db.sortedSetCard('users:flags', next); + } else { + db.getObjectField('global', 'userCount', next); + } + }, + }, next); }, - count: function (next) { - if (set === 'users:online') { - var now = Date.now(); - db.sortedSetCount('users:online', now - 300000, '+inf', next); - } else if (set === 'users:banned') { - db.sortedSetCard('users:banned', next); - } else if (set === 'users:flags') { - db.sortedSetCard('users:flags', next); - } else { - db.getObjectField('global', 'userCount', next); - } + function (results, next) { + results.users = results.users.filter(function (user) { + return user && parseInt(user.uid, 10); + }); + + next(null, results); }, - }, function (err, results) { - if (err) { - return callback(err); - } - results.users = results.users.filter(function (user) { - return user && parseInt(user.uid, 10); - }); - - callback(null, results); - }); + ], callback); }; function render(req, res, data, next) { @@ -232,16 +231,15 @@ function render(req, res, data, next) { data.adminInviteOnly = registrationType === 'admin-invite-only'; data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; - user.getInvitesNumber(req.uid, function (err, numInvites) { - if (err) { - return next(err); - } - - res.append('X-Total-Count', data.userCount); - data.invites = numInvites; + async.waterfall([ + function (next) { + user.getInvitesNumber(req.uid, next); + }, + function (numInvites) { + res.append('X-Total-Count', data.userCount); + data.invites = numInvites; - res.render('users', data); - }); + res.render('users', data); + }, + ], next); } - -module.exports = usersController; diff --git a/src/database/mongo.js b/src/database/mongo.js index 9bc321703b..8e6494ffd7 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -47,13 +47,8 @@ module.init = function (callback) { callback = callback || function () { }; - var mongoClient; - try { - mongoClient = require('mongodb').MongoClient; - } catch (err) { - winston.error('Unable to initialize MongoDB! Is MongoDB installed? Error :' + err.message); - return callback(err); - } + + var mongoClient = require('mongodb').MongoClient; var usernamePassword = ''; if (nconf.get('mongo:username') && nconf.get('mongo:password')) { @@ -84,10 +79,13 @@ var connOptions = { server: { poolSize: parseInt(nconf.get('mongo:poolSize'), 10) || 10, + socketOptions: { autoReconnect: true, keepAlive: nconf.get('mongo:keepAlive') || 0 }, + reconnectTries: 3600, + reconnectInterval: 1000, }, }; - connOptions = _.deepExtend((nconf.get('mongo:options') || {}), connOptions); + connOptions = _.deepExtend(connOptions, nconf.get('mongo:options') || {}); mongoClient.connect(connString, connOptions, function (err, _db) { if (err) { @@ -107,10 +105,7 @@ if (nconf.get('mongo:password') && nconf.get('mongo:username')) { db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) { - if (err) { - return callback(err); - } - callback(); + callback(err); }); } else { winston.warn('You have no mongo password setup!'); diff --git a/src/groups.js b/src/groups.js index 5ed854b944..fd8c4a958f 100644 --- a/src/groups.js +++ b/src/groups.js @@ -6,215 +6,197 @@ var validator = require('validator'); var user = require('./user'); var db = require('./database'); var plugins = require('./plugins'); -var posts = require('./posts'); -var privileges = require('./privileges'); var utils = require('../public/src/utils'); -(function (Groups) { - require('./groups/create')(Groups); - require('./groups/delete')(Groups); - require('./groups/update')(Groups); - require('./groups/membership')(Groups); - require('./groups/ownership')(Groups); - require('./groups/search')(Groups); - require('./groups/cover')(Groups); - - var ephemeralGroups = ['guests']; - - var internals = { - getEphemeralGroup: function (groupName) { - return { - name: groupName, - slug: utils.slugify(groupName), - description: '', - deleted: '0', - hidden: '0', - system: '1', - }; - }, - removeEphemeralGroups: function (groups) { - for (var x = groups.length; x >= 0; x -= 1) { - if (ephemeralGroups.indexOf(groups[x]) !== -1) { - groups.splice(x, 1); - } - } - - return groups; - }, +var Groups = module.exports; + +require('./groups/data')(Groups); +require('./groups/create')(Groups); +require('./groups/delete')(Groups); +require('./groups/update')(Groups); +require('./groups/membership')(Groups); +require('./groups/ownership')(Groups); +require('./groups/search')(Groups); +require('./groups/cover')(Groups); +require('./groups/posts')(Groups); +require('./groups/user')(Groups); + + +Groups.ephemeralGroups = ['guests']; + +Groups.getEphemeralGroup = function (groupName) { + return { + name: groupName, + slug: utils.slugify(groupName), + description: '', + deleted: '0', + hidden: '0', + system: '1', }; +}; - Groups.internals = internals; - - var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w:]+$/; - Groups.isPrivilegeGroup = function (groupName) { - return isPrivilegeGroupRegex.test(groupName); - }; - - Groups.getEphemeralGroups = function () { - return ephemeralGroups; - }; - - Groups.getGroupsFromSet = function (set, uid, start, stop, callback) { - async.waterfall([ - function (next) { - if (set === 'groups:visible:name') { - db.getSortedSetRangeByLex(set, '-', '+', start, stop - start + 1, next); - } else { - db.getSortedSetRevRange(set, start, stop, next); - } - }, - function (groupNames, next) { - if (set === 'groups:visible:name') { - groupNames = groupNames.map(function (name) { - return name.split(':')[1]; - }); - } - - Groups.getGroupsAndMembers(groupNames, next); - }, - ], callback); - }; - - Groups.getGroups = function (set, start, stop, callback) { - db.getSortedSetRevRange(set, start, stop, callback); - }; - - Groups.getGroupsAndMembers = function (groupNames, callback) { - async.parallel({ - groups: function (next) { - Groups.getGroupsData(groupNames, next); - }, - members: function (next) { - Groups.getMemberUsers(groupNames, 0, 3, next); - }, - }, function (err, data) { - if (err) { - return callback(err); +Groups.removeEphemeralGroups = function (groups) { + for (var x = groups.length; x >= 0; x -= 1) { + if (Groups.ephemeralGroups.indexOf(groups[x]) !== -1) { + groups.splice(x, 1); + } + } + + return groups; +}; + +var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w:]+$/; +Groups.isPrivilegeGroup = function (groupName) { + return isPrivilegeGroupRegex.test(groupName); +}; + +Groups.getGroupsFromSet = function (set, uid, start, stop, callback) { + async.waterfall([ + function (next) { + if (set === 'groups:visible:name') { + db.getSortedSetRangeByLex(set, '-', '+', start, stop - start + 1, next); + } else { + db.getSortedSetRevRange(set, start, stop, next); + } + }, + function (groupNames, next) { + if (set === 'groups:visible:name') { + groupNames = groupNames.map(function (name) { + return name.split(':')[1]; + }); } + Groups.getGroupsAndMembers(groupNames, next); + }, + ], callback); +}; + +Groups.getGroups = function (set, start, stop, callback) { + db.getSortedSetRevRange(set, start, stop, callback); +}; + +Groups.getGroupsAndMembers = function (groupNames, callback) { + async.waterfall([ + function (next) { + async.parallel({ + groups: function (next) { + Groups.getGroupsData(groupNames, next); + }, + members: function (next) { + Groups.getMemberUsers(groupNames, 0, 3, next); + }, + }, next); + }, + function (data, next) { data.groups.forEach(function (group, index) { - if (!group) { - return; + if (group) { + group.members = data.members[index] || []; + group.truncated = group.memberCount > data.members.length; } - - group.members = data.members[index] || []; - group.truncated = group.memberCount > data.members.length; }); + next(null, data.groups); + }, + ], callback); +}; + +Groups.get = function (groupName, options, callback) { + if (!groupName) { + return callback(new Error('[[error:invalid-group]]')); + } + + var stop = -1; + + var results; + async.waterfall([ + function (next) { + async.parallel({ + base: function (next) { + db.getObject('group:' + groupName, next); + }, + members: function (next) { + if (options.truncateUserList) { + stop = (parseInt(options.userListCount, 10) || 4) - 1; + } - callback(null, data.groups); - }); - }; - - Groups.get = function (groupName, options, callback) { - if (!groupName) { - return callback(new Error('[[error:invalid-group]]')); - } - - var stop = -1; - - async.parallel({ - base: function (next) { - db.getObject('group:' + groupName, next); - }, - members: function (next) { - if (options.truncateUserList) { - stop = (parseInt(options.userListCount, 10) || 4) - 1; - } - - Groups.getOwnersAndMembers(groupName, options.uid, 0, stop, next); - }, - pending: function (next) { - async.waterfall([ - function (next) { - db.getSetMembers('group:' + groupName + ':pending', next); - }, - function (uids, next) { - user.getUsersData(uids, next); - }, - ], next); - }, - invited: function (next) { - async.waterfall([ - function (next) { - db.getSetMembers('group:' + groupName + ':invited', next); - }, - function (uids, next) { - user.getUsersData(uids, next); - }, - ], next); - }, - isMember: async.apply(Groups.isMember, options.uid, groupName), - isPending: async.apply(Groups.isPending, options.uid, groupName), - isInvited: async.apply(Groups.isInvited, options.uid, groupName), - isOwner: async.apply(Groups.ownership.isOwner, options.uid, groupName), - }, function (err, results) { - if (err) { - return callback(err); - } else if (!results.base) { + Groups.getOwnersAndMembers(groupName, options.uid, 0, stop, next); + }, + pending: function (next) { + Groups.getUsersFromSet('group:' + groupName + ':pending', next); + }, + invited: function (next) { + Groups.getUsersFromSet('group:' + groupName + ':invited', next); + }, + isMember: async.apply(Groups.isMember, options.uid, groupName), + isPending: async.apply(Groups.isPending, options.uid, groupName), + isInvited: async.apply(Groups.isInvited, options.uid, groupName), + isOwner: async.apply(Groups.ownership.isOwner, options.uid, groupName), + }, next); + }, + function (_results, next) { + results = _results; + if (!results.base) { return callback(new Error('[[error:no-group]]')); } - - results.base['cover:url'] = results.base['cover:url'] || require('./coverPhoto').getDefaultGroupCover(groupName); - results.base['cover:position'] = validator.escape(String(results.base['cover:position'] || '50% 50%')); - results.base.labelColor = validator.escape(String(results.base.labelColor || '#000000')); - results.base.icon = validator.escape(String(results.base.icon || '')); - - plugins.fireHook('filter:parse.raw', results.base.description, function (err, descriptionParsed) { - if (err) { - return callback(err); - } - - Groups.escapeGroupData(results.base); - - results.base.descriptionParsed = descriptionParsed; - results.base.userTitleEnabled = results.base.userTitleEnabled ? !!parseInt(results.base.userTitleEnabled, 10) : true; - results.base.createtimeISO = utils.toISOString(results.base.createtime); - results.base.members = results.members; - results.base.membersNextStart = stop + 1; - results.base.pending = results.pending.filter(Boolean); - results.base.invited = results.invited.filter(Boolean); - results.base.deleted = !!parseInt(results.base.deleted, 10); - results.base.hidden = !!parseInt(results.base.hidden, 10); - results.base.system = !!parseInt(results.base.system, 10); - results.base.memberCount = parseInt(results.base.memberCount, 10); - results.base.private = (results.base.private === null || results.base.private === undefined) ? true : !!parseInt(results.base.private, 10); - results.base.disableJoinRequests = parseInt(results.base.disableJoinRequests, 10) === 1; - results.base.isMember = results.isMember; - results.base.isPending = results.isPending; - results.base.isInvited = results.isInvited; - results.base.isOwner = results.isOwner; - - plugins.fireHook('filter:group.get', { group: results.base }, function (err, data) { - callback(err, data ? data.group : null); - }); - }); - }); - }; - - Groups.getOwners = function (groupName, callback) { - db.getSetMembers('group:' + groupName + ':owners', callback); - }; - - Groups.getOwnersAndMembers = function (groupName, uid, start, stop, callback) { - async.parallel({ - owners: function (next) { - async.waterfall([ - function (next) { - db.getSetMembers('group:' + groupName + ':owners', next); - }, - function (uids, next) { - user.getUsers(uids, uid, next); - }, - ], next); - }, - members: function (next) { - user.getUsersFromSet('group:' + groupName + ':members', uid, start, stop, next); - }, - }, function (err, results) { - if (err) { - return callback(err); - } - + plugins.fireHook('filter:parse.raw', results.base.description, next); + }, + function (descriptionParsed, next) { + var groupData = results.base; + Groups.escapeGroupData(groupData); + + groupData.descriptionParsed = descriptionParsed; + groupData.userTitleEnabled = groupData.userTitleEnabled ? !!parseInt(groupData.userTitleEnabled, 10) : true; + groupData.createtimeISO = utils.toISOString(groupData.createtime); + groupData.members = results.members; + groupData.membersNextStart = stop + 1; + groupData.pending = results.pending.filter(Boolean); + groupData.invited = results.invited.filter(Boolean); + groupData.deleted = !!parseInt(groupData.deleted, 10); + groupData.hidden = !!parseInt(groupData.hidden, 10); + groupData.system = !!parseInt(groupData.system, 10); + groupData.memberCount = parseInt(groupData.memberCount, 10); + groupData.private = (groupData.private === null || groupData.private === undefined) ? true : !!parseInt(groupData.private, 10); + groupData.disableJoinRequests = parseInt(groupData.disableJoinRequests, 10) === 1; + groupData.isMember = results.isMember; + groupData.isPending = results.isPending; + groupData.isInvited = results.isInvited; + groupData.isOwner = results.isOwner; + groupData['cover:url'] = groupData['cover:url'] || require('./coverPhoto').getDefaultGroupCover(groupName); + groupData['cover:position'] = validator.escape(String(groupData['cover:position'] || '50% 50%')); + groupData.labelColor = validator.escape(String(groupData.labelColor || '#000000')); + groupData.icon = validator.escape(String(groupData.icon || '')); + + plugins.fireHook('filter:group.get', { group: groupData }, next); + }, + function (results, next) { + next(null, results.group); + }, + ], callback); +}; + +Groups.getOwners = function (groupName, callback) { + db.getSetMembers('group:' + groupName + ':owners', callback); +}; + +Groups.getOwnersAndMembers = function (groupName, uid, start, stop, callback) { + async.waterfall([ + function (next) { + async.parallel({ + owners: function (next) { + async.waterfall([ + function (next) { + db.getSetMembers('group:' + groupName + ':owners', next); + }, + function (uids, next) { + user.getUsers(uids, uid, next); + }, + ], next); + }, + members: function (next) { + user.getUsersFromSet('group:' + groupName + ':members', uid, start, stop, next); + }, + }, next); + }, + function (results, next) { var ownerUids = []; results.owners.forEach(function (user) { if (user) { @@ -228,225 +210,94 @@ var utils = require('../public/src/utils'); }); results.members = results.owners.concat(results.members); - callback(null, results.members); - }); - }; - - Groups.escapeGroupData = function (group) { - if (group) { - group.nameEncoded = encodeURIComponent(group.name); - group.displayName = validator.escape(String(group.name)); - group.description = validator.escape(String(group.description || '')); - group.userTitle = validator.escape(String(group.userTitle || '')) || group.displayName; - } - }; - - Groups.getByGroupslug = function (slug, options, callback) { - db.getObjectField('groupslug:groupname', slug, function (err, groupName) { - if (err) { - return callback(err); - } else if (!groupName) { - return callback(new Error('[[error:no-group]]')); + next(null, results.members); + }, + ], callback); +}; + +Groups.escapeGroupData = function (group) { + if (group) { + group.nameEncoded = encodeURIComponent(group.name); + group.displayName = validator.escape(String(group.name)); + group.description = validator.escape(String(group.description || '')); + group.userTitle = validator.escape(String(group.userTitle || '')) || group.displayName; + } +}; + +Groups.getByGroupslug = function (slug, options, callback) { + async.waterfall([ + function (next) { + db.getObjectField('groupslug:groupname', slug, next); + }, + function (groupName, next) { + if (!groupName) { + return next(new Error('[[error:no-group]]')); } + Groups.get(groupName, options, next); + }, + ], callback); +}; - Groups.get(groupName, options, callback); - }); - }; - - Groups.getGroupNameByGroupSlug = function (slug, callback) { - db.getObjectField('groupslug:groupname', slug, callback); - }; - - Groups.getGroupFields = function (groupName, fields, callback) { - Groups.getMultipleGroupFields([groupName], fields, function (err, groups) { - callback(err, groups ? groups[0] : null); - }); - }; +Groups.getGroupNameByGroupSlug = function (slug, callback) { + db.getObjectField('groupslug:groupname', slug, callback); +}; - Groups.getMultipleGroupFields = function (groups, fields, callback) { - db.getObjectsFields(groups.map(function (group) { - return 'group:' + group; - }), fields, callback); - }; +Groups.isPrivate = function (groupName, callback) { + isFieldOn(groupName, 'private', callback); +}; - Groups.setGroupField = function (groupName, field, value, callback) { - db.setObjectField('group:' + groupName, field, value, function (err) { - if (err) { - return callback(err); - } - plugins.fireHook('action:group.set', { field: field, value: value, type: 'set' }); - callback(); - }); - }; +Groups.isHidden = function (groupName, callback) { + isFieldOn(groupName, 'hidden', callback); +}; - Groups.isPrivate = function (groupName, callback) { - db.getObjectField('group:' + groupName, 'private', function (err, isPrivate) { - if (err) { - return callback(err); - } - - callback(null, parseInt(isPrivate, 10) !== 0); - }); - }; - - Groups.isHidden = function (groupName, callback) { - db.getObjectField('group:' + groupName, 'hidden', function (err, isHidden) { - if (err) { - return callback(err); - } +function isFieldOn(groupName, field, callback) { + async.waterfall([ + function (next) { + db.getObjectField('group:' + groupName, field, next); + }, + function (value, next) { + next(null, parseInt(value, 10) === 1); + }, + ], callback); +} - callback(null, parseInt(isHidden, 10) === 1); +Groups.exists = function (name, callback) { + if (Array.isArray(name)) { + var slugs = name.map(function (groupName) { + return utils.slugify(groupName); }); - }; - - Groups.exists = function (name, callback) { - if (Array.isArray(name)) { - var slugs = name.map(function (groupName) { - return utils.slugify(groupName); - }); - async.parallel([ - function (next) { - next(null, slugs.map(function (slug) { - return ephemeralGroups.indexOf(slug) !== -1; - })); - }, - async.apply(db.isSortedSetMembers, 'groups:createtime', name), - ], function (err, results) { - if (err) { - return callback(err); - } - callback(null, name.map(function (n, index) { - return results[0][index] || results[1][index]; - })); - }); - } else { - var slug = utils.slugify(name); - async.parallel([ - function (next) { - next(null, ephemeralGroups.indexOf(slug) !== -1); - }, - async.apply(db.isSortedSetMember, 'groups:createtime', name), - ], function (err, results) { - callback(err, !err ? (results[0] || results[1]) : null); - }); - } - }; - - Groups.existsBySlug = function (slug, callback) { - if (Array.isArray(slug)) { - db.isObjectFields('groupslug:groupname', slug, callback); - } else { - db.isObjectField('groupslug:groupname', slug, callback); - } - }; - - Groups.getLatestMemberPosts = function (groupName, max, uid, callback) { - async.waterfall([ + async.parallel([ function (next) { - Groups.getMembers(groupName, 0, -1, next); - }, - function (uids, next) { - if (!Array.isArray(uids) || !uids.length) { - return callback(null, []); - } - var keys = uids.map(function (uid) { - return 'uid:' + uid + ':posts'; - }); - db.getSortedSetRevRange(keys, 0, max - 1, next); - }, - function (pids, next) { - privileges.posts.filter('read', pids, uid, next); - }, - function (pids, next) { - posts.getPostSummaryByPids(pids, uid, { stripTags: false }, next); + next(null, slugs.map(function (slug) { + return Groups.ephemeralGroups.indexOf(slug) !== -1; + })); }, - ], callback); - }; - - Groups.getGroupData = function (groupName, callback) { - Groups.getGroupsData([groupName], function (err, groupsData) { - callback(err, Array.isArray(groupsData) && groupsData[0] ? groupsData[0] : null); - }); - }; - - Groups.getGroupsData = function (groupNames, callback) { - if (!Array.isArray(groupNames) || !groupNames.length) { - return callback(null, []); - } - - var keys = groupNames.map(function (groupName) { - return 'group:' + groupName; - }); - - var ephemeralIdx = groupNames.reduce(function (memo, cur, idx) { - if (ephemeralGroups.indexOf(cur) !== -1) { - memo.push(idx); - } - return memo; - }, []); - - db.getObjects(keys, function (err, groupData) { + async.apply(db.isSortedSetMembers, 'groups:createtime', name), + ], function (err, results) { if (err) { return callback(err); } - - if (ephemeralIdx.length) { - ephemeralIdx.forEach(function (idx) { - groupData[idx] = internals.getEphemeralGroup(groupNames[idx]); - }); - } - - groupData.forEach(function (group) { - if (group) { - Groups.escapeGroupData(group); - group.userTitleEnabled = group.userTitleEnabled ? parseInt(group.userTitleEnabled, 10) === 1 : true; - group.labelColor = validator.escape(String(group.labelColor || '#000000')); - group.icon = validator.escape(String(group.icon || '')); - group.createtimeISO = utils.toISOString(group.createtime); - group.hidden = parseInt(group.hidden, 10) === 1; - group.system = parseInt(group.system, 10) === 1; - group.private = (group.private === null || group.private === undefined) ? true : !!parseInt(group.private, 10); - group.disableJoinRequests = parseInt(group.disableJoinRequests, 10) === 1; - - group['cover:url'] = group['cover:url'] || require('./coverPhoto').getDefaultGroupCover(group.name); - group['cover:thumb:url'] = group['cover:thumb:url'] || group['cover:url']; - group['cover:position'] = validator.escape(String(group['cover:position'] || '50% 50%')); - } - }); - - plugins.fireHook('filter:groups.get', { groups: groupData }, function (err, data) { - callback(err, data ? data.groups : null); - }); + callback(null, name.map(function (n, index) { + return results[0][index] || results[1][index]; + })); }); - }; - - Groups.getUserGroups = function (uids, callback) { - Groups.getUserGroupsFromSet('groups:visible:createtime', uids, callback); - }; - - Groups.getUserGroupsFromSet = function (set, uids, callback) { - async.waterfall([ + } else { + var slug = utils.slugify(name); + async.parallel([ function (next) { - db.getSortedSetRevRange(set, 0, -1, next); - }, - function (groupNames, next) { - async.map(uids, function (uid, next) { - Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) { - if (err) { - return next(err); - } - - var memberOf = []; - isMembers.forEach(function (isMember, index) { - if (isMember) { - memberOf.push(groupNames[index]); - } - }); - - Groups.getGroupsData(memberOf, next); - }); - }, next); + next(null, Groups.ephemeralGroups.indexOf(slug) !== -1); }, - ], callback); - }; -}(module.exports)); + async.apply(db.isSortedSetMember, 'groups:createtime', name), + ], function (err, results) { + callback(err, !err ? (results[0] || results[1]) : null); + }); + } +}; + +Groups.existsBySlug = function (slug, callback) { + if (Array.isArray(slug)) { + db.isObjectFields('groupslug:groupname', slug, callback); + } else { + db.isObjectField('groupslug:groupname', slug, callback); + } +}; diff --git a/src/groups/data.js b/src/groups/data.js new file mode 100644 index 0000000000..24e7f99a02 --- /dev/null +++ b/src/groups/data.js @@ -0,0 +1,93 @@ +'use strict'; + +var async = require('async'); +var validator = require('validator'); + +var db = require('../database'); +var plugins = require('../plugins'); +var utils = require('../../public/src/utils'); + +module.exports = function (Groups) { + Groups.getGroupsData = function (groupNames, callback) { + if (!Array.isArray(groupNames) || !groupNames.length) { + return callback(null, []); + } + + var keys = groupNames.map(function (groupName) { + return 'group:' + groupName; + }); + + var ephemeralIdx = groupNames.reduce(function (memo, cur, idx) { + if (Groups.ephemeralGroups.indexOf(cur) !== -1) { + memo.push(idx); + } + return memo; + }, []); + + async.waterfall([ + function (next) { + db.getObjects(keys, next); + }, + function (groupData, next) { + if (ephemeralIdx.length) { + ephemeralIdx.forEach(function (idx) { + groupData[idx] = Groups.getEphemeralGroup(groupNames[idx]); + }); + } + + groupData.forEach(function (group) { + if (group) { + Groups.escapeGroupData(group); + group.userTitleEnabled = group.userTitleEnabled ? parseInt(group.userTitleEnabled, 10) === 1 : true; + group.labelColor = validator.escape(String(group.labelColor || '#000000')); + group.icon = validator.escape(String(group.icon || '')); + group.createtimeISO = utils.toISOString(group.createtime); + group.hidden = parseInt(group.hidden, 10) === 1; + group.system = parseInt(group.system, 10) === 1; + group.private = (group.private === null || group.private === undefined) ? true : !!parseInt(group.private, 10); + group.disableJoinRequests = parseInt(group.disableJoinRequests, 10) === 1; + + group['cover:url'] = group['cover:url'] || require('../coverPhoto').getDefaultGroupCover(group.name); + group['cover:thumb:url'] = group['cover:thumb:url'] || group['cover:url']; + group['cover:position'] = validator.escape(String(group['cover:position'] || '50% 50%')); + } + }); + + plugins.fireHook('filter:groups.get', { groups: groupData }, next); + }, + function (results, next) { + next(null, results.groups); + }, + ], callback); + }; + + Groups.getGroupData = function (groupName, callback) { + Groups.getGroupsData([groupName], function (err, groupsData) { + callback(err, Array.isArray(groupsData) && groupsData[0] ? groupsData[0] : null); + }); + }; + + Groups.getGroupFields = function (groupName, fields, callback) { + Groups.getMultipleGroupFields([groupName], fields, function (err, groups) { + callback(err, groups ? groups[0] : null); + }); + }; + + Groups.getMultipleGroupFields = function (groups, fields, callback) { + db.getObjectsFields(groups.map(function (group) { + return 'group:' + group; + }), fields, callback); + }; + + Groups.setGroupField = function (groupName, field, value, callback) { + async.waterfall([ + function (next) { + db.setObjectField('group:' + groupName, field, value, next); + }, + function (next) { + plugins.fireHook('action:group.set', { field: field, value: value, type: 'set' }); + next(); + }, + ], callback); + }; +}; diff --git a/src/groups/membership.js b/src/groups/membership.js index 31a32da17c..6bcafd2343 100644 --- a/src/groups/membership.js +++ b/src/groups/membership.js @@ -413,32 +413,33 @@ module.exports = function (Groups) { }; Groups.getMemberCount = function (groupName, callback) { - db.getObjectField('group:' + groupName, 'memberCount', function (err, count) { - if (err) { - return callback(err); - } - callback(null, parseInt(count, 10)); - }); + async.waterfall([ + function (next) { + db.getObjectField('group:' + groupName, 'memberCount', next); + }, + function (count, next) { + next(null, parseInt(count, 10)); + }, + ], callback); }; Groups.isMemberOfGroupList = function (uid, groupListKey, callback) { - db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function (err, groupNames) { - if (err) { - return callback(err); - } - groupNames = Groups.internals.removeEphemeralGroups(groupNames); - if (groupNames.length === 0) { - return callback(null, false); - } - - Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, next); + }, + function (groupNames, next) { + groupNames = Groups.removeEphemeralGroups(groupNames); + if (groupNames.length === 0) { + return callback(null, false); } - callback(null, isMembers.indexOf(true) !== -1); - }); - }); + Groups.isMemberOfGroups(uid, groupNames, next); + }, + function (isMembers, next) { + next(null, isMembers.indexOf(true) !== -1); + }, + ], callback); }; Groups.isMemberOfGroupsList = function (uid, groupListKeys, callback) { @@ -446,19 +447,20 @@ module.exports = function (Groups) { return 'group:' + groupName + ':members'; }); - db.getSortedSetsMembers(sets, function (err, members) { - if (err) { - return callback(err); - } - - var uniqueGroups = _.unique(_.flatten(members)); - uniqueGroups = Groups.internals.removeEphemeralGroups(uniqueGroups); - - Groups.isMemberOfGroups(uid, uniqueGroups, function (err, isMembers) { - if (err) { - return callback(err); - } + var uniqueGroups; + var members; + async.waterfall([ + function (next) { + db.getSortedSetsMembers(sets, next); + }, + function (_members, next) { + members = _members; + uniqueGroups = _.unique(_.flatten(members)); + uniqueGroups = Groups.removeEphemeralGroups(uniqueGroups); + Groups.isMemberOfGroups(uid, uniqueGroups, next); + }, + function (isMembers, next) { var map = {}; uniqueGroups.forEach(function (groupName, index) { @@ -474,62 +476,63 @@ module.exports = function (Groups) { return false; }); - callback(null, result); - }); - }); + next(null, result); + }, + ], callback); }; Groups.isMembersOfGroupList = function (uids, groupListKey, callback) { - db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function (err, groupNames) { - if (err) { - return callback(err); - } + var groupNames; + var results = []; + uids.forEach(function () { + results.push(false); + }); - var results = []; - uids.forEach(function () { - results.push(false); - }); + async.waterfall([ + function (next) { + db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, next); + }, + function (_groupNames, next) { + groupNames = Groups.removeEphemeralGroups(_groupNames); - groupNames = Groups.internals.removeEphemeralGroups(groupNames); - if (groupNames.length === 0) { - return callback(null, results); - } + if (groupNames.length === 0) { + return callback(null, results); + } - async.each(groupNames, function (groupName, next) { - Groups.isMembers(uids, groupName, function (err, isMembers) { - if (err) { - return next(err); - } + async.map(groupNames, function (groupName, next) { + Groups.isMembers(uids, groupName, next); + }, next); + }, + function (isGroupMembers, next) { + isGroupMembers.forEach(function (isMembers) { results.forEach(function (isMember, index) { if (!isMember && isMembers[index]) { results[index] = true; } }); - next(); }); - }, function (err) { - callback(err, results); - }); - }); + next(null, results); + }, + ], callback); }; Groups.isInvited = function (uid, groupName, callback) { if (!uid) { - return callback(null, false); + return setImmediate(callback, null, false); } db.isSetMember('group:' + groupName + ':invited', uid, callback); }; Groups.isPending = function (uid, groupName, callback) { if (!uid) { - return callback(null, false); + return setImmediate(callback, null, false); } db.isSetMember('group:' + groupName + ':pending', uid, callback); }; Groups.getPending = function (groupName, callback) { if (!groupName) { - return callback(null, []); + return setImmediate(callback, null, []); } db.getSetMembers('group:' + groupName + ':pending', callback); }; diff --git a/src/groups/posts.js b/src/groups/posts.js new file mode 100644 index 0000000000..6f4d65520d --- /dev/null +++ b/src/groups/posts.js @@ -0,0 +1,32 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var privileges = require('../privileges'); +var posts = require('../posts'); + +module.exports = function (Groups) { + Groups.getLatestMemberPosts = function (groupName, max, uid, callback) { + async.waterfall([ + function (next) { + Groups.getMembers(groupName, 0, -1, next); + }, + function (uids, next) { + if (!Array.isArray(uids) || !uids.length) { + return callback(null, []); + } + var keys = uids.map(function (uid) { + return 'uid:' + uid + ':posts'; + }); + db.getSortedSetRevRange(keys, 0, max - 1, next); + }, + function (pids, next) { + privileges.posts.filter('read', pids, uid, next); + }, + function (pids, next) { + posts.getPostSummaryByPids(pids, uid, { stripTags: false }, next); + }, + ], callback); + }; +}; diff --git a/src/groups/search.js b/src/groups/search.js index 3c580b5e8d..62df79696c 100644 --- a/src/groups/search.js +++ b/src/groups/search.js @@ -16,7 +16,7 @@ module.exports = function (Groups) { async.apply(db.getObjectValues, 'groupslug:groupname'), function (groupNames, next) { // Ephemeral groups and the registered-users groups are searchable - groupNames = Groups.getEphemeralGroups().concat(groupNames).concat('registered-users'); + groupNames = Groups.ephemeralGroups.concat(groupNames).concat('registered-users'); groupNames = groupNames.filter(function (name) { return name.toLowerCase().indexOf(query) !== -1 && name !== 'administrators' && !Groups.isPrivilegeGroup(name); }); diff --git a/src/groups/update.js b/src/groups/update.js index 99e8dcc65a..9bf53886e5 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -91,16 +91,18 @@ module.exports = function (Groups) { async.apply(db.sortedSetRemove, 'groups:visible:name', groupName.toLowerCase() + ':' + groupName), ], callback); } else { - db.getObjectFields('group:' + groupName, ['createtime', 'memberCount'], function (err, groupData) { - if (err) { - return callback(err); - } - async.parallel([ - async.apply(db.sortedSetAdd, 'groups:visible:createtime', groupData.createtime, groupName), - async.apply(db.sortedSetAdd, 'groups:visible:memberCount', groupData.memberCount, groupName), - async.apply(db.sortedSetAdd, 'groups:visible:name', 0, groupName.toLowerCase() + ':' + groupName), - ], callback); - }); + async.waterfall([ + function (next) { + db.getObjectFields('group:' + groupName, ['createtime', 'memberCount'], next); + }, + function (groupData, next) { + async.parallel([ + async.apply(db.sortedSetAdd, 'groups:visible:createtime', groupData.createtime, groupName), + async.apply(db.sortedSetAdd, 'groups:visible:memberCount', groupData.memberCount, groupName), + async.apply(db.sortedSetAdd, 'groups:visible:name', 0, groupName.toLowerCase() + ':' + groupName), + ], next); + }, + ], callback); } } @@ -155,40 +157,48 @@ module.exports = function (Groups) { function checkNameChange(currentName, newName, callback) { if (currentName === newName) { - return callback(); + return setImmediate(callback); } var currentSlug = utils.slugify(currentName); var newSlug = utils.slugify(newName); if (currentSlug === newSlug) { - return callback(); + return setImmediate(callback); } - Groups.existsBySlug(newSlug, function (err, exists) { - if (err || exists) { - return callback(err || new Error('[[error:group-already-exists]]')); - } - callback(); - }); + async.waterfall([ + function (next) { + Groups.existsBySlug(newSlug, next); + }, + function (exists, next) { + next(exists ? new Error('[[error:group-already-exists]]') : null); + }, + ], callback); } function renameGroup(oldName, newName, callback) { if (oldName === newName || !newName || newName.length === 0) { - return callback(); + return setImmediate(callback); } + var group; + async.waterfall([ + function (next) { + db.getObject('group:' + oldName, next); + }, + function (_group, next) { + group = _group; + if (!group) { + return callback(); + } - db.getObject('group:' + oldName, function (err, group) { - if (err || !group) { - return callback(err); - } - - if (parseInt(group.system, 10) === 1) { - return callback(); - } - - Groups.exists(newName, function (err, exists) { - if (err || exists) { - return callback(err || new Error('[[error:group-already-exists]]')); + if (parseInt(group.system, 10) === 1) { + return callback(new Error('[[error:not-allowed-to-rename-system-group]]')); } + Groups.exists(newName, next); + }, + function (exists, next) { + if (exists) { + return callback(new Error('[[error:group-already-exists]]')); + } async.series([ async.apply(db.setObjectField, 'group:' + oldName, 'name', newName), async.apply(db.setObjectField, 'group:' + oldName, 'slug', utils.slugify(newName)), @@ -222,29 +232,33 @@ module.exports = function (Groups) { next(); }, - ], callback); - }); + ], next); + }, + ], function (err) { + callback(err); }); } function renameGroupMember(group, oldName, newName, callback) { - db.isSortedSetMember(group, oldName, function (err, isMember) { - if (err || !isMember) { - return callback(err); - } - var score; - async.waterfall([ - function (next) { - db.sortedSetScore(group, oldName, next); - }, - function (_score, next) { - score = _score; - db.sortedSetRemove(group, oldName, next); - }, - function (next) { - db.sortedSetAdd(group, score, newName, next); - }, - ], callback); - }); + var score; + async.waterfall([ + function (next) { + db.isSortedSetMember(group, oldName, next); + }, + function (isMember, next) { + if (!isMember) { + return callback(); + } + + db.sortedSetScore(group, oldName, next); + }, + function (_score, next) { + score = _score; + db.sortedSetRemove(group, oldName, next); + }, + function (next) { + db.sortedSetAdd(group, score, newName, next); + }, + ], callback); } }; diff --git a/src/groups/user.js b/src/groups/user.js new file mode 100644 index 0000000000..9a68478ade --- /dev/null +++ b/src/groups/user.js @@ -0,0 +1,50 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var user = require('../user'); + +module.exports = function (Groups) { + Groups.getUsersFromSet = function (set, callback) { + async.waterfall([ + function (next) { + db.getSetMembers(set, next); + }, + function (uids, next) { + user.getUsersData(uids, next); + }, + ], callback); + }; + + Groups.getUserGroups = function (uids, callback) { + Groups.getUserGroupsFromSet('groups:visible:createtime', uids, callback); + }; + + Groups.getUserGroupsFromSet = function (set, uids, callback) { + async.waterfall([ + function (next) { + db.getSortedSetRevRange(set, 0, -1, next); + }, + function (groupNames, next) { + async.map(uids, function (uid, next) { + async.waterfall([ + function (next) { + Groups.isMemberOfGroups(uid, groupNames, next); + }, + function (isMembers, next) { + var memberOf = []; + isMembers.forEach(function (isMember, index) { + if (isMember) { + memberOf.push(groupNames[index]); + } + }); + + Groups.getGroupsData(memberOf, next); + }, + ], next); + }, next); + }, + ], callback); + }; +}; diff --git a/src/messaging.js b/src/messaging.js index e141adff4f..e507609d81 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -102,7 +102,7 @@ Messaging.isNewSet = function (uid, roomId, timestamp, callback) { }, function (messages, next) { if (messages && messages.length) { - next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + (1000 * 60 * 5)); + next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + Messaging.newMessageCutoff); } else { next(null, true); } diff --git a/src/messaging/data.js b/src/messaging/data.js index e3cb67fbd8..98d4dcded8 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -8,6 +8,8 @@ var user = require('../user'); var utils = require('../../public/src/utils'); module.exports = function (Messaging) { + Messaging.newMessageCutoff = 1000 * 60 * 3; + Messaging.getMessageField = function (mid, field, callback) { Messaging.getMessageFields(mid, [field], function (err, fields) { callback(err, fields ? fields[field] : null); @@ -80,7 +82,7 @@ module.exports = function (Messaging) { // Add a spacer in between messages with time gaps between them messages = messages.map(function (message, index) { // Compare timestamps with the previous message, and check if a spacer needs to be added - if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index - 1].timestamp, 10) + (1000 * 60 * 5)) { + if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index - 1].timestamp, 10) + Messaging.newMessageCutoff) { // If it's been 5 minutes, this is a new set of messages message.newSet = true; } else if (index > 0 && message.fromuid !== messages[index - 1].fromuid) { @@ -115,7 +117,7 @@ module.exports = function (Messaging) { } if ( - (parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000 * 60 * 5)) || + (parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + Messaging.newMessageCutoff) || (parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10)) ) { // If it's been 5 minutes, this is a new set of messages diff --git a/src/meta/js.js b/src/meta/js.js index b82aa34d25..dffd927055 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -58,6 +58,7 @@ module.exports = function (Meta) { 'public/src/client/topic/fork.js', 'public/src/client/topic/move.js', 'public/src/client/topic/posts.js', + 'public/src/client/topic/images.js', 'public/src/client/topic/postTools.js', 'public/src/client/topic/threadTools.js', 'public/src/client/categories.js', diff --git a/src/meta/tags.js b/src/meta/tags.js index 56269633ae..5b1097d427 100644 --- a/src/meta/tags.js +++ b/src/meta/tags.js @@ -61,6 +61,14 @@ module.exports = function (Meta) { href: nconf.get('relative_path') + '/manifest.json', }]; + if (plugins.hasListeners('filter:search.query')) { + defaultLinks.push({ + rel: 'search', + type: 'application/opensearchdescription+xml', + href: nconf.get('relative_path') + '/osd.xml', + }); + } + // Touch icons for mobile-devices if (Meta.config['brand:touchIcon']) { defaultLinks.push({ @@ -131,10 +139,10 @@ module.exports = function (Meta) { } }); - if (!hasDescription) { + if (!hasDescription && Meta.config.description) { meta.push({ name: 'description', - content: validator.escape(String(Meta.config.description || '')), + content: validator.escape(String(Meta.config.description)), }); } } diff --git a/src/meta/themes.js b/src/meta/themes.js index e764010454..0d3be261e0 100644 --- a/src/meta/themes.js +++ b/src/meta/themes.js @@ -114,7 +114,7 @@ module.exports = function (Meta) { themeData['theme:templates'] = config.templates ? config.templates : ''; themeData['theme:src'] = ''; - db.setObject('config', themeData, next); + Meta.configs.setMultiple(themeData, next); // Re-set the themes path (for when NodeBB is reloaded) Meta.themes.setPath(config); @@ -125,7 +125,10 @@ module.exports = function (Meta) { break; case 'bootswatch': - Meta.configs.set('theme:src', data.src, callback); + Meta.configs.setMultiple({ + 'theme:src': data.src, + bootswatchSkin: data.id.toLowerCase(), + }, callback); break; } }; diff --git a/src/middleware/header.js b/src/middleware/header.js index accbbf2fe6..e5d4de66bb 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -42,7 +42,6 @@ module.exports = function (middleware) { middleware.renderHeader = function (req, res, data, callback) { var registrationType = meta.config.registrationType || 'normal'; var templateValues = { - bootswatchCSS: meta.config['theme:src'], title: meta.config.title || '', description: meta.config.description || '', 'cache-buster': meta.config['cache-buster'] || '', @@ -117,9 +116,7 @@ module.exports = function (middleware) { results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1; results.user.isEmailConfirmSent = !!results.isEmailConfirmSent; - if (res.locals.config && parseInt(meta.config.disableCustomUserSkins, 10) !== 1 && res.locals.config.bootswatchSkin !== 'default') { - templateValues.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + res.locals.config.bootswatchSkin + '/bootstrap.min.css'; - } + setBootswatchCSS(templateValues, res.locals.config); templateValues.browserTitle = controllers.helpers.buildTitle(data.title); templateValues.navigation = results.navigation; @@ -191,5 +188,21 @@ module.exports = function (middleware) { return title; } + + function setBootswatchCSS(obj, config) { + if (config && config.bootswatchSkin !== 'noskin') { + var skinToUse = ''; + + if (parseInt(meta.config.disableCustomUserSkins, 10) !== 1) { + skinToUse = config.bootswatchSkin; + } else if (meta.config.bootswatchSkin) { + skinToUse = meta.config.bootswatchSkin; + } + + if (skinToUse) { + obj.bootswatchCSS = '//maxcdn.bootstrapcdn.com/bootswatch/latest/' + skinToUse + '/bootstrap.min.css'; + } + } + } }; diff --git a/src/middleware/index.js b/src/middleware/index.js index 5a3f9aa5ba..de0bd6797e 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -53,22 +53,24 @@ middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) { The "self" part of this middleware hinges on you having used middleware.exposeUid prior to invoking this middleware. */ - if (req.user) { - if (req.user.uid === res.locals.uid) { - return next(); - } + async.waterfall([ + function (next) { + if (!req.uid) { + return setImmediate(next, null, false); + } - user.isAdminOrGlobalMod(req.uid, function (err, ok) { - if (err) { - return next(err); - } else if (ok) { - return next(); + if (req.uid === parseInt(res.locals.uid, 10)) { + return setImmediate(next, null, true); } - controllers.helpers.notAllowed(req, res); - }); - } else { - controllers.helpers.notAllowed(req, res); - } + user.isAdminOrGlobalMod(req.uid, next); + }, + function (isAdminOrGlobalMod, next) { + if (!isAdminOrGlobalMod) { + return controllers.helpers.notAllowed(req, res); + } + next(); + }, + ], next); }; middleware.ensureSelfOrPrivileged = function (req, res, next) { diff --git a/src/middleware/render.js b/src/middleware/render.js index 48d4a0b526..88e9879887 100644 --- a/src/middleware/render.js +++ b/src/middleware/render.js @@ -123,7 +123,7 @@ module.exports = function (middleware) { winston.error(err.message); p = ''; } - + p = validator.escape(String(p)); parts[index] = index ? parts[0] + '-' + p : 'page-' + (p || 'home'); }); return parts.join(' '); diff --git a/src/pagination.js b/src/pagination.js index 3e57c5b201..4058e7f8f4 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -1,6 +1,7 @@ 'use strict'; var qs = require('querystring'); +var _ = require('underscore'); var pagination = {}; @@ -37,7 +38,7 @@ pagination.create = function (currentPage, pageCount, queryObj) { return a - b; }); - queryObj = queryObj || {}; + queryObj = _.clone(queryObj || {}); delete queryObj._; diff --git a/src/posts/parse.js b/src/posts/parse.js index 946c589056..8ac2028bbd 100644 --- a/src/posts/parse.js +++ b/src/posts/parse.js @@ -1,5 +1,6 @@ 'use strict'; +var async = require('async'); var nconf = require('nconf'); var url = require('url'); var winston = require('winston'); @@ -14,31 +15,26 @@ var urlRegex = /href="([^"]+)"/g; module.exports = function (Posts) { Posts.parsePost = function (postData, callback) { - postData.content = postData.content || ''; + postData.content = String(postData.content || ''); if (postData.pid && cache.has(String(postData.pid))) { postData.content = cache.get(String(postData.pid)); return callback(null, postData); } - // Casting post content into a string, just in case - if (typeof postData.content !== 'string') { - postData.content = postData.content.toString(); - } - - plugins.fireHook('filter:parse.post', { postData: postData }, function (err, data) { - if (err) { - return callback(err); - } - - data.postData.content = translator.escape(data.postData.content); + async.waterfall([ + function (next) { + plugins.fireHook('filter:parse.post', { postData: postData }, next); + }, + function (data, next) { + data.postData.content = translator.escape(data.postData.content); - if (global.env === 'production' && data.postData.pid) { - cache.set(String(data.postData.pid), data.postData.content); - } - - callback(null, data.postData); - }); + if (global.env === 'production' && data.postData.pid) { + cache.set(String(data.postData.pid), data.postData.content); + } + next(null, data.postData); + }, + ], callback); }; Posts.parseSignature = function (userData, uid, callback) { @@ -51,7 +47,6 @@ module.exports = function (Posts) { var parsed; var current = urlRegex.exec(content); var absolute; - while (current !== null) { if (current[1]) { try { @@ -78,7 +73,7 @@ module.exports = function (Posts) { }; function sanitizeSignature(signature) { - var string = S(signature); + var string = S(signature); var tagsToStrip = []; if (parseInt(meta.config['signatures:disableLinks'], 10) === 1) { diff --git a/src/privileges/categories.js b/src/privileges/categories.js index ff3d16148f..6678f5d5aa 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -96,7 +96,7 @@ module.exports = function (privileges) { return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1; }); - groupNames = groups.getEphemeralGroups().concat(groupNames); + groupNames = groups.ephemeralGroups.concat(groupNames); var registeredUsersIndex = groupNames.indexOf('registered-users'); if (registeredUsersIndex !== -1) { groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]); @@ -155,34 +155,36 @@ module.exports = function (privileges) { privileges.categories.get = function (cid, uid, callback) { var privs = ['topics:create', 'topics:read', 'read']; - async.parallel({ - privileges: function (next) { - helpers.isUserAllowedTo(privs, uid, cid, next); - }, - isAdministrator: function (next) { - user.isAdministrator(uid, next); + async.waterfall([ + function (next) { + async.parallel({ + privileges: function (next) { + helpers.isUserAllowedTo(privs, uid, cid, next); + }, + isAdministrator: function (next) { + user.isAdministrator(uid, next); + }, + isModerator: function (next) { + user.isModerator(uid, cid, next); + }, + }, next); }, - isModerator: function (next) { - user.isModerator(uid, cid, next); + function (results, next) { + var privData = _.object(privs, results.privileges); + var isAdminOrMod = results.isAdministrator || results.isModerator; + + plugins.fireHook('filter:privileges.categories.get', { + 'topics:create': privData['topics:create'] || isAdminOrMod, + 'topics:read': privData['topics:read'] || isAdminOrMod, + read: privData.read || isAdminOrMod, + cid: cid, + uid: uid, + editable: isAdminOrMod, + view_deleted: isAdminOrMod, + isAdminOrMod: isAdminOrMod, + }, next); }, - }, function (err, results) { - if (err) { - return callback(err); - } - var privData = _.object(privs, results.privileges); - var isAdminOrMod = results.isAdministrator || results.isModerator; - - plugins.fireHook('filter:privileges.categories.get', { - 'topics:create': privData['topics:create'] || isAdminOrMod, - 'topics:read': privData['topics:read'] || isAdminOrMod, - read: privData.read || isAdminOrMod, - cid: cid, - uid: uid, - editable: isAdminOrMod, - view_deleted: isAdminOrMod, - isAdminOrMod: isAdminOrMod, - }, callback); - }); + ], callback); }; privileges.categories.isAdminOrMod = function (cid, uid, callback) { @@ -213,29 +215,29 @@ module.exports = function (privileges) { return callback(null, false); } - categories.getCategoryField(cid, 'disabled', function (err, disabled) { - if (err) { - return callback(err); - } - - if (parseInt(disabled, 10) === 1) { - return callback(null, false); - } - - helpers.some([ - function (next) { - helpers.isUserAllowedTo(privilege, uid, [cid], function (err, results) { - next(err, Array.isArray(results) && results.length ? results[0] : false); - }); - }, - function (next) { - user.isModerator(uid, cid, next); - }, - function (next) { - user.isAdministrator(uid, next); - }, - ], callback); - }); + async.waterfall([ + function (next) { + categories.getCategoryField(cid, 'disabled', next); + }, + function (disabled, next) { + if (parseInt(disabled, 10) === 1) { + return callback(null, false); + } + helpers.some([ + function (next) { + helpers.isUserAllowedTo(privilege, uid, [cid], function (err, results) { + next(err, Array.isArray(results) && results.length ? results[0] : false); + }); + }, + function (next) { + user.isModerator(uid, cid, next); + }, + function (next) { + user.isAdministrator(uid, next); + }, + ], next); + }, + ], callback); }; privileges.categories.filterCids = function (privilege, cids, uid, callback) { @@ -247,18 +249,19 @@ module.exports = function (privileges) { return array.indexOf(cid) === index; }); - privileges.categories.getBase(privilege, cids, uid, function (err, results) { - if (err) { - return callback(err); - } - - cids = cids.filter(function (cid, index) { - return !results.categories[index].disabled && - (results.allowedTo[index] || results.isAdmin || results.isModerators[index]); - }); + async.waterfall([ + function (next) { + privileges.categories.getBase(privilege, cids, uid, next); + }, + function (results, next) { + cids = cids.filter(function (cid, index) { + return !results.categories[index].disabled && + (results.allowedTo[index] || results.isAdmin || results.isModerators[index]); + }); - callback(null, cids.filter(Boolean)); - }); + next(null, cids.filter(Boolean)); + }, + ], callback); }; privileges.categories.getBase = function (privilege, cids, uid, callback) { @@ -287,26 +290,27 @@ module.exports = function (privileges) { return array.indexOf(uid) === index; }); - async.parallel({ - allowedTo: function (next) { - helpers.isUsersAllowedTo(privilege, uids, cid, next); - }, - isModerators: function (next) { - user.isModerator(uids, cid, next); + async.waterfall([ + function (next) { + async.parallel({ + allowedTo: function (next) { + helpers.isUsersAllowedTo(privilege, uids, cid, next); + }, + isModerators: function (next) { + user.isModerator(uids, cid, next); + }, + isAdmin: function (next) { + user.isAdministrator(uids, next); + }, + }, next); }, - isAdmin: function (next) { - user.isAdministrator(uids, next); + function (results, next) { + uids = uids.filter(function (uid, index) { + return results.allowedTo[index] || results.isModerators[index] || results.isAdmin[index]; + }); + next(null, uids); }, - }, function (err, results) { - if (err) { - return callback(err); - } - - uids = uids.filter(function (uid, index) { - return results.allowedTo[index] || results.isModerators[index] || results.isAdmin[index]; - }); - callback(null, uids); - }); + ], callback); }; privileges.categories.give = function (privileges, cid, groupName, callback) { @@ -324,23 +328,24 @@ module.exports = function (privileges) { } privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) { - async.parallel({ - isAdministrator: function (next) { - user.isAdministrator(uid, next); - }, - moderatorOfCurrent: function (next) { - user.isModerator(uid, currentCid, next); + async.waterfall([ + function (next) { + async.parallel({ + isAdministrator: function (next) { + user.isAdministrator(uid, next); + }, + moderatorOfCurrent: function (next) { + user.isModerator(uid, currentCid, next); + }, + moderatorOfTarget: function (next) { + user.isModerator(uid, targetCid, next); + }, + }, next); }, - moderatorOfTarget: function (next) { - user.isModerator(uid, targetCid, next); + function (results, next) { + next(null, results.isAdministrator || (results.moderatorOfCurrent && results.moderatorOfTarget)); }, - }, function (err, results) { - if (err) { - return callback(err); - } - - callback(null, results.isAdministrator || (results.moderatorOfCurrent && results.moderatorOfTarget)); - }); + ], callback); }; privileges.categories.userPrivileges = function (cid, uid, callback) { diff --git a/src/routes/api.js b/src/routes/api.js index 16ead5c588..9b5a7f77c7 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -11,17 +11,17 @@ module.exports = function (app, middleware, controllers) { router.get('/config', middleware.applyCSRF, controllers.api.getConfig); router.get('/widgets/render', controllers.api.renderWidgets); - router.get('/me', middleware.checkGlobalPrivacySettings, controllers.api.getCurrentUser); - router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.api.getUserByUID); - router.get('/user/username/:username', middleware.checkGlobalPrivacySettings, controllers.api.getUserByUsername); - router.get('/user/email/:email', middleware.checkGlobalPrivacySettings, controllers.api.getUserByEmail); + router.get('/me', middleware.checkGlobalPrivacySettings, controllers.user.getCurrentUser); + router.get('/user/uid/:uid', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUID); + router.get('/user/username/:username', middleware.checkGlobalPrivacySettings, controllers.user.getUserByUsername); + router.get('/user/email/:email', middleware.checkGlobalPrivacySettings, controllers.user.getUserByEmail); router.get('/:type/pid/:id', controllers.api.getObject); router.get('/:type/tid/:id', controllers.api.getObject); router.get('/:type/cid/:id', controllers.api.getObject); router.get('/categories/:cid/moderators', controllers.api.getModerators); - router.get('/recent/posts/:term?', controllers.api.getRecentPosts); + router.get('/recent/posts/:term?', controllers.posts.getRecentPosts); router.get('/unread/:filter?/total', middleware.authenticate, controllers.unread.unreadTotal); router.get('/topic/teaser/:topic_id', controllers.topics.teaser); router.get('/topic/pagination/:topic_id', controllers.topics.pagination); diff --git a/src/routes/index.js b/src/routes/index.js index f1a833fbf6..6d5fcfc018 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -199,9 +199,9 @@ module.exports = function (app, middleware, hotswapIds) { }); app.use(relativePath + '/assets/vendor/jquery/timeago/locales', middleware.processTimeagoLocales); - app.use(controllers.handle404); - app.use(controllers.handleURIErrors); - app.use(controllers.handleErrors); + app.use(controllers['404'].handle404); + app.use(controllers.errors.handleURIErrors); + app.use(controllers.errors.handleErrors); // Add plugin routes async.series([ diff --git a/src/routes/meta.js b/src/routes/meta.js index cfeeac5b9b..de0bb52406 100644 --- a/src/routes/meta.js +++ b/src/routes/meta.js @@ -8,4 +8,5 @@ module.exports = function (app, middleware, controllers) { app.get('/robots.txt', controllers.robots); app.get('/manifest.json', controllers.manifest); app.get('/css/previews/:theme', controllers.admin.themes.get); + app.get('/osd.xml', controllers.osd.handle); }; diff --git a/src/socket.io/posts/edit.js b/src/socket.io/posts/edit.js index 4d5602effe..5ab6973ce1 100644 --- a/src/socket.io/posts/edit.js +++ b/src/socket.io/posts/edit.js @@ -3,6 +3,7 @@ var async = require('async'); var validator = require('validator'); var _ = require('underscore'); +var S = require('string'); var posts = require('../../posts'); var groups = require('../../groups'); @@ -16,7 +17,12 @@ module.exports = function (SocketPosts) { return callback(new Error('[[error:not-logged-in]]')); } else if (!data || !data.pid || !data.content) { return callback(new Error('[[error:invalid-data]]')); - } else if (data.title && data.title.length < parseInt(meta.config.minimumTitleLength, 10)) { + } + + // Trim and remove HTML (latter for composers that send in HTML, like redactor) + var contentLen = S(data.content).stripTags().s.trim().length; + + if (data.title && data.title.length < parseInt(meta.config.minimumTitleLength, 10)) { return callback(new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]')); } else if (data.title && data.title.length > parseInt(meta.config.maximumTitleLength, 10)) { return callback(new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]')); @@ -24,9 +30,9 @@ module.exports = function (SocketPosts) { return callback(new Error('[[error:not-enough-tags, ' + meta.config.minimumTagsPerTopic + ']]')); } else if (data.tags && data.tags.length > parseInt(meta.config.maximumTagsPerTopic, 10)) { return callback(new Error('[[error:too-many-tags, ' + meta.config.maximumTagsPerTopic + ']]')); - } else if (!data.content || data.content.length < parseInt(meta.config.minimumPostLength, 10)) { + } else if (contentLen < parseInt(meta.config.minimumPostLength, 10)) { return callback(new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]')); - } else if (data.content.length > parseInt(meta.config.maximumPostLength, 10)) { + } else if (contentLen > parseInt(meta.config.maximumPostLength, 10)) { return callback(new Error('[[error:content-too-long, ' + meta.config.maximumPostLength + ']]')); } diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index 5eb9ce61dd..3b8a69d0f4 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -1,12 +1,14 @@ 'use strict'; +var async = require('async'); + var topics = require('../topics'); var websockets = require('./index'); var user = require('../user'); var apiController = require('../controllers/api'); var socketHelpers = require('./helpers'); -var SocketTopics = {}; +var SocketTopics = module.exports; require('./topics/unread')(SocketTopics); require('./topics/move')(SocketTopics); @@ -23,18 +25,19 @@ SocketTopics.post = function (socket, data, callback) { data.req = websockets.reqFromSocket(socket); data.timestamp = Date.now(); - topics.post(data, function (err, result) { - if (err) { - return callback(err); - } - - callback(null, result.topicData); + async.waterfall([ + function (next) { + topics.post(data, next); + }, + function (result, next) { + next(null, result.topicData); - socket.emit('event:new_post', { posts: [result.postData] }); - socket.emit('event:new_topic', result.topicData); + socket.emit('event:new_post', { posts: [result.postData] }); + socket.emit('event:new_topic', result.topicData); - socketHelpers.notifyNew(socket.uid, 'newTopic', { posts: [result.postData], topic: result.topicData }); - }); + socketHelpers.notifyNew(socket.uid, 'newTopic', { posts: [result.postData], topic: result.topicData }); + }, + ], callback); }; SocketTopics.postcount = function (socket, tid, callback) { @@ -61,7 +64,7 @@ SocketTopics.createTopicFromPosts = function (socket, data, callback) { }; SocketTopics.changeWatching = function (socket, data, callback) { - if (!data.tid || !data.type) { + if (!data || !data.tid || !data.type) { return callback(new Error('[[error:invalid-data]]')); } var commands = ['follow', 'unfollow', 'ignore']; @@ -90,20 +93,23 @@ SocketTopics.isFollowed = function (socket, tid, callback) { }; SocketTopics.search = function (socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } topics.search(data.tid, data.term, callback); }; SocketTopics.isModerator = function (socket, tid, callback) { - topics.getTopicField(tid, 'cid', function (err, cid) { - if (err) { - return callback(err); - } - user.isModerator(socket.uid, cid, callback); - }); + async.waterfall([ + function (next) { + topics.getTopicField(tid, 'cid', next); + }, + function (cid, next) { + user.isModerator(socket.uid, cid, next); + }, + ], callback); }; SocketTopics.getTopic = function (socket, tid, callback) { apiController.getTopicData(tid, socket.uid, callback); }; - -module.exports = SocketTopics; diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js index 023ad6266e..cf7c37e050 100644 --- a/src/socket.io/topics/infinitescroll.js +++ b/src/socket.io/topics/infinitescroll.js @@ -40,19 +40,19 @@ module.exports = function (SocketTopics) { var reverse = data.topicPostSort === 'newest_to_oldest' || data.topicPostSort === 'most_votes'; var start = Math.max(0, parseInt(data.after, 10)); - var infScrollPostsPerPage = 10; + var infScrollPostsPerPage = Math.max(0, Math.min(meta.config.postsPerPage, parseInt(data.postsPerPage, 10) || meta.config.postsPerPage) - 1); if (data.direction > 0) { if (reverse) { start = results.topic.postcount - start; } } else if (reverse) { - start = results.topic.postcount - start - infScrollPostsPerPage - 1; + start = results.topic.postcount - start - infScrollPostsPerPage; } else { - start = start - infScrollPostsPerPage - 1; + start = start - infScrollPostsPerPage; } - var stop = start + (infScrollPostsPerPage - 1); + var stop = start + (infScrollPostsPerPage); start = Math.max(0, start); stop = Math.max(0, stop); @@ -93,7 +93,7 @@ module.exports = function (SocketTopics) { } var start = parseInt(data.after, 10); - var stop = start + 9; + var stop = start + Math.max(0, Math.min(meta.config.topicsPerPage, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage) - 1); topics.getUnreadTopics({ cid: data.cid, uid: socket.uid, start: start, stop: stop, filter: data.filter }, callback); }; @@ -104,7 +104,7 @@ module.exports = function (SocketTopics) { } var start = parseInt(data.after, 10); - var stop = start + 9; + var stop = start + Math.max(0, Math.min(meta.config.topicsPerPage, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage) - 1); topics.getRecentTopics(data.cid, socket.uid, start, stop, data.filter, callback); }; @@ -115,7 +115,7 @@ module.exports = function (SocketTopics) { } var start = parseInt(data.after, 10); - var stop = start + 9; + var stop = start + Math.max(0, Math.min(meta.config.topicsPerPage, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage) - 1); topics.getTopicsFromSet(data.set, socket.uid, start, stop, callback); }; diff --git a/src/socket.io/user.js b/src/socket.io/user.js index f7208b116a..78f696a19b 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -12,7 +12,7 @@ var meta = require('../meta'); var events = require('../events'); var emailer = require('../emailer'); var db = require('../database'); -var apiController = require('../controllers/api'); +var userController = require('../controllers/user'); var privileges = require('../privileges'); var SocketUser = {}; @@ -303,15 +303,15 @@ SocketUser.invite = function (socket, email, callback) { }; SocketUser.getUserByUID = function (socket, uid, callback) { - apiController.getUserDataByField(socket.uid, 'uid', uid, callback); + userController.getUserDataByField(socket.uid, 'uid', uid, callback); }; SocketUser.getUserByUsername = function (socket, username, callback) { - apiController.getUserDataByField(socket.uid, 'username', username, callback); + userController.getUserDataByField(socket.uid, 'username', username, callback); }; SocketUser.getUserByEmail = function (socket, email, callback) { - apiController.getUserDataByField(socket.uid, 'email', email, callback); + userController.getUserDataByField(socket.uid, 'email', email, callback); }; SocketUser.setModerationNote = function (socket, data, callback) { diff --git a/src/topics.js b/src/topics.js index 0eba6545fd..164200016d 100644 --- a/src/topics.js +++ b/src/topics.js @@ -180,6 +180,7 @@ var social = require('./social'); isIgnoring: async.apply(Topics.isIgnoring, [topicData.tid], uid), bookmark: async.apply(Topics.getUserBookmark, topicData.tid, uid), postSharing: async.apply(social.getActivePostSharing), + deleter: async.apply(getDeleter, topicData), related: function (next) { async.waterfall([ function (next) { @@ -202,6 +203,8 @@ var social = require('./social'); topicData.isIgnoring = results.isIgnoring[0]; topicData.bookmark = results.bookmark; topicData.postSharing = results.postSharing; + topicData.deleter = results.deleter; + topicData.deletedTimestampISO = utils.toISOString(topicData.deletedTimestamp); topicData.related = results.related || []; topicData.unreplied = parseInt(topicData.postcount, 10) === 1; @@ -258,6 +261,13 @@ var social = require('./social'); ], callback); } + function getDeleter(topicData, callback) { + if (!topicData.deleterUid) { + return setImmediate(callback, null, null); + } + user.getUserFields(topicData.deleterUid, ['username', 'userslug', 'picture'], callback); + } + Topics.getMainPost = function (tid, uid, callback) { Topics.getMainPosts([tid], uid, function (err, mainPosts) { callback(err, Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null); @@ -317,7 +327,7 @@ var social = require('./social'); term: term, }, callback); } else { - callback(new Error('no-plugins-available'), []); + callback(new Error('[[error:no-plugins-available]]'), []); } }; }(exports)); diff --git a/src/topics/create.js b/src/topics/create.js index d0e2c37e7b..418098cbd9 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -330,7 +330,7 @@ module.exports = function (Topics) { function check(item, min, max, minError, maxError, callback) { // Trim and remove HTML (latter for composers that send in HTML, like redactor) if (typeof item === 'string') { - item = S(item.trim()).stripTags().s; + item = S(item).stripTags().s.trim(); } if (!item || item.length < parseInt(min, 10)) { diff --git a/src/topics/data.js b/src/topics/data.js index cf1df2787c..17e060f679 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -86,4 +86,8 @@ module.exports = function (Topics) { Topics.deleteTopicField = function (tid, field, callback) { db.deleteObjectField('topic:' + tid, field, callback); }; + + Topics.deleteTopicFields = function (tid, fields, callback) { + db.deleteObjectFields('topic:' + tid, fields, callback); + }; }; diff --git a/src/topics/delete.js b/src/topics/delete.js index ec9d7f3a42..3598248d26 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -18,7 +18,11 @@ module.exports = function (Topics) { async.parallel([ function (next) { - Topics.setTopicField(tid, 'deleted', 1, next); + Topics.setTopicFields(tid, { + deleted: 1, + deleterUid: uid, + deletedTimestamp: Date.now(), + }, next); }, function (next) { db.sortedSetsRemove(['topics:recent', 'topics:posts', 'topics:views'], tid, next); @@ -47,6 +51,9 @@ module.exports = function (Topics) { function (next) { Topics.setTopicField(tid, 'deleted', 0, next); }, + function (next) { + Topics.deleteTopicFields(tid, ['deleterUid', 'deletedTimestamp'], next); + }, function (next) { Topics.updateRecent(tid, topicData.lastposttime, next); }, diff --git a/src/topics/unread.js b/src/topics/unread.js index 3e8fb55da9..50c65e2511 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -204,7 +204,7 @@ module.exports = function (Topics) { Topics.markAsRead = function (tids, uid, callback) { callback = callback || function () {}; if (!Array.isArray(tids) || !tids.length) { - return callback(); + return setImmediate(callback, null, false); } tids = tids.filter(function (tid, index, array) { @@ -212,7 +212,7 @@ module.exports = function (Topics) { }); if (!tids.length) { - return callback(null, false); + return setImmediate(callback, null, false); } async.waterfall([ diff --git a/src/user.js b/src/user.js index 6cd1490520..d521b77f0d 100644 --- a/src/user.js +++ b/src/user.js @@ -6,388 +6,334 @@ var _ = require('underscore'); var groups = require('./groups'); var plugins = require('./plugins'); var db = require('./database'); -var topics = require('./topics'); var privileges = require('./privileges'); var meta = require('./meta'); -(function (User) { - User.email = require('./user/email'); - User.notifications = require('./user/notifications'); - User.reset = require('./user/reset'); - User.digest = require('./user/digest'); - - require('./user/data')(User); - require('./user/auth')(User); - require('./user/bans')(User); - require('./user/create')(User); - require('./user/posts')(User); - require('./user/topics')(User); - require('./user/categories')(User); - require('./user/follow')(User); - require('./user/profile')(User); - require('./user/admin')(User); - require('./user/delete')(User); - require('./user/settings')(User); - require('./user/search')(User); - require('./user/jobs')(User); - require('./user/picture')(User); - require('./user/approval')(User); - require('./user/invite')(User); - require('./user/password')(User); - require('./user/info')(User); - - User.updateLastOnlineTime = function (uid, callback) { - callback = callback || function () {}; - db.getObjectFields('user:' + uid, ['status', 'lastonline'], function (err, userData) { - var now = Date.now(); - if (err || userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) { - return callback(err); - } - User.setUserField(uid, 'lastonline', now, callback); - }); - }; - - User.updateOnlineUsers = function (uid, callback) { - callback = callback || function () {}; - +var User = module.exports; + +User.email = require('./user/email'); +User.notifications = require('./user/notifications'); +User.reset = require('./user/reset'); +User.digest = require('./user/digest'); + +require('./user/data')(User); +require('./user/auth')(User); +require('./user/bans')(User); +require('./user/create')(User); +require('./user/posts')(User); +require('./user/topics')(User); +require('./user/categories')(User); +require('./user/follow')(User); +require('./user/profile')(User); +require('./user/admin')(User); +require('./user/delete')(User); +require('./user/settings')(User); +require('./user/search')(User); +require('./user/jobs')(User); +require('./user/picture')(User); +require('./user/approval')(User); +require('./user/invite')(User); +require('./user/password')(User); +require('./user/info')(User); +require('./user/online')(User); + +User.getUidsFromSet = function (set, start, stop, callback) { + if (set === 'users:online') { + var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; var now = Date.now(); - async.waterfall([ - function (next) { - db.sortedSetScore('users:online', uid, next); - }, - function (userOnlineTime, next) { - if (now - parseInt(userOnlineTime, 10) < 300000) { - return callback(); - } - db.sortedSetAdd('users:online', now, uid, next); - }, - function (next) { - topics.pushUnreadCount(uid); - plugins.fireHook('action:user.online', { uid: uid, timestamp: now }); - next(); - }, - ], callback); - }; - - User.getUidsFromSet = function (set, start, stop, callback) { - if (set === 'users:online') { - var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; - var now = Date.now(); - db.getSortedSetRevRangeByScore(set, start, count, '+inf', now - 300000, callback); - } else { - db.getSortedSetRevRange(set, start, stop, callback); - } - }; - - User.getUsersFromSet = function (set, uid, start, stop, callback) { - async.waterfall([ - function (next) { - User.getUidsFromSet(set, start, stop, next); - }, - function (uids, next) { - User.getUsers(uids, uid, next); - }, - ], callback); - }; - - User.getUsersWithFields = function (uids, fields, uid, callback) { - async.waterfall([ - function (next) { - plugins.fireHook('filter:users.addFields', { fields: fields }, next); - }, - function (data, next) { - data.fields = data.fields.filter(function (field, index, array) { - return array.indexOf(field) === index; - }); - - async.parallel({ - userData: function (next) { - User.getUsersFields(uids, data.fields, next); - }, - isAdmin: function (next) { - User.isAdministrator(uids, next); - }, - }, next); - }, - function (results, next) { - results.userData.forEach(function (user, index) { - if (user) { - user.status = User.getStatus(user); - user.administrator = results.isAdmin[index]; - user.banned = parseInt(user.banned, 10) === 1; - user.banned_until = parseInt(user['banned:expire'], 10) || 0; - user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned'; - user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1; - } - }); - plugins.fireHook('filter:userlist.get', { users: results.userData, uid: uid }, next); - }, - function (data, next) { - next(null, data.users); - }, - ], callback); - }; - - User.getUsers = function (uids, uid, callback) { - var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'flags', - 'banned', 'banned:expire', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline']; - - User.getUsersWithFields(uids, fields, uid, callback); - }; - - User.getStatus = function (userData) { - var isOnline = (Date.now() - parseInt(userData.lastonline, 10)) < 300000; - return isOnline ? (userData.status || 'online') : 'offline'; - }; - - User.isOnline = function (uid, callback) { - if (Array.isArray(uid)) { - db.sortedSetScores('users:online', uid, function (err, lastonline) { - if (err) { - return callback(err); - } - var now = Date.now(); - var isOnline = uid.map(function (uid, index) { - return now - lastonline[index] < 300000; - }); - callback(null, isOnline); + db.getSortedSetRevRangeByScore(set, start, count, '+inf', now - 300000, callback); + } else { + db.getSortedSetRevRange(set, start, stop, callback); + } +}; + +User.getUsersFromSet = function (set, uid, start, stop, callback) { + async.waterfall([ + function (next) { + User.getUidsFromSet(set, start, stop, next); + }, + function (uids, next) { + User.getUsers(uids, uid, next); + }, + ], callback); +}; + +User.getUsersWithFields = function (uids, fields, uid, callback) { + async.waterfall([ + function (next) { + plugins.fireHook('filter:users.addFields', { fields: fields }, next); + }, + function (data, next) { + data.fields = data.fields.filter(function (field, index, array) { + return array.indexOf(field) === index; }); - } else { - db.sortedSetScore('users:online', uid, function (err, lastonline) { - if (err) { - return callback(err); + + async.parallel({ + userData: function (next) { + User.getUsersFields(uids, data.fields, next); + }, + isAdmin: function (next) { + User.isAdministrator(uids, next); + }, + }, next); + }, + function (results, next) { + results.userData.forEach(function (user, index) { + if (user) { + user.status = User.getStatus(user); + user.administrator = results.isAdmin[index]; + user.banned = parseInt(user.banned, 10) === 1; + user.banned_until = parseInt(user['banned:expire'], 10) || 0; + user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned'; + user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1; } - var isOnline = Date.now() - parseInt(lastonline, 10) < 300000; - callback(null, isOnline); }); - } - }; - - User.exists = function (uid, callback) { - db.isSortedSetMember('users:joindate', uid, callback); - }; - - User.existsBySlug = function (userslug, callback) { - User.getUidByUserslug(userslug, function (err, exists) { - callback(err, !!exists); - }); - }; - - User.getUidByUsername = function (username, callback) { - if (!username) { - return callback(null, 0); - } - db.sortedSetScore('username:uid', username, callback); - }; - - User.getUidsByUsernames = function (usernames, callback) { - db.sortedSetScores('username:uid', usernames, callback); - }; - - User.getUidByUserslug = function (userslug, callback) { - if (!userslug) { - return callback(null, 0); - } - db.sortedSetScore('userslug:uid', userslug, callback); - }; - - User.getUsernamesByUids = function (uids, callback) { - User.getUsersFields(uids, ['username'], function (err, users) { - if (err) { - return callback(err); - } - + plugins.fireHook('filter:userlist.get', { users: results.userData, uid: uid }, next); + }, + function (data, next) { + next(null, data.users); + }, + ], callback); +}; + +User.getUsers = function (uids, uid, callback) { + var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'flags', + 'banned', 'banned:expire', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline']; + + User.getUsersWithFields(uids, fields, uid, callback); +}; + +User.getStatus = function (userData) { + var isOnline = (Date.now() - parseInt(userData.lastonline, 10)) < 300000; + return isOnline ? (userData.status || 'online') : 'offline'; +}; + +User.exists = function (uid, callback) { + db.isSortedSetMember('users:joindate', uid, callback); +}; + +User.existsBySlug = function (userslug, callback) { + User.getUidByUserslug(userslug, function (err, exists) { + callback(err, !!exists); + }); +}; + +User.getUidByUsername = function (username, callback) { + if (!username) { + return callback(null, 0); + } + db.sortedSetScore('username:uid', username, callback); +}; + +User.getUidsByUsernames = function (usernames, callback) { + db.sortedSetScores('username:uid', usernames, callback); +}; + +User.getUidByUserslug = function (userslug, callback) { + if (!userslug) { + return callback(null, 0); + } + db.sortedSetScore('userslug:uid', userslug, callback); +}; + +User.getUsernamesByUids = function (uids, callback) { + async.waterfall([ + function (next) { + User.getUsersFields(uids, ['username'], next); + }, + function (users, next) { users = users.map(function (user) { return user.username; }); - callback(null, users); - }); - }; - - User.getUsernameByUserslug = function (slug, callback) { - async.waterfall([ - function (next) { - User.getUidByUserslug(slug, next); - }, - function (uid, next) { - User.getUserField(uid, 'username', next); - }, - ], callback); - }; - - User.getUidByEmail = function (email, callback) { - db.sortedSetScore('email:uid', email.toLowerCase(), callback); - }; - - User.getUidsByEmails = function (emails, callback) { - emails = emails.map(function (email) { - return email && email.toLowerCase(); - }); - db.sortedSetScores('email:uid', emails, callback); - }; - - User.getUsernameByEmail = function (email, callback) { - db.sortedSetScore('email:uid', email.toLowerCase(), function (err, uid) { - if (err) { - return callback(err); - } - User.getUserField(uid, 'username', callback); - }); - }; - - User.isModerator = function (uid, cid, callback) { - privileges.users.isModerator(uid, cid, callback); - }; - - User.isModeratorOfAnyCategory = function (uid, callback) { - User.getModeratedCids(uid, function (err, cids) { - callback(err, Array.isArray(cids) ? !!cids.length : false); - }); - }; - - User.isAdministrator = function (uid, callback) { - privileges.users.isAdministrator(uid, callback); - }; - - User.isGlobalModerator = function (uid, callback) { - privileges.users.isGlobalModerator(uid, callback); - }; - - User.isPrivileged = function (uid, callback) { - async.parallel([ - async.apply(User.isAdministrator, uid), - async.apply(User.isGlobalModerator, uid), - async.apply(User.isModeratorOfAnyCategory, uid), - ], function (err, results) { - callback(err, results ? results.some(Boolean) : false); - }); - }; - - User.isAdminOrGlobalMod = function (uid, callback) { - async.parallel({ - isAdmin: async.apply(User.isAdministrator, uid), - isGlobalMod: async.apply(User.isGlobalModerator, uid), - }, function (err, results) { - callback(err, results ? (results.isAdmin || results.isGlobalMod) : false); - }); - }; - - User.isAdminOrSelf = function (callerUid, uid, callback) { - if (parseInt(callerUid, 10) === parseInt(uid, 10)) { - return callback(); - } - User.isAdministrator(callerUid, function (err, isAdmin) { - if (err || !isAdmin) { - return callback(err || new Error('[[error:no-privileges]]')); - } - callback(); - }); - }; - - User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) { - if (parseInt(callerUid, 10) === parseInt(uid, 10)) { - return callback(); - } - User.isAdminOrGlobalMod(callerUid, function (err, isAdminOrGlobalMod) { - if (err || !isAdminOrGlobalMod) { - return callback(err || new Error('[[error:no-privileges]]')); - } - callback(); - }); - }; - - User.getAdminsandGlobalMods = function (callback) { - async.parallel({ - admins: async.apply(groups.getMembers, 'administrators', 0, -1), - mods: async.apply(groups.getMembers, 'Global Moderators', 0, -1), - }, function (err, results) { - if (err) { - return callback(err); + next(null, users); + }, + ], callback); +}; + +User.getUsernameByUserslug = function (slug, callback) { + async.waterfall([ + function (next) { + User.getUidByUserslug(slug, next); + }, + function (uid, next) { + User.getUserField(uid, 'username', next); + }, + ], callback); +}; + +User.getUidByEmail = function (email, callback) { + db.sortedSetScore('email:uid', email.toLowerCase(), callback); +}; + +User.getUidsByEmails = function (emails, callback) { + emails = emails.map(function (email) { + return email && email.toLowerCase(); + }); + db.sortedSetScores('email:uid', emails, callback); +}; + +User.getUsernameByEmail = function (email, callback) { + async.waterfall([ + function (next) { + db.sortedSetScore('email:uid', email.toLowerCase(), next); + }, + function (uid, next) { + User.getUserField(uid, 'username', next); + }, + ], callback); +}; + +User.isModerator = function (uid, cid, callback) { + privileges.users.isModerator(uid, cid, callback); +}; + +User.isModeratorOfAnyCategory = function (uid, callback) { + User.getModeratedCids(uid, function (err, cids) { + callback(err, Array.isArray(cids) ? !!cids.length : false); + }); +}; + +User.isAdministrator = function (uid, callback) { + privileges.users.isAdministrator(uid, callback); +}; + +User.isGlobalModerator = function (uid, callback) { + privileges.users.isGlobalModerator(uid, callback); +}; + +User.isPrivileged = function (uid, callback) { + async.parallel([ + async.apply(User.isAdministrator, uid), + async.apply(User.isGlobalModerator, uid), + async.apply(User.isModeratorOfAnyCategory, uid), + ], function (err, results) { + callback(err, results ? results.some(Boolean) : false); + }); +}; + +User.isAdminOrGlobalMod = function (uid, callback) { + async.parallel({ + isAdmin: async.apply(User.isAdministrator, uid), + isGlobalMod: async.apply(User.isGlobalModerator, uid), + }, function (err, results) { + callback(err, results ? (results.isAdmin || results.isGlobalMod) : false); + }); +}; + +User.isAdminOrSelf = function (callerUid, uid, callback) { + isSelfOrMethod(callerUid, uid, User.isAdministrator, callback); +}; + +User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) { + isSelfOrMethod(callerUid, uid, User.isAdminOrGlobalMod, callback); +}; + +function isSelfOrMethod(callerUid, uid, method, callback) { + if (parseInt(callerUid, 10) === parseInt(uid, 10)) { + return callback(); + } + async.waterfall([ + function (next) { + method(callerUid, next); + }, + function (isPass, next) { + if (!isPass) { + return next(new Error('[[error:no-privileges]]')); } - var uids = results.admins.concat(results.mods).filter(function (uid, index, array) { - return uid && array.indexOf(uid) === index; + next(); + }, + ], callback); +} + +User.getAdminsandGlobalMods = function (callback) { + async.waterfall([ + function (next) { + async.parallel([ + async.apply(groups.getMembers, 'administrators', 0, -1), + async.apply(groups.getMembers, 'Global Moderators', 0, -1), + ], next); + }, + function (results, next) { + User.getUsersData(_.union(results), next); + }, + ], callback); +}; + +User.getAdminsandGlobalModsandModerators = function (callback) { + async.waterfall([ + function (next) { + async.parallel([ + async.apply(groups.getMembers, 'administrators', 0, -1), + async.apply(groups.getMembers, 'Global Moderators', 0, -1), + async.apply(User.getModeratorUids), + ], next); + }, + function (results, next) { + User.getUsersData(_.union.apply(_, results), next); + }, + ], callback); +}; + +User.getModeratorUids = function (callback) { + async.waterfall([ + async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), + function (cids, next) { + var groupNames = cids.map(function (cid) { + return 'cid:' + cid + ':privileges:mods'; }); - User.getUsersData(uids, callback); - }); - }; - - User.getAdminsandGlobalModsandModerators = function (callback) { - async.parallel([ - async.apply(groups.getMembers, 'administrators', 0, -1), - async.apply(groups.getMembers, 'Global Moderators', 0, -1), - async.apply(User.getModeratorUids), - ], function (err, results) { - if (err) { - return callback(err); - } - User.getUsersData(_.union.apply(_, results), callback); - }); - }; + groups.getMembersOfGroups(groupNames, next); + }, + function (memberSets, next) { + next(null, _.union.apply(_, memberSets)); + }, + ], callback); +}; + +User.getModeratedCids = function (uid, callback) { + var cids; + async.waterfall([ + function (next) { + db.getSortedSetRange('categories:cid', 0, -1, next); + }, + function (_cids, next) { + cids = _cids; + User.isModerator(uid, cids, next); + }, + function (isMods, next) { + cids = cids.filter(function (cid, index) { + return cid && isMods[index]; + }); + next(null, cids); + }, + ], callback); +}; + +User.addInterstitials = function (callback) { + plugins.registerHook('core', { + hook: 'filter:register.interstitial', + method: function (data, callback) { + if (meta.config.termsOfUse && !data.userData.acceptTos) { + data.interstitials.push({ + template: 'partials/acceptTos', + data: { + termsOfUse: meta.config.termsOfUse, + }, + callback: function (userData, formData, next) { + if (formData['agree-terms'] === 'on') { + userData.acceptTos = true; + } - User.getModeratorUids = function (callback) { - async.waterfall([ - async.apply(db.getSortedSetRange, 'categories:cid', 0, -1), - function (cids, next) { - var groupNames = cids.map(function (cid) { - return 'cid:' + cid + ':privileges:mods'; + next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]')); + }, }); + } - groups.getMembersOfGroups(groupNames, function (err, memberSets) { - if (err) { - return next(err); - } - - next(null, _.union.apply(_, memberSets)); - }); - }, - ], callback); - }; - - User.getModeratedCids = function (uid, callback) { - var cids; - async.waterfall([ - function (next) { - db.getSortedSetRange('categories:cid', 0, -1, next); - }, - function (_cids, next) { - cids = _cids; - User.isModerator(uid, cids, next); - }, - function (isMods, next) { - cids = cids.filter(function (cid, index) { - return cid && isMods[index]; - }); - next(null, cids); - }, - ], callback); - }; - - User.addInterstitials = function (callback) { - plugins.registerHook('core', { - hook: 'filter:register.interstitial', - method: function (data, callback) { - if (meta.config.termsOfUse && !data.userData.acceptTos) { - data.interstitials.push({ - template: 'partials/acceptTos', - data: { - termsOfUse: meta.config.termsOfUse, - }, - callback: function (userData, formData, next) { - if (formData['agree-terms'] === 'on') { - userData.acceptTos = true; - } - - next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]')); - }, - }); - } + callback(null, data); + }, + }); - callback(null, data); - }, - }); + callback(); +}; - callback(); - }; -}(exports)); diff --git a/src/user/admin.js b/src/user/admin.js index 28668cff80..d463523f89 100644 --- a/src/user/admin.js +++ b/src/user/admin.js @@ -16,13 +16,7 @@ module.exports = function (User) { }; User.getIPs = function (uid, stop, callback) { - db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, function (err, ips) { - if (err) { - return callback(err); - } - - callback(null, ips); - }); + db.getSortedSetRevRange('uid:' + uid + ':ip', 0, stop, callback); }; User.getUsersCSV = function (callback) { diff --git a/src/user/data.js b/src/user/data.js index 2dca1b4143..3377d5370e 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -1,5 +1,6 @@ 'use strict'; +var async = require('async'); var validator = require('validator'); var nconf = require('nconf'); var winston = require('winston'); @@ -63,17 +64,24 @@ module.exports = function (User) { addField('lastonline'); } - db.getObjectsFields(keys, fields, function (err, users) { - if (err) { - return callback(err); - } - - users = uids.map(function (uid) { - return users[ref[uid]]; - }); - - modifyUserData(users, fieldsToRemove, callback); - }); + async.waterfall([ + function (next) { + db.getObjectsFields(keys, fields, function (err, users) { + if (err) { + return callback(err); + } + + users = uids.map(function (uid) { + return users[ref[uid]]; + }); + + next(null, users); + }); + }, + function (users, next) { + modifyUserData(users, fieldsToRemove, next); + }, + ], callback); }; User.getMultipleUserFields = function (uids, fields, callback) { @@ -105,17 +113,24 @@ module.exports = function (User) { return 'user:' + uid; }); - db.getObjects(keys, function (err, users) { - if (err) { - return callback(err); - } - - users = uids.map(function (uid) { - return users[ref[uid]]; - }); - - modifyUserData(users, [], callback); - }); + async.waterfall([ + function (next) { + db.getObjects(keys, function (err, users) { + if (err) { + return callback(err); + } + + users = uids.map(function (uid) { + return users[ref[uid]]; + }); + + next(null, users); + }); + }, + function (users, next) { + modifyUserData(users, [], next); + }, + ], callback); }; function modifyUserData(users, fieldsToRemove, callback) { @@ -178,51 +193,53 @@ module.exports = function (User) { User.setUserField = function (uid, field, value, callback) { callback = callback || function () {}; - db.setObjectField('user:' + uid, field, value, function (err) { - if (err) { - return callback(err); - } - plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'set' }); - callback(); - }); + async.waterfall([ + function (next) { + db.setObjectField('user:' + uid, field, value, next); + }, + function (next) { + plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'set' }); + next(); + }, + ], callback); }; User.setUserFields = function (uid, data, callback) { callback = callback || function () {}; - db.setObject('user:' + uid, data, function (err) { - if (err) { - return callback(err); - } - for (var field in data) { - if (data.hasOwnProperty(field)) { - plugins.fireHook('action:user.set', { uid: uid, field: field, value: data[field], type: 'set' }); + async.waterfall([ + function (next) { + db.setObject('user:' + uid, data, next); + }, + function (next) { + for (var field in data) { + if (data.hasOwnProperty(field)) { + plugins.fireHook('action:user.set', { uid: uid, field: field, value: data[field], type: 'set' }); + } } - } - callback(); - }); + next(); + }, + ], callback); }; User.incrementUserFieldBy = function (uid, field, value, callback) { - callback = callback || function () {}; - db.incrObjectFieldBy('user:' + uid, field, value, function (err, value) { - if (err) { - return callback(err); - } - plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'increment' }); - - callback(null, value); - }); + incrDecrUserFieldBy(uid, field, value, 'increment', callback); }; User.decrementUserFieldBy = function (uid, field, value, callback) { - callback = callback || function () {}; - db.incrObjectFieldBy('user:' + uid, field, -value, function (err, value) { - if (err) { - return callback(err); - } - plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'decrement' }); - - callback(null, value); - }); + incrDecrUserFieldBy(uid, field, -value, 'decrement', callback); }; + + function incrDecrUserFieldBy(uid, field, value, type, callback) { + callback = callback || function () {}; + async.waterfall([ + function (next) { + db.incrObjectFieldBy('user:' + uid, field, value, next); + }, + function (value, next) { + plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: type }); + + next(null, value); + }, + ], callback); + } }; diff --git a/src/user/online.js b/src/user/online.js new file mode 100644 index 0000000000..6cb19cd22f --- /dev/null +++ b/src/user/online.js @@ -0,0 +1,70 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var topics = require('../topics'); +var plugins = require('../plugins'); + +module.exports = function (User) { + User.updateLastOnlineTime = function (uid, callback) { + callback = callback || function () {}; + db.getObjectFields('user:' + uid, ['status', 'lastonline'], function (err, userData) { + var now = Date.now(); + if (err || userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) { + return callback(err); + } + User.setUserField(uid, 'lastonline', now, callback); + }); + }; + + User.updateOnlineUsers = function (uid, callback) { + callback = callback || function () {}; + + var now = Date.now(); + async.waterfall([ + function (next) { + db.sortedSetScore('users:online', uid, next); + }, + function (userOnlineTime, next) { + if (now - parseInt(userOnlineTime, 10) < 300000) { + return callback(); + } + db.sortedSetAdd('users:online', now, uid, next); + }, + function (next) { + topics.pushUnreadCount(uid); + plugins.fireHook('action:user.online', { uid: uid, timestamp: now }); + next(); + }, + ], callback); + }; + + User.isOnline = function (uid, callback) { + var now = Date.now(); + async.waterfall([ + function (next) { + if (Array.isArray(uid)) { + db.sortedSetScores('users:online', uid, next); + } else { + db.sortedSetScore('users:online', uid, next); + } + }, + function (lastonline, next) { + function checkOnline(lastonline) { + return now - lastonline < 300000; + } + + var isOnline; + if (Array.isArray(uid)) { + isOnline = uid.map(function (uid, index) { + return checkOnline(lastonline[index]); + }); + } else { + isOnline = checkOnline(lastonline); + } + next(null, isOnline); + }, + ], callback); + }; +}; diff --git a/src/user/picture.js b/src/user/picture.js index be489364b9..13288774b2 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -50,12 +50,7 @@ module.exports = function (User) { }, next); }, function (image, next) { - User.setUserFields(uid, { - uploadedpicture: image.url, - picture: image.url, - }, function (err) { - next(err, image); - }); + next(null, image); }, ], callback); }; diff --git a/src/user/profile.js b/src/user/profile.js index 84c142f35b..3071977949 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -18,6 +18,14 @@ module.exports = function (User) { var updateUid = data.uid; var oldData; + if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) { + return callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]')); + } + + if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) { + return callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]')); + } + async.waterfall([ function (next) { plugins.fireHook('filter:user.updateProfile', { uid: uid, data: data, fields: fields }, next); @@ -27,8 +35,6 @@ module.exports = function (User) { data = data.data; async.series([ - async.apply(isAboutMeValid, data), - async.apply(isSignatureValid, data), async.apply(isEmailAvailable, data, updateUid), async.apply(isUsernameAvailable, data, updateUid), async.apply(isGroupTitleValid, data), @@ -68,22 +74,6 @@ module.exports = function (User) { ], callback); }; - function isAboutMeValid(data, callback) { - if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) { - callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]')); - } else { - callback(); - } - } - - function isSignatureValid(data, callback) { - if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) { - callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]')); - } else { - callback(); - } - } - function isEmailAvailable(data, uid, callback) { if (!data.email) { return callback(); diff --git a/src/user/settings.js b/src/user/settings.js index 870dd2e59c..bef20e0087 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -74,7 +74,7 @@ module.exports = function (User) { settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1; settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; settings.delayImageLoading = parseInt(getSetting(settings, 'delayImageLoading', 1), 10) === 1; - settings.bootswatchSkin = settings.bootswatchSkin || 'default'; + settings.bootswatchSkin = settings.bootswatchSkin || meta.config.bootswatchSkin || 'default'; settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; callback(null, settings); diff --git a/src/views/admin/partials/installed_plugin_item.tpl b/src/views/admin/partials/installed_plugin_item.tpl index b76ae9f964..2e95606ef8 100644 --- a/src/views/admin/partials/installed_plugin_item.tpl +++ b/src/views/admin/partials/installed_plugin_item.tpl @@ -36,13 +36,7 @@
  • - -<<<<<<< HEAD - - -======= ->>>>>>> `admin/extend` translations

    {installed.id}

    diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl index 68705951da..2d9a198ec8 100644 --- a/src/views/admin/settings/general.tpl +++ b/src/views/admin/settings/general.tpl @@ -31,8 +31,8 @@
    - -
    +
    +
    @@ -140,6 +140,11 @@ [[admin/settings/general:outgoing-links.warning-page]] + +
    +
    + +
    diff --git a/src/views/admin/settings/group.tpl b/src/views/admin/settings/group.tpl index 1c0b660361..fd696cb5ad 100644 --- a/src/views/admin/settings/group.tpl +++ b/src/views/admin/settings/group.tpl @@ -43,7 +43,7 @@

    [[admin/settings/group:default-cover-help]]

    -
    +
    diff --git a/src/views/admin/settings/uploads.tpl b/src/views/admin/settings/uploads.tpl index 6c9b59db80..feff2dbff3 100644 --- a/src/views/admin/settings/uploads.tpl +++ b/src/views/admin/settings/uploads.tpl @@ -50,7 +50,7 @@
    - +

    [[admin/settings/uploads:allowed-file-extensions-help]]

    @@ -131,7 +131,7 @@

    [[admin/settings/uploads:default-covers-help]]

    - +
    diff --git a/src/webserver.js b/src/webserver.js index 4ce61b1c2e..7d3c6aa9a2 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -61,21 +61,20 @@ module.exports.listen = function (callback) { logger.init(app); - initializeNodeBB(function (err) { - if (err) { - return callback(err); - } - - winston.info('NodeBB Ready'); + async.waterfall([ + initializeNodeBB, + function (next) { + winston.info('NodeBB Ready'); - require('./socket.io').server.emit('event:nodebb.ready', { - 'cache-buster': meta.config['cache-buster'], - }); + require('./socket.io').server.emit('event:nodebb.ready', { + 'cache-buster': meta.config['cache-buster'], + }); - plugins.fireHook('action:nodebb.ready'); + plugins.fireHook('action:nodebb.ready'); - listen(callback); - }); + listen(next); + }, + ], callback); }; function initializeNodeBB(callback) { @@ -107,7 +106,9 @@ function initializeNodeBB(callback) { meta.blacklist.load, ], next); }, - ], callback); + ], function (err) { + callback(err); + }); } function setupExpressApp(app) { diff --git a/test/categories.js b/test/categories.js index bdd91602de..8ef059d8cb 100644 --- a/test/categories.js +++ b/test/categories.js @@ -390,6 +390,7 @@ describe('Categories', function () { it('should get all categories', function (done) { socketCategories.getAll({ uid: adminUid }, {}, function (err, data) { assert.ifError(err); + assert(data); done(); }); }); @@ -615,6 +616,72 @@ describe('Categories', function () { }); + describe('privileges', function () { + var privileges = require('../src/privileges'); + + it('should return empty array if uids is empty array', function (done) { + privileges.categories.filterUids('find', categoryObj.cid, [], function (err, uids) { + assert.ifError(err); + assert.equal(uids.length, 0); + done(); + }); + }); + + it('should filter uids by privilege', function (done) { + privileges.categories.filterUids('find', categoryObj.cid, [1, 2, 3, 4], function (err, uids) { + assert.ifError(err); + assert.deepEqual(uids, [1, 2]); + done(); + }); + }); + + it('should load user privileges', function (done) { + privileges.categories.userPrivileges(categoryObj.cid, 1, function (err, data) { + assert.ifError(err); + assert.deepEqual(data, { + find: false, + mods: false, + 'posts:delete': false, + read: false, + 'topics:reply': false, + 'topics:read': false, + 'topics:create': false, + 'topics:delete': false, + 'posts:edit': false, + }); + + done(); + }); + }); + + it('should load group privileges', function (done) { + privileges.categories.groupPrivileges(categoryObj.cid, 'registered-users', function (err, data) { + assert.ifError(err); + assert.deepEqual(data, { + 'groups:find': true, + 'groups:posts:edit': true, + 'groups:topics:delete': false, + 'groups:topics:create': true, + 'groups:topics:reply': true, + 'groups:posts:delete': true, + 'groups:read': true, + 'groups:topics:read': true, + }); + + done(); + }); + }); + + it('should return false if cid is falsy', function (done) { + privileges.categories.isUserAllowedTo('find', null, adminUid, function (err, isAllowed) { + assert.ifError(err); + assert.equal(isAllowed, false); + done(); + }); + }); + }); + + after(function (done) { db.emptydb(done); }); diff --git a/test/controllers-admin.js b/test/controllers-admin.js index ad59b4705d..5a17ca96a9 100644 --- a/test/controllers-admin.js +++ b/test/controllers-admin.js @@ -21,6 +21,7 @@ describe('Admin Controllers', function () { var jar; before(function (done) { + groups.resetCache(); async.series({ category: function (next) { categories.create({ @@ -43,9 +44,10 @@ describe('Admin Controllers', function () { cid = results.category.cid; topics.post({ uid: adminUid, title: 'test topic title', content: 'test topic content', cid: results.category.cid }, function (err, result) { + assert.ifError(err); tid = result.topicData.tid; pid = result.postData.pid; - done(err); + done(); }); }); }); diff --git a/test/controllers.js b/test/controllers.js index d79235d16f..e86ae0c00e 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -27,7 +27,7 @@ describe('Controllers', function () { }, next); }, user: function (next) { - user.create({ username: 'foo', password: 'barbar' }, next); + user.create({ username: 'foo', password: 'barbar', email: 'foo@test.com' }, next); }, navigation: function (next) { var navigation = require('../src/navigation/admin'); @@ -498,11 +498,23 @@ describe('Controllers', function () { hidden: 0, }, function (err) { assert.ifError(err); - request(nconf.get('url') + '/groups/group-details', function (err, res, body) { + groups.join('group-details', fooUid, function (err) { assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); + topics.post({ + uid: fooUid, + title: 'topic title', + content: 'test topic content', + cid: cid, + }, function (err) { + assert.ifError(err); + request(nconf.get('url') + '/api/groups/group-details', { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + assert.equal(body.posts[0].content, 'test topic content'); + done(); + }); + }); }); }); }); @@ -532,6 +544,15 @@ describe('Controllers', function () { }); }); + it('should get recent posts', function (done) { + request(nconf.get('url') + '/api/recent/posts/month', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + it('should get post data', function (done) { request(nconf.get('url') + '/api/post/pid/' + pid, function (err, res, body) { assert.ifError(err); @@ -890,6 +911,42 @@ describe('Controllers', function () { }, ], done); }); + + it('should 404 if user does not exist', function (done) { + request(nconf.get('url') + '/api/user/email/doesnotexist', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + assert(body); + done(); + }); + }); + + it('should load user by uid', function (done) { + request(nconf.get('url') + '/api/user/uid/' + fooUid, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should load user by username', function (done) { + request(nconf.get('url') + '/api/user/username/foo', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should load user by email', function (done) { + request(nconf.get('url') + '/api/user/email/foo@test.com', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); }); describe('account follow page', function () { @@ -943,7 +1000,7 @@ describe('Controllers', function () { describe('post redirect', function () { it('should 404 for invalid pid', function (done) { - request(nconf.get('url') + '/post/fail', function (err, res) { + request(nconf.get('url') + '/api/post/fail', function (err, res) { assert.ifError(err); assert.equal(res.statusCode, 404); done(); diff --git a/test/groups.js b/test/groups.js index 4d587b3aad..6b52787efa 100644 --- a/test/groups.js +++ b/test/groups.js @@ -315,6 +315,15 @@ describe('Groups', function () { }); }); }); + + it('should fail if system groups is being renamed', function (done) { + Groups.update('administrators', { + name: 'administrators_fail', + }, function (err) { + assert.equal(err.message, '[[error:not-allowed-to-rename-system-group]]'); + done(); + }); + }); }); describe('.destroy()', function () { diff --git a/test/posts.js b/test/posts.js index 0ea8074d28..8508ab70f4 100644 --- a/test/posts.js +++ b/test/posts.js @@ -11,6 +11,7 @@ var categories = require('../src/categories'); var privileges = require('../src/privileges'); var user = require('../src/user'); var groups = require('../src/groups'); +var socketPosts = require('../src/socket.io/posts'); describe('Post\'s', function () { var voterUid; @@ -66,7 +67,6 @@ describe('Post\'s', function () { }); describe('voting', function () { - var socketPosts = require('../src/socket.io/posts'); it('should upvote a post', function (done) { socketPosts.upvote({ uid: voterUid }, { pid: postData.pid, room_id: 'topic_1' }, function (err, result) { assert.ifError(err); @@ -138,7 +138,7 @@ describe('Post\'s', function () { describe('bookmarking', function () { it('should bookmark a post', function (done) { - posts.bookmark(postData.pid, voterUid, function (err, data) { + socketPosts.bookmark({ uid: voterUid }, { pid: postData.pid, room_id: 'topic_' + postData.tid }, function (err, data) { assert.ifError(err); assert.equal(data.isBookmarked, true); posts.hasBookmarked(postData.pid, voterUid, function (err, hasBookmarked) { @@ -150,7 +150,7 @@ describe('Post\'s', function () { }); it('should unbookmark a post', function (done) { - posts.unbookmark(postData.pid, voterUid, function (err, data) { + socketPosts.unbookmark({ uid: voterUid }, { pid: postData.pid, room_id: 'topic_' + postData.tid }, function (err, data) { assert.ifError(err); assert.equal(data.isBookmarked, false); posts.hasBookmarked([postData.pid], voterUid, function (err, hasBookmarked) { @@ -163,8 +163,6 @@ describe('Post\'s', function () { }); describe('post tools', function () { - var socketPosts = require('../src/socket.io/posts'); - it('should error if data is invalid', function (done) { socketPosts.loadPostTools({ uid: globalModUid }, null, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); @@ -209,7 +207,6 @@ describe('Post\'s', function () { var mainPid; var replyPid; - var socketPosts = require('../src/socket.io/posts'); before(function (done) { createTopicWithReply(function (topicPostData, replyData) { tid = topicPostData.topicData.tid; @@ -299,7 +296,6 @@ describe('Post\'s', function () { var pid; var replyPid; var tid; - var socketPosts = require('../src/socket.io/posts'); var meta = require('../src/meta'); before(function (done) { topics.post({ @@ -430,7 +426,6 @@ describe('Post\'s', function () { var replyPid; var tid; var moveTid; - var socketPosts = require('../src/socket.io/posts'); before(function (done) { async.waterfall([ @@ -539,6 +534,50 @@ describe('Post\'s', function () { }); }); + describe('parse', function () { + it('should store post content in cache', function (done) { + var oldValue = global.env; + global.env = 'production'; + var postData = { + pid: 9999, + content: 'some post content', + }; + posts.parsePost(postData, function (err) { + assert.ifError(err); + posts.parsePost(postData, function (err) { + assert.ifError(err); + global.env = oldValue; + done(); + }); + }); + }); + + it('should parse signature and remove links and images', function (done) { + var meta = require('../src/meta'); + meta.config['signatures:disableLinks'] = 1; + meta.config['signatures:disableImages'] = 1; + var userData = { + signature: '
    test derp', + }; + + posts.parseSignature(userData, 1, function (err, data) { + assert.ifError(err); + assert.equal(data.userData.signature, 'test derp'); + meta.config['signatures:disableLinks'] = 0; + meta.config['signatures:disableImages'] = 0; + done(); + }); + }); + + it('should turn relative links in post body to absolute urls', function (done) { + var nconf = require('nconf'); + var content = 'test youtube'; + var parsedContent = posts.relativeToAbsolute(content); + assert.equal(parsedContent, 'test youtube'); + done(); + }); + }); + describe('socket methods', function () { var pid; before(function (done) { @@ -554,7 +593,6 @@ describe('Post\'s', function () { }); }); - var socketPosts = require('../src/socket.io/posts'); it('should error with invalid data', function (done) { socketPosts.reply({ uid: 0 }, null, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); @@ -606,7 +644,7 @@ describe('Post\'s', function () { }); it('shold error with invalid data', function (done) { - socketPosts.loadMoreBookmarks({ uid: voterUid }, { uid: voterUid, after: null }, function (err, postData) { + socketPosts.loadMoreBookmarks({ uid: voterUid }, { uid: voterUid, after: null }, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); done(); }); diff --git a/test/socket.io.js b/test/socket.io.js index c534d0969c..fc24d24e30 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -419,11 +419,16 @@ describe('socket.io', function () { }); it('should set theme to bootswatch', function (done) { - socketAdmin.themes.set({ uid: adminUid }, { type: 'bootswatch', src: 'darkly' }, function (err) { + socketAdmin.themes.set({ uid: adminUid }, { + type: 'bootswatch', + src: '//maxcdn.bootstrapcdn.com/bootswatch/latest/darkly/bootstrap.min.css', + id: 'darkly', + }, function (err) { assert.ifError(err); - meta.configs.get('theme:src', function (err, id) { + meta.configs.getFields(['theme:src', 'bootswatchSkin'], function (err, fields) { assert.ifError(err); - assert.equal(id, 'darkly'); + assert.equal(fields['theme:src'], '//maxcdn.bootstrapcdn.com/bootswatch/latest/darkly/bootstrap.min.css'); + assert.equal(fields.bootswatchSkin, 'darkly'); done(); }); }); diff --git a/test/topics.js b/test/topics.js index 3c6e7efe71..6782f1041d 100644 --- a/test/topics.js +++ b/test/topics.js @@ -12,6 +12,7 @@ var User = require('../src/user'); var groups = require('../src/groups'); var helpers = require('./helpers'); var socketPosts = require('../src/socket.io/posts'); +var socketTopics = require('../src/socket.io/topics'); describe('Topic\'s', function () { var topic; @@ -49,11 +50,34 @@ describe('Topic\'s', function () { }); describe('.post', function () { + it('should fail to create topic with invalid data', function (done) { + socketTopics.post({ uid: 0 }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + it('should create a new topic with proper parameters', function (done) { topics.post({ uid: topic.userId, title: topic.title, content: topic.content, cid: topic.categoryId }, function (err, result) { - assert.equal(err, null, 'was created with error'); - assert.ok(result); + assert.ifError(err); + assert(result); + topic.tid = result.topicData.tid; + done(); + }); + }); + it('should get post count', function (done) { + socketTopics.postcount({ uid: adminUid }, topic.tid, function (err, count) { + assert.ifError(err); + assert.equal(count, 1); + done(); + }); + }); + + it('should load topic', function (done) { + socketTopics.getTopic({ uid: adminUid }, topic.tid, function (err, data) { + assert.ifError(err); + assert.equal(data.tid, topic.tid); done(); }); }); @@ -246,7 +270,7 @@ describe('Topic\'s', function () { var newTopic; var followerUid; var moveCid; - var socketTopics = require('../src/socket.io/topics'); + before(function (done) { async.waterfall([ function (next) { @@ -589,8 +613,7 @@ describe('Topic\'s', function () { assert.ok(result); replies.push(result); next(); - } - ); + }); } before(function (done) { @@ -619,25 +642,45 @@ describe('Topic\'s', function () { function (next) { postReply(next); }, function (next) { topicPids = replies.map(function (reply) { return reply.pid; }); - topics.setUserBookmark(newTopic.tid, topic.userId, originalBookmark, next); + socketTopics.bookmark({ uid: topic.userId }, { tid: newTopic.tid, index: originalBookmark }, next); }], done); }); + it('should fail with invalid data', function (done) { + socketTopics.bookmark({ uid: topic.userId }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + it('should have 12 replies', function (done) { assert.equal(12, replies.length); done(); }); + it('should fail with invalid data', function (done) { + socketTopics.createTopicFromPosts({ uid: 0 }, null, function (err) { + assert.equal(err.message, '[[error:not-logged-in]]'); + done(); + }); + }); + + it('should fail with invalid data', function (done) { + socketTopics.createTopicFromPosts({ uid: 1 }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + it('should not update the user\'s bookmark', function (done) { async.waterfall([ function (next) { - topics.createTopicFromPosts( - topic.userId, - 'Fork test, no bookmark update', - topicPids.slice(-2), - newTopic.tid, - next); + socketTopics.createTopicFromPosts({ uid: topic.userId }, { + title: 'Fork test, no bookmark update', + pids: topicPids.slice(-2), + fromTid: newTopic.tid, + }, next); }, function (forkedTopicData, next) { topics.getUserBookmark(newTopic.tid, topic.userId, next); @@ -859,7 +902,7 @@ describe('Topic\'s', function () { }); it('should infinite load topic posts', function (done) { - socketTopics.loadMore({ uid: adminUid }, { tid: tid, after: 0 }, function (err, data) { + socketTopics.loadMore({ uid: adminUid }, { tid: tid, after: 0, count: 10 }, function (err, data) { assert.ifError(err); assert(data.mainPost); assert(data.posts); @@ -878,7 +921,7 @@ describe('Topic\'s', function () { it('should load more unread topics', function (done) { socketTopics.markUnread({ uid: adminUid }, tid, function (err) { assert.ifError(err); - socketTopics.loadMoreUnreadTopics({ uid: adminUid }, { cid: topic.categoryId, after: 0 }, function (err, data) { + socketTopics.loadMoreUnreadTopics({ uid: adminUid }, { cid: topic.categoryId, after: 0, count: 10 }, function (err, data) { assert.ifError(err); assert(data); assert(Array.isArray(data.topics)); @@ -896,7 +939,7 @@ describe('Topic\'s', function () { it('should load more recent topics', function (done) { - socketTopics.loadMoreRecentTopics({ uid: adminUid }, { cid: topic.categoryId, after: 0 }, function (err, data) { + socketTopics.loadMoreRecentTopics({ uid: adminUid }, { cid: topic.categoryId, after: 0, count: 10 }, function (err, data) { assert.ifError(err); assert(data); assert(Array.isArray(data.topics)); @@ -912,7 +955,7 @@ describe('Topic\'s', function () { }); it('should load more from custom set', function (done) { - socketTopics.loadMoreFromSet({ uid: adminUid }, { set: 'uid:' + adminUid + ':topics', after: 0 }, function (err, data) { + socketTopics.loadMoreFromSet({ uid: adminUid }, { set: 'uid:' + adminUid + ':topics', after: 0, count: 10 }, function (err, data) { assert.ifError(err); assert(data); assert(Array.isArray(data.topics)); @@ -1138,6 +1181,14 @@ describe('Topic\'s', function () { }); }); }); + + it('should not do anything if tids is empty array', function (done) { + socketTopics.markAsRead({ uid: adminUid }, [], function (err, markedRead) { + assert.ifError(err); + assert(!markedRead); + done(); + }); + }); }); describe('tags', function () { @@ -1388,6 +1439,13 @@ describe('Topic\'s', function () { }); }); + it('should error if not logged in', function (done) { + socketTopics.changeWatching({ uid: 0 }, { tid: tid, type: 'ignore' }, function (err) { + assert.equal(err.message, '[[error:not-logged-in]]'); + done(); + }); + }); + it('should filter ignoring uids', function (done) { socketTopics.changeWatching({ uid: followerUid }, { tid: tid, type: 'ignore' }, function (err) { assert.ifError(err); @@ -1418,7 +1476,7 @@ describe('Topic\'s', function () { topics.toggleFollow(tid, followerUid, function (err, isFollowing) { assert.ifError(err); assert(isFollowing); - topics.isFollowing([tid], followerUid, function (err, isFollowing) { + socketTopics.isFollowed({ uid: followerUid }, tid, function (err, isFollowing) { assert.ifError(err); assert(isFollowing); done(); @@ -1427,6 +1485,44 @@ describe('Topic\'s', function () { }); }); + describe('topics search', function () { + it('should error with invalid data', function (done) { + socketTopics.search({ uid: adminUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should error if no search plugin', function (done) { + socketTopics.search({ uid: adminUid }, { tid: topic.tid, term: 'test' }, function (err) { + assert.equal(err.message, '[[error:no-plugins-available]]'); + done(); + }); + }); + + it('should return results', function (done) { + var plugins = require('../src/plugins'); + plugins.registerHook('myTestPlugin', { + hook: 'filter:topic.search', + method: function (data, callback) { + callback(null, [1, 2, 3]); + }, + }); + socketTopics.search({ uid: adminUid }, { tid: topic.tid, term: 'test' }, function (err, results) { + assert.ifError(err); + assert.deepEqual(results, [1, 2, 3]); + done(); + }); + }); + }); + + it('should check if user is moderator', function (done) { + socketTopics.isModerator({ uid: adminUid }, topic.tid, function (err, isModerator) { + assert.ifError(err); + assert(!isModerator); + done(); + }); + }); after(function (done) { db.emptydb(done);