Merge branch 'develop' into upgrades-refactor

v1.18.x
Julian Lam 8 years ago
commit 2fd4edc0e2

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -273,3 +273,7 @@ body {
background: lighten(@brand-success, 10%);
opacity: 0.5;
}
form small {
color: @gray-light;
}

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

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

@ -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,11 +366,16 @@ $(document).ready(function () {
window.open(this.href, '_blank');
e.preventDefault();
} else if (config.useOutgoingLinksPage) {
ajaxify.go('outgoing?url=' + encodeURIComponent(this.href));
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();
}
}
}
}
};
if ($(this).attr('data-ajaxify') === 'false') {

@ -241,7 +241,14 @@ 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;
}
socket.emit('user.uploadProfileImageFromUrl', {
uid: ajaxify.data.uid,
url: url,
}, function (err, url) {
if (err) {
return app.alertError(err);
}
uploadModal.modal('hide');
@ -256,7 +263,7 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
paramName: 'uid',
paramValue: ajaxify.data.theirid,
}, onUploadComplete);
});
return false;
});
});

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

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

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

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

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

@ -75,6 +75,12 @@ define('forum/groups/details', [
break;
case 'kick':
translator.translate('[[groups:details.kick_confirm]]', function (translated) {
bootbox.confirm(translated, function (confirm) {
if (!confirm) {
return;
}
socket.emit('groups.kick', {
uid: uid,
groupName: groupName,
@ -85,6 +91,8 @@ define('forum/groups/details', [
app.alertError(err.message);
}
});
});
});
break;
case 'update':

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,68 +1,57 @@
'use strict';
var async = require('async');
var sitemap = require('../sitemap');
var meta = require('../meta');
var sitemapController = {};
sitemapController.render = function (req, res, next) {
sitemap.render(function (err, tplData) {
if (err) {
return next(err);
}
var sitemapController = module.exports;
req.app.render('sitemap', tplData, function (err, xml) {
if (err) {
return next(err);
}
sitemapController.render = function (req, res, next) {
async.waterfall([
function (next) {
sitemap.render(next);
},
function (tplData, next) {
req.app.render('sitemap', tplData, next);
},
function (xml) {
res.header('Content-Type', 'application/xml');
res.send(xml);
});
});
},
], next);
};
sitemapController.getPages = function (req, res, next) {
if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
return next();
}
sitemap.getPages(function (err, xml) {
if (err) {
return next(err);
}
res.header('Content-Type', 'application/xml');
res.send(xml);
});
sendSitemap(sitemap.getPages, res, next);
};
sitemapController.getCategories = function (req, res, next) {
if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
return next();
}
sitemap.getCategories(function (err, xml) {
if (err) {
return next(err);
}
res.header('Content-Type', 'application/xml');
res.send(xml);
});
sendSitemap(sitemap.getCategories, res, next);
};
sitemapController.getTopicPage = function (req, res, next) {
sendSitemap(function (callback) {
sitemap.getTopicPage(parseInt(req.params[0], 10), callback);
}, res, next);
};
function sendSitemap(method, res, callback) {
if (parseInt(meta.config['feeds:disableSitemap'], 10) === 1) {
return next();
return callback();
}
sitemap.getTopicPage(parseInt(req.params[0], 10), function (err, xml) {
if (err) {
return next(err);
} else if (!xml) {
return next();
async.waterfall([
function (next) {
method(next);
},
function (xml) {
if (!xml) {
return callback();
}
res.header('Content-Type', 'application/xml');
res.send(xml);
});
};
},
], callback);
}
module.exports = sitemapController;

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

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

@ -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,6 +31,8 @@ usersController.index = function (req, res, next) {
};
usersController.search = function (req, res, next) {
async.waterfall([
function (next) {
async.parallel({
search: function (next) {
user.search({
@ -48,21 +48,22 @@ usersController.search = function (req, res, next) {
isAdminOrGlobalMod: function (next) {
user.isAdminOrGlobalMod(req.uid, next);
},
}, function (err, results) {
if (err) {
return next(err);
}
}, 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);
});
},
], next);
};
usersController.getOnlineUsers = function (req, res, next) {
async.waterfall([
function (next) {
async.parallel({
users: function (next) {
usersController.getUsers('users:online', req.uid, req.query, next);
@ -70,10 +71,9 @@ usersController.getOnlineUsers = function (req, res, next) {
guests: function (next) {
require('../socket.io/admin/rooms').getTotalGuestCount(next);
},
}, function (err, results) {
if (err) {
return next(err);
}
}, next);
},
function (results, next) {
var userData = results.users;
var hiddenCount = 0;
if (!userData.isAdminOrGlobalMod) {
@ -88,7 +88,8 @@ usersController.getOnlineUsers = function (req, res, next) {
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);
}
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,6 +165,8 @@ usersController.getUsers = function (set, uid, query, callback) {
var start = Math.max(0, page - 1) * resultsPerPage;
var stop = start + resultsPerPage - 1;
async.waterfall([
function (next) {
async.parallel({
isAdminOrGlobalMod: function (next) {
user.isAdminOrGlobalMod(uid, next);
@ -176,11 +174,9 @@ usersController.getUsers = function (set, uid, query, callback) {
usersData: function (next) {
usersController.getUsersAndCount(set, uid, start, stop, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
}, next);
},
function (results, next) {
var pageCount = Math.ceil(results.usersData.count / resultsPerPage);
var userData = {
users: results.usersData.users,
@ -191,11 +187,14 @@ usersController.getUsers = function (set, uid, query, callback) {
isAdminOrGlobalMod: results.isAdminOrGlobalMod,
};
userData['section_' + (query.section || 'joindate')] = true;
callback(null, userData);
});
next(null, userData);
},
], callback);
};
usersController.getUsersAndCount = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
async.parallel({
users: function (next) {
user.getUsersFromSet(set, uid, start, stop, next);
@ -212,16 +211,16 @@ usersController.getUsersAndCount = function (set, uid, start, stop, callback) {
db.getObjectField('global', 'userCount', next);
}
},
}, function (err, results) {
if (err) {
return callback(err);
}
}, next);
},
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.render('users', data);
});
},
], next);
}
module.exports = usersController;

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

@ -6,23 +6,25 @@ 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) {
Groups.ephemeralGroups = ['guests'];
Groups.getEphemeralGroup = function (groupName) {
return {
name: groupName,
slug: utils.slugify(groupName),
@ -31,30 +33,24 @@ var utils = require('../public/src/utils');
hidden: '0',
system: '1',
};
},
removeEphemeralGroups: function (groups) {
};
Groups.removeEphemeralGroups = function (groups) {
for (var x = groups.length; x >= 0; x -= 1) {
if (ephemeralGroups.indexOf(groups[x]) !== -1) {
if (Groups.ephemeralGroups.indexOf(groups[x]) !== -1) {
groups.splice(x, 1);
}
}
return groups;
},
};
};
Groups.internals = internals;
var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w:]+$/;
Groups.isPrivilegeGroup = function (groupName) {
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) {
Groups.getGroupsFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
if (set === 'groups:visible:name') {
@ -73,13 +69,15 @@ var utils = require('../public/src/utils');
Groups.getGroupsAndMembers(groupNames, next);
},
], callback);
};
};
Groups.getGroups = function (set, start, stop, callback) {
Groups.getGroups = function (set, start, stop, callback) {
db.getSortedSetRevRange(set, start, stop, callback);
};
};
Groups.getGroupsAndMembers = function (groupNames, callback) {
Groups.getGroupsAndMembers = function (groupNames, callback) {
async.waterfall([
function (next) {
async.parallel({
groups: function (next) {
Groups.getGroupsData(groupNames, next);
@ -87,31 +85,30 @@ var utils = require('../public/src/utils');
members: function (next) {
Groups.getMemberUsers(groupNames, 0, 3, next);
},
}, function (err, data) {
if (err) {
return callback(err);
}
}, next);
},
function (data, next) {
data.groups.forEach(function (group, index) {
if (!group) {
return;
}
if (group) {
group.members = data.members[index] || [];
group.truncated = group.memberCount > data.members.length;
}
});
next(null, data.groups);
},
], callback);
};
callback(null, data.groups);
});
};
Groups.get = function (groupName, options, callback) {
Groups.get = function (groupName, options, callback) {
if (!groupName) {
return callback(new Error('[[error:invalid-group]]'));
}
var stop = -1;
var results;
async.waterfall([
function (next) {
async.parallel({
base: function (next) {
db.getObject('group:' + groupName, next);
@ -124,78 +121,65 @@ var utils = require('../public/src/utils');
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);
Groups.getUsersFromSet('group:' + groupName + ':pending', next);
},
invited: function (next) {
async.waterfall([
function (next) {
db.getSetMembers('group:' + groupName + ':invited', next);
},
function (uids, next) {
user.getUsersData(uids, next);
},
], 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),
}, function (err, results) {
if (err) {
return callback(err);
} else if (!results.base) {
}, 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);
groupData.descriptionParsed = descriptionParsed;
groupData.userTitleEnabled = groupData.userTitleEnabled ? !!parseInt(groupData.userTitleEnabled, 10) : true;
groupData.createtimeISO = utils.toISOString(groupData.createtime);
groupData.members = results.members;
groupData.membersNextStart = stop + 1;
groupData.pending = results.pending.filter(Boolean);
groupData.invited = results.invited.filter(Boolean);
groupData.deleted = !!parseInt(groupData.deleted, 10);
groupData.hidden = !!parseInt(groupData.hidden, 10);
groupData.system = !!parseInt(groupData.system, 10);
groupData.memberCount = parseInt(groupData.memberCount, 10);
groupData.private = (groupData.private === null || groupData.private === undefined) ? true : !!parseInt(groupData.private, 10);
groupData.disableJoinRequests = parseInt(groupData.disableJoinRequests, 10) === 1;
groupData.isMember = results.isMember;
groupData.isPending = results.isPending;
groupData.isInvited = results.isInvited;
groupData.isOwner = results.isOwner;
groupData['cover:url'] = groupData['cover:url'] || require('./coverPhoto').getDefaultGroupCover(groupName);
groupData['cover:position'] = validator.escape(String(groupData['cover:position'] || '50% 50%'));
groupData.labelColor = validator.escape(String(groupData.labelColor || '#000000'));
groupData.icon = validator.escape(String(groupData.icon || ''));
plugins.fireHook('filter:group.get', { group: groupData }, next);
},
function (results, next) {
next(null, results.group);
},
], callback);
};
results.base['cover:url'] = results.base['cover:url'] || require('./coverPhoto').getDefaultGroupCover(groupName);
results.base['cover:position'] = validator.escape(String(results.base['cover:position'] || '50% 50%'));
results.base.labelColor = validator.escape(String(results.base.labelColor || '#000000'));
results.base.icon = validator.escape(String(results.base.icon || ''));
plugins.fireHook('filter:parse.raw', results.base.description, function (err, descriptionParsed) {
if (err) {
return callback(err);
}
Groups.escapeGroupData(results.base);
results.base.descriptionParsed = descriptionParsed;
results.base.userTitleEnabled = results.base.userTitleEnabled ? !!parseInt(results.base.userTitleEnabled, 10) : true;
results.base.createtimeISO = utils.toISOString(results.base.createtime);
results.base.members = results.members;
results.base.membersNextStart = stop + 1;
results.base.pending = results.pending.filter(Boolean);
results.base.invited = results.invited.filter(Boolean);
results.base.deleted = !!parseInt(results.base.deleted, 10);
results.base.hidden = !!parseInt(results.base.hidden, 10);
results.base.system = !!parseInt(results.base.system, 10);
results.base.memberCount = parseInt(results.base.memberCount, 10);
results.base.private = (results.base.private === null || results.base.private === undefined) ? true : !!parseInt(results.base.private, 10);
results.base.disableJoinRequests = parseInt(results.base.disableJoinRequests, 10) === 1;
results.base.isMember = results.isMember;
results.base.isPending = results.isPending;
results.base.isInvited = results.isInvited;
results.base.isOwner = results.isOwner;
plugins.fireHook('filter:group.get', { group: results.base }, function (err, data) {
callback(err, data ? data.group : null);
});
});
});
};
Groups.getOwners = function (groupName, callback) {
Groups.getOwners = function (groupName, callback) {
db.getSetMembers('group:' + groupName + ':owners', callback);
};
};
Groups.getOwnersAndMembers = function (groupName, uid, start, stop, callback) {
Groups.getOwnersAndMembers = function (groupName, uid, start, stop, callback) {
async.waterfall([
function (next) {
async.parallel({
owners: function (next) {
async.waterfall([
@ -210,11 +194,9 @@ var utils = require('../public/src/utils');
members: function (next) {
user.getUsersFromSet('group:' + groupName + ':members', uid, start, stop, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
}, next);
},
function (results, next) {
var ownerUids = [];
results.owners.forEach(function (user) {
if (user) {
@ -228,78 +210,58 @@ 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) {
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) {
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);
}
Groups.isPrivate = function (groupName, callback) {
isFieldOn(groupName, 'private', callback);
};
callback(null, parseInt(isPrivate, 10) !== 0);
});
};
Groups.isHidden = function (groupName, callback) {
db.getObjectField('group:' + groupName, 'hidden', function (err, isHidden) {
if (err) {
return callback(err);
}
Groups.isHidden = function (groupName, callback) {
isFieldOn(groupName, 'hidden', callback);
};
callback(null, parseInt(isHidden, 10) === 1);
});
};
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) {
Groups.exists = function (name, callback) {
if (Array.isArray(name)) {
var slugs = name.map(function (groupName) {
return utils.slugify(groupName);
@ -307,7 +269,7 @@ var utils = require('../public/src/utils');
async.parallel([
function (next) {
next(null, slugs.map(function (slug) {
return ephemeralGroups.indexOf(slug) !== -1;
return Groups.ephemeralGroups.indexOf(slug) !== -1;
}));
},
async.apply(db.isSortedSetMembers, 'groups:createtime', name),
@ -323,130 +285,19 @@ var utils = require('../public/src/utils');
var slug = utils.slugify(name);
async.parallel([
function (next) {
next(null, ephemeralGroups.indexOf(slug) !== -1);
next(null, Groups.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) {
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) {
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);
});
});
};
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) {
Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) {
if (err) {
return next(err);
}
var memberOf = [];
isMembers.forEach(function (isMember, index) {
if (isMember) {
memberOf.push(groupNames[index]);
}
});
Groups.getGroupsData(memberOf, next);
});
}, next);
},
], callback);
};
}(module.exports));
};

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

@ -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);
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);
}
Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) {
if (err) {
return callback(err);
}
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);
});
groupNames = Groups.internals.removeEphemeralGroups(groupNames);
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, 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);
};

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

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

@ -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.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);
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();
return callback(new Error('[[error:not-allowed-to-rename-system-group]]'));
}
Groups.exists(newName, function (err, exists) {
if (err || exists) {
return callback(err || new Error('[[error:group-already-exists]]'));
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,19 +232,24 @@ 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.isSortedSetMember(group, oldName, next);
},
function (isMember, next) {
if (!isMember) {
return callback();
}
db.sortedSetScore(group, oldName, next);
},
function (_score, next) {
@ -245,6 +260,5 @@ module.exports = function (Groups) {
db.sortedSetAdd(group, score, newName, next);
},
], callback);
});
}
};

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

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

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

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

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

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

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

@ -53,22 +53,24 @@ middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) {
The "self" part of this middleware hinges on you having used
middleware.exposeUid prior to invoking this middleware.
*/
if (req.user) {
if (req.user.uid === res.locals.uid) {
return next();
async.waterfall([
function (next) {
if (!req.uid) {
return setImmediate(next, null, false);
}
user.isAdminOrGlobalMod(req.uid, function (err, ok) {
if (err) {
return next(err);
} else if (ok) {
return next();
if (req.uid === parseInt(res.locals.uid, 10)) {
return setImmediate(next, null, true);
}
controllers.helpers.notAllowed(req, res);
});
} else {
controllers.helpers.notAllowed(req, res);
user.isAdminOrGlobalMod(req.uid, next);
},
function (isAdminOrGlobalMod, next) {
if (!isAdminOrGlobalMod) {
return controllers.helpers.notAllowed(req, res);
}
next();
},
], next);
};
middleware.ensureSelfOrPrivileged = function (req, res, next) {

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

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

@ -1,5 +1,6 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var url = require('url');
var winston = require('winston');
@ -14,31 +15,26 @@ var urlRegex = /href="([^"]+)"/g;
module.exports = function (Posts) {
Posts.parsePost = function (postData, callback) {
postData.content = postData.content || '';
postData.content = String(postData.content || '');
if (postData.pid && cache.has(String(postData.pid))) {
postData.content = cache.get(String(postData.pid));
return callback(null, postData);
}
// Casting post content into a string, just in case
if (typeof postData.content !== 'string') {
postData.content = postData.content.toString();
}
plugins.fireHook('filter:parse.post', { postData: postData }, function (err, data) {
if (err) {
return callback(err);
}
async.waterfall([
function (next) {
plugins.fireHook('filter:parse.post', { postData: postData }, next);
},
function (data, next) {
data.postData.content = translator.escape(data.postData.content);
if (global.env === 'production' && data.postData.pid) {
cache.set(String(data.postData.pid), data.postData.content);
}
callback(null, data.postData);
});
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 {

@ -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,6 +155,8 @@ module.exports = function (privileges) {
privileges.categories.get = function (cid, uid, callback) {
var privs = ['topics:create', 'topics:read', 'read'];
async.waterfall([
function (next) {
async.parallel({
privileges: function (next) {
helpers.isUserAllowedTo(privs, uid, cid, next);
@ -165,10 +167,9 @@ module.exports = function (privileges) {
isModerator: function (next) {
user.isModerator(uid, cid, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
}, next);
},
function (results, next) {
var privData = _.object(privs, results.privileges);
var isAdminOrMod = results.isAdministrator || results.isModerator;
@ -181,8 +182,9 @@ module.exports = function (privileges) {
editable: isAdminOrMod,
view_deleted: isAdminOrMod,
isAdminOrMod: isAdminOrMod,
}, callback);
});
}, next);
},
], callback);
};
privileges.categories.isAdminOrMod = function (cid, uid, callback) {
@ -213,15 +215,14 @@ module.exports = function (privileges) {
return callback(null, false);
}
categories.getCategoryField(cid, 'disabled', function (err, disabled) {
if (err) {
return callback(err);
}
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) {
@ -234,8 +235,9 @@ module.exports = function (privileges) {
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]);
});
callback(null, cids.filter(Boolean));
});
next(null, cids.filter(Boolean));
},
], callback);
};
privileges.categories.getBase = function (privilege, cids, uid, callback) {
@ -287,6 +290,8 @@ module.exports = function (privileges) {
return array.indexOf(uid) === index;
});
async.waterfall([
function (next) {
async.parallel({
allowedTo: function (next) {
helpers.isUsersAllowedTo(privilege, uids, cid, next);
@ -297,16 +302,15 @@ module.exports = function (privileges) {
isAdmin: function (next) {
user.isAdministrator(uids, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
}, next);
},
function (results, next) {
uids = uids.filter(function (uid, index) {
return results.allowedTo[index] || results.isModerators[index] || results.isAdmin[index];
});
callback(null, uids);
});
next(null, uids);
},
], callback);
};
privileges.categories.give = function (privileges, cid, groupName, callback) {
@ -324,6 +328,8 @@ module.exports = function (privileges) {
}
privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) {
async.waterfall([
function (next) {
async.parallel({
isAdministrator: function (next) {
user.isAdministrator(uid, next);
@ -334,13 +340,12 @@ module.exports = function (privileges) {
moderatorOfTarget: function (next) {
user.isModerator(uid, targetCid, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
callback(null, results.isAdministrator || (results.moderatorOfCurrent && results.moderatorOfTarget));
});
}, next);
},
function (results, next) {
next(null, results.isAdministrator || (results.moderatorOfCurrent && results.moderatorOfTarget));
},
], callback);
};
privileges.categories.userPrivileges = function (cid, uid, callback) {

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

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

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

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

@ -1,12 +1,14 @@
'use strict';
var async = require('async');
var topics = require('../topics');
var websockets = require('./index');
var user = require('../user');
var apiController = require('../controllers/api');
var socketHelpers = require('./helpers');
var SocketTopics = {};
var SocketTopics = module.exports;
require('./topics/unread')(SocketTopics);
require('./topics/move')(SocketTopics);
@ -23,18 +25,19 @@ SocketTopics.post = function (socket, data, callback) {
data.req = websockets.reqFromSocket(socket);
data.timestamp = Date.now();
topics.post(data, function (err, result) {
if (err) {
return callback(err);
}
callback(null, result.topicData);
async.waterfall([
function (next) {
topics.post(data, next);
},
function (result, next) {
next(null, result.topicData);
socket.emit('event:new_post', { posts: [result.postData] });
socket.emit('event:new_topic', result.topicData);
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;

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

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

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

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

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

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

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

@ -6,70 +6,38 @@ var _ = require('underscore');
var groups = require('./groups');
var plugins = require('./plugins');
var db = require('./database');
var topics = require('./topics');
var privileges = require('./privileges');
var meta = require('./meta');
(function (User) {
User.email = require('./user/email');
User.notifications = require('./user/notifications');
User.reset = require('./user/reset');
User.digest = require('./user/digest');
require('./user/data')(User);
require('./user/auth')(User);
require('./user/bans')(User);
require('./user/create')(User);
require('./user/posts')(User);
require('./user/topics')(User);
require('./user/categories')(User);
require('./user/follow')(User);
require('./user/profile')(User);
require('./user/admin')(User);
require('./user/delete')(User);
require('./user/settings')(User);
require('./user/search')(User);
require('./user/jobs')(User);
require('./user/picture')(User);
require('./user/approval')(User);
require('./user/invite')(User);
require('./user/password')(User);
require('./user/info')(User);
User.updateLastOnlineTime = function (uid, callback) {
callback = callback || function () {};
db.getObjectFields('user:' + uid, ['status', 'lastonline'], function (err, userData) {
var now = Date.now();
if (err || userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) {
return callback(err);
}
User.setUserField(uid, 'lastonline', now, callback);
});
};
User.updateOnlineUsers = function (uid, callback) {
callback = callback || function () {};
var now = Date.now();
async.waterfall([
function (next) {
db.sortedSetScore('users:online', uid, next);
},
function (userOnlineTime, next) {
if (now - parseInt(userOnlineTime, 10) < 300000) {
return callback();
}
db.sortedSetAdd('users:online', now, uid, next);
},
function (next) {
topics.pushUnreadCount(uid);
plugins.fireHook('action:user.online', { uid: uid, timestamp: now });
next();
},
], callback);
};
User.getUidsFromSet = function (set, start, stop, callback) {
var User = module.exports;
User.email = require('./user/email');
User.notifications = require('./user/notifications');
User.reset = require('./user/reset');
User.digest = require('./user/digest');
require('./user/data')(User);
require('./user/auth')(User);
require('./user/bans')(User);
require('./user/create')(User);
require('./user/posts')(User);
require('./user/topics')(User);
require('./user/categories')(User);
require('./user/follow')(User);
require('./user/profile')(User);
require('./user/admin')(User);
require('./user/delete')(User);
require('./user/settings')(User);
require('./user/search')(User);
require('./user/jobs')(User);
require('./user/picture')(User);
require('./user/approval')(User);
require('./user/invite')(User);
require('./user/password')(User);
require('./user/info')(User);
require('./user/online')(User);
User.getUidsFromSet = function (set, start, stop, callback) {
if (set === 'users:online') {
var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1;
var now = Date.now();
@ -77,9 +45,9 @@ var meta = require('./meta');
} else {
db.getSortedSetRevRange(set, start, stop, callback);
}
};
};
User.getUsersFromSet = function (set, uid, start, stop, callback) {
User.getUsersFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
User.getUidsFromSet(set, start, stop, next);
@ -88,9 +56,9 @@ var meta = require('./meta');
User.getUsers(uids, uid, next);
},
], callback);
};
};
User.getUsersWithFields = function (uids, fields, uid, callback) {
User.getUsersWithFields = function (uids, fields, uid, callback) {
async.waterfall([
function (next) {
plugins.fireHook('filter:users.addFields', { fields: fields }, next);
@ -126,86 +94,64 @@ var meta = require('./meta');
next(null, data.users);
},
], callback);
};
};
User.getUsers = function (uids, 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.getUsersWithFields(uids, fields, uid, callback);
};
};
User.getStatus = function (userData) {
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);
});
} else {
db.sortedSetScore('users:online', uid, function (err, lastonline) {
if (err) {
return callback(err);
}
var isOnline = Date.now() - parseInt(lastonline, 10) < 300000;
callback(null, isOnline);
});
}
};
};
User.exists = function (uid, callback) {
User.exists = function (uid, callback) {
db.isSortedSetMember('users:joindate', uid, callback);
};
};
User.existsBySlug = function (userslug, callback) {
User.existsBySlug = function (userslug, callback) {
User.getUidByUserslug(userslug, function (err, exists) {
callback(err, !!exists);
});
};
};
User.getUidByUsername = function (username, callback) {
User.getUidByUsername = function (username, callback) {
if (!username) {
return callback(null, 0);
}
db.sortedSetScore('username:uid', username, callback);
};
};
User.getUidsByUsernames = function (usernames, callback) {
User.getUidsByUsernames = function (usernames, callback) {
db.sortedSetScores('username:uid', usernames, callback);
};
};
User.getUidByUserslug = function (userslug, callback) {
User.getUidByUserslug = function (userslug, callback) {
if (!userslug) {
return callback(null, 0);
}
db.sortedSetScore('userslug:uid', userslug, callback);
};
User.getUsernamesByUids = function (uids, callback) {
User.getUsersFields(uids, ['username'], function (err, users) {
if (err) {
return callback(err);
}
};
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) {
User.getUsernameByUserslug = function (slug, callback) {
async.waterfall([
function (next) {
User.getUidByUserslug(slug, next);
@ -214,47 +160,49 @@ var meta = require('./meta');
User.getUserField(uid, 'username', next);
},
], callback);
};
};
User.getUidByEmail = function (email, callback) {
User.getUidByEmail = function (email, callback) {
db.sortedSetScore('email:uid', email.toLowerCase(), callback);
};
};
User.getUidsByEmails = function (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.getUserField(uid, 'username', callback);
});
};
User.getUsernameByEmail = function (email, callback) {
async.waterfall([
function (next) {
db.sortedSetScore('email:uid', email.toLowerCase(), next);
},
function (uid, next) {
User.getUserField(uid, 'username', next);
},
], callback);
};
User.isModerator = function (uid, cid, callback) {
User.isModerator = function (uid, cid, callback) {
privileges.users.isModerator(uid, cid, callback);
};
};
User.isModeratorOfAnyCategory = function (uid, 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) {
User.isAdministrator = function (uid, callback) {
privileges.users.isAdministrator(uid, callback);
};
};
User.isGlobalModerator = function (uid, callback) {
User.isGlobalModerator = function (uid, callback) {
privileges.users.isGlobalModerator(uid, callback);
};
};
User.isPrivileged = function (uid, callback) {
User.isPrivileged = function (uid, callback) {
async.parallel([
async.apply(User.isAdministrator, uid),
async.apply(User.isGlobalModerator, uid),
@ -262,71 +210,72 @@ var meta = require('./meta');
], function (err, results) {
callback(err, results ? results.some(Boolean) : false);
});
};
};
User.isAdminOrGlobalMod = function (uid, callback) {
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.isAdminOrSelf = function (callerUid, uid, callback) {
isSelfOrMethod(callerUid, uid, User.isAdministrator, callback);
};
User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) {
isSelfOrMethod(callerUid, uid, User.isAdminOrGlobalMod, callback);
};
User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) {
function isSelfOrMethod(callerUid, uid, method, 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]]'));
async.waterfall([
function (next) {
method(callerUid, next);
},
function (isPass, next) {
if (!isPass) {
return next(new Error('[[error:no-privileges]]'));
}
callback();
});
};
next();
},
], 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.getUsersData(uids, callback);
});
};
User.getAdminsandGlobalMods = function (callback) {
async.waterfall([
function (next) {
async.parallel([
async.apply(groups.getMembers, 'administrators', 0, -1),
async.apply(groups.getMembers, 'Global Moderators', 0, -1),
], next);
},
function (results, next) {
User.getUsersData(_.union(results), next);
},
], callback);
};
User.getAdminsandGlobalModsandModerators = function (callback) {
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),
], function (err, results) {
if (err) {
return callback(err);
}
User.getUsersData(_.union.apply(_, results), callback);
});
};
], next);
},
function (results, next) {
User.getUsersData(_.union.apply(_, results), next);
},
], callback);
};
User.getModeratorUids = function (callback) {
User.getModeratorUids = function (callback) {
async.waterfall([
async.apply(db.getSortedSetRange, 'categories:cid', 0, -1),
function (cids, next) {
@ -334,18 +283,15 @@ var meta = require('./meta');
return 'cid:' + cid + ':privileges:mods';
});
groups.getMembersOfGroups(groupNames, function (err, memberSets) {
if (err) {
return next(err);
}
groups.getMembersOfGroups(groupNames, next);
},
function (memberSets, next) {
next(null, _.union.apply(_, memberSets));
});
},
], callback);
};
};
User.getModeratedCids = function (uid, callback) {
User.getModeratedCids = function (uid, callback) {
var cids;
async.waterfall([
function (next) {
@ -362,9 +308,9 @@ var meta = require('./meta');
next(null, cids);
},
], callback);
};
};
User.addInterstitials = function (callback) {
User.addInterstitials = function (callback) {
plugins.registerHook('core', {
hook: 'filter:register.interstitial',
method: function (data, callback) {
@ -389,5 +335,5 @@ var meta = require('./meta');
});
callback();
};
}(exports));
};

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

@ -1,5 +1,6 @@
'use strict';
var async = require('async');
var validator = require('validator');
var nconf = require('nconf');
var winston = require('winston');
@ -63,6 +64,8 @@ module.exports = function (User) {
addField('lastonline');
}
async.waterfall([
function (next) {
db.getObjectsFields(keys, fields, function (err, users) {
if (err) {
return callback(err);
@ -72,8 +75,13 @@ module.exports = function (User) {
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,6 +113,8 @@ module.exports = function (User) {
return 'user:' + uid;
});
async.waterfall([
function (next) {
db.getObjects(keys, function (err, users) {
if (err) {
return callback(err);
@ -114,8 +124,13 @@ module.exports = function (User) {
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);
}
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' });
callback();
});
next();
},
], callback);
};
User.setUserFields = function (uid, data, callback) {
callback = callback || function () {};
db.setObject('user:' + uid, data, function (err) {
if (err) {
return callback(err);
}
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) {
incrDecrUserFieldBy(uid, field, -value, 'decrement', callback);
};
function incrDecrUserFieldBy(uid, field, value, type, 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' });
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 });
callback(null, value);
});
};
next(null, value);
},
], callback);
}
};

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

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

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

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

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

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

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

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

@ -61,11 +61,9 @@ 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');
require('./socket.io').server.emit('event:nodebb.ready', {
@ -74,8 +72,9 @@ module.exports.listen = function (callback) {
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) {

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

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

@ -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,14 +498,26 @@ 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);
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();
});
});
});
});
});
it('should load group members page', function (done) {
request(nconf.get('url') + '/groups/group-details/members', function (err, res, body) {
@ -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();

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

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

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

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

Loading…
Cancel
Save