Merge branch 'develop' into upgrades-refactor

This commit is contained in:
Julian Lam
2017-03-10 15:44:40 -05:00
96 changed files with 2397 additions and 1851 deletions

View File

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

View File

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

View File

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

View File

@@ -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 <em>(Default: enabled)</em>",
"private-groups.warning": "<strong>Beware!</strong> If this option is disabled and you have private groups, they automatically become public.",

View File

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

View File

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

View File

@@ -31,7 +31,7 @@
"signature": "Потпис",
"birthday": "Рођендан",
"chat": "Ђаскање",
"chat_with": "Настави ћаскање са %1",
"chat_with": "Ћаскај са %1",
"new_chat_with": "Започни ново ћаскање са %1",
"flag-profile": "Означи профил",
"follow": "Прати",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "邀请注册",

View File

@@ -272,4 +272,8 @@ body {
border: 1px dashed @brand-success;
background: lighten(@brand-success, 10%);
opacity: 0.5;
}
form small {
color: @gray-light;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = $('<link id="bootswatchCSS" href="' + cssSource + '" rel="stylesheet" media="screen">');
$('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 = {};

View File

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

View File

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

View File

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

View File

@@ -76,7 +76,6 @@ define('forum/flags/list', ['components', 'Chart'], function (components, Chart)
yAxes: [{
ticks: {
beginAtZero: true,
stepSize: 1,
},
}],
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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('<a href="' + src + '" target="_blank">');
}
});
};
return Images;
});

View File

@@ -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('<a href="' + src + '" target="_blank">');
}
});
};
Posts.showBottomPostBar = function () {
var mainPost = components.get('post', 'index', 0);
var placeHolder = $('.post-bar-placeholder');

View File

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

View File

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

View File

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

47
src/controllers/404.js Normal file
View File

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

View File

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

View File

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

View File

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

63
src/controllers/errors.js Normal file
View File

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

View File

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

32
src/controllers/osd.js Normal file
View File

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

View File

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

View File

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

View File

@@ -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) {
if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
return next();
}
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);
});
sendSitemap(function (callback) {
sitemap.getTopicPage(parseInt(req.params[0], 10), callback);
}, res, next);
};
module.exports = sitemapController;
function sendSitemap(method, res, callback) {
if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
return callback();
}
async.waterfall([
function (next) {
method(next);
},
function (xml) {
if (!xml) {
return callback();
}
res.header('Content-Type', 'application/xml');
res.send(xml);
},
], callback);
}

View File

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

99
src/controllers/user.js Normal file
View File

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

View File

@@ -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);
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);
},
guests: function (next) {
require('../socket.io/admin/rooms').getTotalGuestCount(next);
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;
render(req, res, userData, 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';
});
}
userData.anonymousUserCount = results.guests + hiddenCount;
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 (err, results) {
if (err) {
return callback(err);
}
results.users = results.users.filter(function (user) {
return user && parseInt(user.uid, 10);
});
function (results, next) {
results.users = results.users.filter(function (user) {
return user && parseInt(user.uid, 10);
});
callback(null, results);
});
next(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);
}
async.waterfall([
function (next) {
user.getInvitesNumber(req.uid, next);
},
function (numInvites) {
res.append('X-Total-Count', data.userCount);
data.invites = numInvites;
res.append('X-Total-Count', data.userCount);
data.invites = numInvites;
res.render('users', data);
});
res.render('users', data);
},
], next);
}
module.exports = usersController;

View File

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

View File

@@ -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 Groups = module.exports;
var ephemeralGroups = ['guests'];
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);
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;
},
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);
}
data.groups.forEach(function (group, index) {
if (!group) {
return;
}
group.members = data.members[index] || [];
group.truncated = group.memberCount > data.members.length;
});
callback(null, data.groups);
});
};
Groups.get = function (groupName, options, callback) {
if (!groupName) {
return callback(new Error('[[error:invalid-group]]'));
Groups.removeEphemeralGroups = function (groups) {
for (var x = groups.length; x >= 0; x -= 1) {
if (Groups.ephemeralGroups.indexOf(groups[x]) !== -1) {
groups.splice(x, 1);
}
}
var stop = -1;
return groups;
};
async.parallel({
base: function (next) {
db.getObject('group:' + groupName, next);
},
members: function (next) {
if (options.truncateUserList) {
stop = (parseInt(options.userListCount, 10) || 4) - 1;
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) {
group.members = data.members[index] || [];
group.truncated = group.memberCount > data.members.length;
}
});
next(null, data.groups);
},
], callback);
};
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.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;
}
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]]'));
}
plugins.fireHook('filter:parse.raw', results.base.description, next);
},
function (descriptionParsed, next) {
var groupData = results.base;
Groups.escapeGroupData(groupData);
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 || ''));
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:parse.raw', results.base.description, function (err, descriptionParsed) {
if (err) {
return callback(err);
}
plugins.fireHook('filter:group.get', { group: groupData }, next);
},
function (results, next) {
next(null, results.group);
},
], callback);
};
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);
}
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);
});
};
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.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]]'));
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.isPrivate = function (groupName, callback) {
isFieldOn(groupName, 'private', callback);
};
Groups.isHidden = function (groupName, callback) {
isFieldOn(groupName, 'hidden', callback);
};
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);
}
Groups.exists = function (name, callback) {
if (Array.isArray(name)) {
var slugs = name.map(function (groupName) {
return utils.slugify(groupName);
});
};
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.getMultipleGroupFields = function (groups, fields, callback) {
db.getObjectsFields(groups.map(function (group) {
return 'group:' + group;
}), fields, 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.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);
}
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);
});
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];
async.parallel([
function (next) {
next(null, slugs.map(function (slug) {
return Groups.ephemeralGroups.indexOf(slug) !== -1;
}));
});
} 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([
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);
};
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);
next(null, Groups.ephemeralGroups.indexOf(slug) !== -1);
},
function (groupNames, next) {
async.map(uids, function (uid, next) {
Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) {
if (err) {
return next(err);
}
async.apply(db.isSortedSetMember, 'groups:createtime', name),
], function (err, results) {
callback(err, !err ? (results[0] || results[1]) : null);
});
}
};
var memberOf = [];
isMembers.forEach(function (isMember, index) {
if (isMember) {
memberOf.push(groupNames[index]);
}
});
Groups.getGroupsData(memberOf, next);
});
}, next);
},
], callback);
};
}(module.exports));
Groups.existsBySlug = function (slug, callback) {
if (Array.isArray(slug)) {
db.isObjectFields('groupslug:groupname', slug, callback);
} else {
db.isObjectField('groupslug:groupname', slug, callback);
}
};

93
src/groups/data.js Normal file
View File

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

View File

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

32
src/groups/posts.js Normal file
View File

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

View File

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

View File

@@ -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);
}
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]]'));
var group;
async.waterfall([
function (next) {
db.getObject('group:' + oldName, next);
},
function (_group, next) {
group = _group;
if (!group) {
return callback();
}
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);
}
};

50
src/groups/user.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}
user.isAdminOrGlobalMod(req.uid, function (err, ok) {
if (err) {
return next(err);
} else if (ok) {
return next();
async.waterfall([
function (next) {
if (!req.uid) {
return setImmediate(next, null, false);
}
controllers.helpers.notAllowed(req, res);
});
} else {
controllers.helpers.notAllowed(req, res);
}
if (req.uid === parseInt(res.locals.uid, 10)) {
return setImmediate(next, null, true);
}
user.isAdminOrGlobalMod(req.uid, next);
},
function (isAdminOrGlobalMod, next) {
if (!isAdminOrGlobalMod) {
return controllers.helpers.notAllowed(req, res);
}
next();
},
], next);
};
middleware.ensureSelfOrPrivileged = function (req, res, next) {

View File

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

View File

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

View File

@@ -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();
}
async.waterfall([
function (next) {
plugins.fireHook('filter:parse.post', { postData: postData }, next);
},
function (data, next) {
data.postData.content = translator.escape(data.postData.content);
plugins.fireHook('filter:parse.post', { postData: postData }, function (err, data) {
if (err) {
return callback(err);
}
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) {

View File

@@ -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);
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);
},
isAdministrator: function (next) {
user.isAdministrator(uid, next);
},
isModerator: function (next) {
user.isModerator(uid, cid, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
var privData = _.object(privs, results.privileges);
var isAdminOrMod = results.isAdministrator || results.isModerator;
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,
}, callback);
});
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);
},
], 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);
}
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]);
});
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);
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);
},
isModerators: function (next) {
user.isModerator(uids, cid, next);
function (results, next) {
uids = uids.filter(function (uid, index) {
return results.allowedTo[index] || results.isModerators[index] || results.isAdmin[index];
});
next(null, uids);
},
isAdmin: function (next) {
user.isAdministrator(uids, next);
},
}, 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);
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);
},
moderatorOfCurrent: function (next) {
user.isModerator(uid, currentCid, next);
function (results, next) {
next(null, results.isAdministrator || (results.moderatorOfCurrent && results.moderatorOfTarget));
},
moderatorOfTarget: function (next) {
user.isModerator(uid, targetCid, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
callback(null, results.isAdministrator || (results.moderatorOfCurrent && results.moderatorOfTarget));
});
], callback);
};
privileges.categories.userPrivileges = function (cid, uid, callback) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}
async.waterfall([
function (next) {
topics.post(data, next);
},
function (result, next) {
next(null, result.topicData);
callback(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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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');
var User = module.exports;
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.email = require('./user/email');
User.notifications = require('./user/notifications');
User.reset = require('./user/reset');
User.digest = require('./user/digest');
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 () {};
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);
};
db.getSortedSetRevRangeByScore(set, start, count, '+inf', now - 300000, callback);
} else {
db.getSortedSetRevRange(set, start, stop, 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.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);
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);
});
}
};
plugins.fireHook('filter:userlist.get', { users: results.userData, uid: uid }, next);
},
function (data, next) {
next(null, data.users);
},
], callback);
};
User.exists = function (uid, callback) {
db.isSortedSetMember('users:joindate', uid, 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.existsBySlug = function (userslug, callback) {
User.getUidByUserslug(userslug, function (err, exists) {
callback(err, !!exists);
});
};
User.getUsersWithFields(uids, fields, uid, callback);
};
User.getUidByUsername = function (username, callback) {
if (!username) {
return callback(null, 0);
}
db.sortedSetScore('username:uid', username, callback);
};
User.getStatus = function (userData) {
var isOnline = (Date.now() - parseInt(userData.lastonline, 10)) < 300000;
return isOnline ? (userData.status || 'online') : 'offline';
};
User.getUidsByUsernames = function (usernames, callback) {
db.sortedSetScores('username:uid', usernames, callback);
};
User.exists = function (uid, callback) {
db.isSortedSetMember('users:joindate', uid, callback);
};
User.getUidByUserslug = function (userslug, callback) {
if (!userslug) {
return callback(null, 0);
}
db.sortedSetScore('userslug:uid', userslug, callback);
};
User.existsBySlug = function (userslug, callback) {
User.getUidByUserslug(userslug, function (err, exists) {
callback(err, !!exists);
});
};
User.getUsernamesByUids = function (uids, callback) {
User.getUsersFields(uids, ['username'], function (err, users) {
if (err) {
return callback(err);
}
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);
});
};
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.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.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.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.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]]'));
}
User.getUserField(uid, 'username', callback);
});
};
next();
},
], callback);
}
User.isModerator = function (uid, cid, callback) {
privileges.users.isModerator(uid, cid, 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.isModeratorOfAnyCategory = function (uid, callback) {
User.getModeratedCids(uid, function (err, cids) {
callback(err, Array.isArray(cids) ? !!cids.length : false);
});
};
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.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);
}
var uids = results.admins.concat(results.mods).filter(function (uid, index, array) {
return uid && array.indexOf(uid) === index;
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);
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;
}
next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]'));
},
});
}
User.getUsersData(_.union.apply(_, results), callback);
});
};
callback(null, data);
},
});
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';
});
callback();
};
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();
};
}(exports));

View File

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

View File

@@ -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);
}
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]];
});
users = uids.map(function (uid) {
return users[ref[uid]];
});
modifyUserData(users, fieldsToRemove, callback);
});
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);
}
async.waterfall([
function (next) {
db.getObjects(keys, function (err, users) {
if (err) {
return callback(err);
}
users = uids.map(function (uid) {
return users[ref[uid]];
});
users = uids.map(function (uid) {
return users[ref[uid]];
});
modifyUserData(users, [], callback);
});
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);
}
};

70
src/user/online.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -36,13 +36,7 @@
<li data-plugin-id="{installed.id}" class="clearfix">
<div class="pull-right">
<button class="btn btn-default disabled"><i class="fa fa-exclamation-triangle"></i> [[admin/extend/plugins:plugin-item.unknown]]</button>
<<<<<<< HEAD
<button data-action="toggleInstall" data-installed="1" class="btn btn-danger"><i class="fa fa-trash-o"></i> Uninstall</button>
=======
<button data-action="toggleInstall" data-installed="1" class="btn btn-danger"><i class="fa fa-trash-o"></i> [[admin/extend/plugins:plugin-item.uninstall]]</button>
>>>>>>> `admin/extend` translations
</div>
<h2><strong>{installed.id}</strong></h2>

View File

@@ -31,8 +31,8 @@
<label>[[admin/settings/general:description]]</label>
<input type="text" class="form-control" placeholder="[[admin/settings/general:description.placeholder]]" data-field="description" /><br />
<label>[[admin/settings/general:keywords]]</label>
<input type="text" class="form-control" placeholder="[[admin/settings/general:keywords-placeholder]]" data-field="keywords" /><br />
<label>[[admin/settings/general:keywords]]</label><br />
<input type="text" class="form-control" placeholder="[[admin/settings/general:keywords-placeholder]]" data-field="keywords" data-field-type="tagsinput" /><br />
</form>
</div>
</div>
@@ -140,6 +140,11 @@
<span class="mdl-switch__label"><strong>[[admin/settings/general:outgoing-links.warning-page]]</strong></span>
</label>
</div>
<div class="form-group">
<label for="outgoingLinks:whitelist">[[admin/settings/general:outgoing-links.whitelist]]</label><br />
<input id="outgoingLinks:whitelist" type="text" class="form-control" placeholder="subdomain.domain.com" data-field="outgoingLinks:whitelist" data-field-type="tagsinput" />
</div>
</form>
</div>
</div>

View File

@@ -43,7 +43,7 @@
<p class="help-block">
[[admin/settings/group:default-cover-help]]
</p>
<input type="text" class="form-control input-lg" id="groups:defaultCovers" data-field="groups:defaultCovers" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" /><br />
<input type="text" class="form-control input-lg" id="groups:defaultCovers" data-field="groups:defaultCovers" data-field-type="tagsinput" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" /><br />
</form>
</div>
</div>

View File

@@ -50,7 +50,7 @@
<div class="form-group">
<label for="allowedFileExtensions">[[admin/settings/uploads:allowed-file-extensions]]</label>
<input type="text" class="form-control" value="" data-field="allowedFileExtensions" />
<input type="text" class="form-control" value="" data-field="allowedFileExtensions" data-field-type="tagsinput" />
<p class="help-block">
[[admin/settings/uploads:allowed-file-extensions-help]]
</p>
@@ -131,7 +131,7 @@
<p class="help-block">
[[admin/settings/uploads:default-covers-help]]
</p>
<input type="text" class="form-control input-lg" id="profile:defaultCovers" data-field="profile:defaultCovers" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" />
<input type="text" class="form-control input-lg" id="profile:defaultCovers" data-field="profile:defaultCovers" data-field-type="tagsinput" value="{config.relative_path}/assets/images/cover-default.png" placeholder="https://example.com/group1.png, https://example.com/group2.png" />
</form>
</div>
</div>

View File

@@ -61,21 +61,20 @@ module.exports.listen = function (callback) {
logger.init(app);
initializeNodeBB(function (err) {
if (err) {
return callback(err);
}
async.waterfall([
initializeNodeBB,
function (next) {
winston.info('NodeBB Ready');
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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: '<img src="boop"/><a href="link">test</a> 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 = '<a href="/users">test</a> <a href="youtube.com">youtube</a>';
var parsedContent = posts.relativeToAbsolute(content);
assert.equal(parsedContent, '<a href="' + nconf.get('url') + '/users">test</a> <a href="//youtube.com">youtube</a>');
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();
});

View File

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

View File

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