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

@ -272,4 +272,8 @@ body {
border: 1px dashed @brand-success;
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,8 +366,13 @@ $(document).ready(function () {
window.open(this.href, '_blank');
e.preventDefault();
} else if (config.useOutgoingLinksPage) {
ajaxify.go('outgoing?url=' + encodeURIComponent(this.href));
e.preventDefault();
var safeUrls = config.outgoingLinksWhitelist.trim().split(/[\s,]+/g);
var href = this.href;
if (!safeUrls.some(function (url) { return href.indexOf(url) !== -1; })) {
ajaxify.go('outgoing?url=' + encodeURIComponent(href));
e.preventDefault();
}
}
}
}

@ -241,22 +241,29 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
uploadModal.find('.upload-btn').on('click', function () {
var url = uploadModal.find('#uploadFromUrl').val();
if (!url) {
return;
return false;
}
uploadModal.modal('hide');
pictureCropper.handleImageCrop({
socket.emit('user.uploadProfileImageFromUrl', {
uid: ajaxify.data.uid,
url: url,
socketMethod: 'user.uploadCroppedPicture',
aspectRatio: '1 / 1',
allowSkippingCrop: false,
restrictImageDimension: true,
imageDimension: ajaxify.data.profileImageDimension,
paramName: 'uid',
paramValue: ajaxify.data.theirid,
}, onUploadComplete);
}, function (err, url) {
if (err) {
return app.alertError(err);
}
uploadModal.modal('hide');
pictureCropper.handleImageCrop({
url: url,
socketMethod: 'user.uploadCroppedPicture',
aspectRatio: '1 / 1',
allowSkippingCrop: false,
restrictImageDimension: true,
imageDimension: ajaxify.data.profileImageDimension,
paramName: 'uid',
paramValue: ajaxify.data.theirid,
}, onUploadComplete);
});
return false;
});
});

@ -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,15 +75,23 @@ define('forum/groups/details', [
break;
case 'kick':
socket.emit('groups.kick', {
uid: uid,
groupName: groupName,
}, function (err) {
if (!err) {
userRow.slideUp().remove();
} else {
app.alertError(err.message);
}
translator.translate('[[groups:details.kick_confirm]]', function (translated) {
bootbox.confirm(translated, function (confirm) {
if (!confirm) {
return;
}
socket.emit('groups.kick', {
uid: uid,
groupName: groupName,
}, function (err) {
if (!err) {
userRow.slideUp().remove();
} else {
app.alertError(err.message);
}
});
});
});
break;

@ -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);
}
helpers.redirect(res, path);
});
async.waterfall([
function (next) {
posts.generatePostPath(pid, req.uid, next);
},
function (path, next) {
if (!path) {
return next();
}
helpers.redirect(res, path);
},
], next);
};
module.exports = postsController;
postsController.getRecentPosts = function (req, res, next) {
async.waterfall([
function (next) {
posts.getRecentPosts(req.uid, 0, 19, req.params.term, next);
},
function (data) {
res.json(data);
},
], next);
};

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

@ -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,62 +31,65 @@ usersController.index = function (req, res, next) {
};
usersController.search = function (req, res, next) {
async.parallel({
search: function (next) {
user.search({
query: req.query.term,
searchBy: req.query.searchBy || 'username',
page: req.query.page || 1,
sortBy: req.query.sortBy,
onlineOnly: req.query.onlineOnly === 'true',
bannedOnly: req.query.bannedOnly === 'true',
flaggedOnly: req.query.flaggedOnly === 'true',
async.waterfall([
function (next) {
async.parallel({
search: function (next) {
user.search({
query: req.query.term,
searchBy: req.query.searchBy || 'username',
page: req.query.page || 1,
sortBy: req.query.sortBy,
onlineOnly: req.query.onlineOnly === 'true',
bannedOnly: req.query.bannedOnly === 'true',
flaggedOnly: req.query.flaggedOnly === 'true',
}, next);
},
isAdminOrGlobalMod: function (next) {
user.isAdminOrGlobalMod(req.uid, next);
},
}, next);
},
isAdminOrGlobalMod: function (next) {
user.isAdminOrGlobalMod(req.uid, next);
function (results, next) {
var section = req.query.section || 'joindate';
results.search.isAdminOrGlobalMod = results.isAdminOrGlobalMod;
results.search.pagination = pagination.create(req.query.page, results.search.pageCount, req.query);
results.search['section_' + section] = true;
render(req, res, results.search, next);
},
}, function (err, results) {
if (err) {
return next(err);
}
var section = req.query.section || 'joindate';
results.search.isAdminOrGlobalMod = results.isAdminOrGlobalMod;
results.search.pagination = pagination.create(req.query.page, results.search.pageCount, req.query);
results.search['section_' + section] = true;
render(req, res, results.search, next);
});
], next);
};
usersController.getOnlineUsers = function (req, res, next) {
async.parallel({
users: function (next) {
usersController.getUsers('users:online', req.uid, req.query, next);
},
guests: function (next) {
require('../socket.io/admin/rooms').getTotalGuestCount(next);
async.waterfall([
function (next) {
async.parallel({
users: function (next) {
usersController.getUsers('users:online', req.uid, req.query, next);
},
guests: function (next) {
require('../socket.io/admin/rooms').getTotalGuestCount(next);
},
}, next);
},
}, function (err, results) {
if (err) {
return next(err);
}
var userData = results.users;
var hiddenCount = 0;
if (!userData.isAdminOrGlobalMod) {
userData.users = userData.users.filter(function (user) {
if (user && user.status === 'offline') {
hiddenCount += 1;
}
return user && user.status !== 'offline';
});
}
function (results, next) {
var userData = results.users;
var hiddenCount = 0;
if (!userData.isAdminOrGlobalMod) {
userData.users = userData.users.filter(function (user) {
if (user && user.status === 'offline') {
hiddenCount += 1;
}
return user && user.status !== 'offline';
});
}
userData.anonymousUserCount = results.guests + hiddenCount;
userData.anonymousUserCount = results.guests + hiddenCount;
render(req, res, userData, next);
});
render(req, res, userData, next);
},
], next);
};
usersController.getUsersSortedByPosts = function (req, res, next) {
@ -107,41 +108,36 @@ usersController.getUsersSortedByJoinDate = function (req, res, next) {
};
usersController.getBannedUsers = function (req, res, next) {
usersController.getUsers('users:banned', req.uid, req.query, function (err, userData) {
if (err) {
return next(err);
}
if (!userData.isAdminOrGlobalMod) {
return next();
}
render(req, res, userData, next);
});
renderIfAdminOrGlobalMod('users:banned', req, res, next);
};
usersController.getFlaggedUsers = function (req, res, next) {
usersController.getUsers('users:flags', req.uid, req.query, function (err, userData) {
if (err) {
return next(err);
}
if (!userData.isAdminOrGlobalMod) {
return next();
}
render(req, res, userData, next);
});
renderIfAdminOrGlobalMod('users:flags', req, res, next);
};
usersController.renderUsersPage = function (set, req, res, next) {
usersController.getUsers(set, req.uid, req.query, function (err, userData) {
if (err) {
return next(err);
}
function renderIfAdminOrGlobalMod(set, req, res, next) {
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(req.uid, next);
},
function (isAdminOrGlobalMod, next) {
if (!isAdminOrGlobalMod) {
return helpers.notAllowed(req, res);
}
usersController.renderUsersPage(set, req, res, next);
},
], next);
}
render(req, res, userData, next);
});
usersController.renderUsersPage = function (set, req, res, next) {
async.waterfall([
function (next) {
usersController.getUsers(set, req.uid, req.query, next);
},
function (userData, next) {
render(req, res, userData, next);
},
], next);
};
usersController.getUsers = function (set, uid, query, callback) {
@ -169,59 +165,62 @@ usersController.getUsers = function (set, uid, query, callback) {
var start = Math.max(0, page - 1) * resultsPerPage;
var stop = start + resultsPerPage - 1;
async.parallel({
isAdminOrGlobalMod: function (next) {
user.isAdminOrGlobalMod(uid, next);
async.waterfall([
function (next) {
async.parallel({
isAdminOrGlobalMod: function (next) {
user.isAdminOrGlobalMod(uid, next);
},
usersData: function (next) {
usersController.getUsersAndCount(set, uid, start, stop, next);
},
}, next);
},
usersData: function (next) {
usersController.getUsersAndCount(set, uid, start, stop, next);
function (results, next) {
var pageCount = Math.ceil(results.usersData.count / resultsPerPage);
var userData = {
users: results.usersData.users,
pagination: pagination.create(page, pageCount, query),
userCount: results.usersData.count,
title: setToData[set].title || '[[pages:users/latest]]',
breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs),
isAdminOrGlobalMod: results.isAdminOrGlobalMod,
};
userData['section_' + (query.section || 'joindate')] = true;
next(null, userData);
},
}, function (err, results) {
if (err) {
return callback(err);
}
var pageCount = Math.ceil(results.usersData.count / resultsPerPage);
var userData = {
users: results.usersData.users,
pagination: pagination.create(page, pageCount, query),
userCount: results.usersData.count,
title: setToData[set].title || '[[pages:users/latest]]',
breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs),
isAdminOrGlobalMod: results.isAdminOrGlobalMod,
};
userData['section_' + (query.section || 'joindate')] = true;
callback(null, userData);
});
], callback);
};
usersController.getUsersAndCount = function (set, uid, start, stop, callback) {
async.parallel({
users: function (next) {
user.getUsersFromSet(set, uid, start, stop, next);
async.waterfall([
function (next) {
async.parallel({
users: function (next) {
user.getUsersFromSet(set, uid, start, stop, next);
},
count: function (next) {
if (set === 'users:online') {
var now = Date.now();
db.sortedSetCount('users:online', now - 300000, '+inf', next);
} else if (set === 'users:banned') {
db.sortedSetCard('users:banned', next);
} else if (set === 'users:flags') {
db.sortedSetCard('users:flags', next);
} else {
db.getObjectField('global', 'userCount', next);
}
},
}, next);
},
count: function (next) {
if (set === 'users:online') {
var now = Date.now();
db.sortedSetCount('users:online', now - 300000, '+inf', next);
} else if (set === 'users:banned') {
db.sortedSetCard('users:banned', next);
} else if (set === 'users:flags') {
db.sortedSetCard('users:flags', next);
} else {
db.getObjectField('global', 'userCount', next);
}
function (results, next) {
results.users = results.users.filter(function (user) {
return user && parseInt(user.uid, 10);
});
next(null, results);
},
}, function (err, results) {
if (err) {
return callback(err);
}
results.users = results.users.filter(function (user) {
return user && parseInt(user.uid, 10);
});
callback(null, results);
});
], callback);
};
function render(req, res, data, next) {
@ -232,16 +231,15 @@ function render(req, res, data, next) {
data.adminInviteOnly = registrationType === 'admin-invite-only';
data['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
user.getInvitesNumber(req.uid, function (err, numInvites) {
if (err) {
return next(err);
}
res.append('X-Total-Count', data.userCount);
data.invites = numInvites;
async.waterfall([
function (next) {
user.getInvitesNumber(req.uid, next);
},
function (numInvites) {
res.append('X-Total-Count', data.userCount);
data.invites = numInvites;
res.render('users', data);
});
res.render('users', data);
},
], next);
}
module.exports = usersController;

@ -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,215 +6,197 @@ var validator = require('validator');
var user = require('./user');
var db = require('./database');
var plugins = require('./plugins');
var posts = require('./posts');
var privileges = require('./privileges');
var utils = require('../public/src/utils');
(function (Groups) {
require('./groups/create')(Groups);
require('./groups/delete')(Groups);
require('./groups/update')(Groups);
require('./groups/membership')(Groups);
require('./groups/ownership')(Groups);
require('./groups/search')(Groups);
require('./groups/cover')(Groups);
var ephemeralGroups = ['guests'];
var internals = {
getEphemeralGroup: function (groupName) {
return {
name: groupName,
slug: utils.slugify(groupName),
description: '',
deleted: '0',
hidden: '0',
system: '1',
};
},
removeEphemeralGroups: function (groups) {
for (var x = groups.length; x >= 0; x -= 1) {
if (ephemeralGroups.indexOf(groups[x]) !== -1) {
groups.splice(x, 1);
}
}
return groups;
},
var Groups = module.exports;
require('./groups/data')(Groups);
require('./groups/create')(Groups);
require('./groups/delete')(Groups);
require('./groups/update')(Groups);
require('./groups/membership')(Groups);
require('./groups/ownership')(Groups);
require('./groups/search')(Groups);
require('./groups/cover')(Groups);
require('./groups/posts')(Groups);
require('./groups/user')(Groups);
Groups.ephemeralGroups = ['guests'];
Groups.getEphemeralGroup = function (groupName) {
return {
name: groupName,
slug: utils.slugify(groupName),
description: '',
deleted: '0',
hidden: '0',
system: '1',
};
};
Groups.internals = internals;
var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w:]+$/;
Groups.isPrivilegeGroup = function (groupName) {
return isPrivilegeGroupRegex.test(groupName);
};
Groups.getEphemeralGroups = function () {
return ephemeralGroups;
};
Groups.getGroupsFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
if (set === 'groups:visible:name') {
db.getSortedSetRangeByLex(set, '-', '+', start, stop - start + 1, next);
} else {
db.getSortedSetRevRange(set, start, stop, next);
}
},
function (groupNames, next) {
if (set === 'groups:visible:name') {
groupNames = groupNames.map(function (name) {
return name.split(':')[1];
});
}
Groups.getGroupsAndMembers(groupNames, next);
},
], callback);
};
Groups.getGroups = function (set, start, stop, callback) {
db.getSortedSetRevRange(set, start, stop, callback);
};
Groups.getGroupsAndMembers = function (groupNames, callback) {
async.parallel({
groups: function (next) {
Groups.getGroupsData(groupNames, next);
},
members: function (next) {
Groups.getMemberUsers(groupNames, 0, 3, next);
},
}, function (err, data) {
if (err) {
return callback(err);
Groups.removeEphemeralGroups = function (groups) {
for (var x = groups.length; x >= 0; x -= 1) {
if (Groups.ephemeralGroups.indexOf(groups[x]) !== -1) {
groups.splice(x, 1);
}
}
return groups;
};
var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w:]+$/;
Groups.isPrivilegeGroup = function (groupName) {
return isPrivilegeGroupRegex.test(groupName);
};
Groups.getGroupsFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
if (set === 'groups:visible:name') {
db.getSortedSetRangeByLex(set, '-', '+', start, stop - start + 1, next);
} else {
db.getSortedSetRevRange(set, start, stop, next);
}
},
function (groupNames, next) {
if (set === 'groups:visible:name') {
groupNames = groupNames.map(function (name) {
return name.split(':')[1];
});
}
Groups.getGroupsAndMembers(groupNames, next);
},
], callback);
};
Groups.getGroups = function (set, start, stop, callback) {
db.getSortedSetRevRange(set, start, stop, callback);
};
Groups.getGroupsAndMembers = function (groupNames, callback) {
async.waterfall([
function (next) {
async.parallel({
groups: function (next) {
Groups.getGroupsData(groupNames, next);
},
members: function (next) {
Groups.getMemberUsers(groupNames, 0, 3, next);
},
}, next);
},
function (data, next) {
data.groups.forEach(function (group, index) {
if (!group) {
return;
if (group) {
group.members = data.members[index] || [];
group.truncated = group.memberCount > data.members.length;
}
group.members = data.members[index] || [];
group.truncated = group.memberCount > data.members.length;
});
next(null, data.groups);
},
], callback);
};
Groups.get = function (groupName, options, callback) {
if (!groupName) {
return callback(new Error('[[error:invalid-group]]'));
}
var stop = -1;
var results;
async.waterfall([
function (next) {
async.parallel({
base: function (next) {
db.getObject('group:' + groupName, next);
},
members: function (next) {
if (options.truncateUserList) {
stop = (parseInt(options.userListCount, 10) || 4) - 1;
}
callback(null, data.groups);
});
};
Groups.get = function (groupName, options, callback) {
if (!groupName) {
return callback(new Error('[[error:invalid-group]]'));
}
var stop = -1;
async.parallel({
base: function (next) {
db.getObject('group:' + groupName, next);
},
members: function (next) {
if (options.truncateUserList) {
stop = (parseInt(options.userListCount, 10) || 4) - 1;
}
Groups.getOwnersAndMembers(groupName, options.uid, 0, stop, next);
},
pending: function (next) {
async.waterfall([
function (next) {
db.getSetMembers('group:' + groupName + ':pending', next);
},
function (uids, next) {
user.getUsersData(uids, next);
},
], next);
},
invited: function (next) {
async.waterfall([
function (next) {
db.getSetMembers('group:' + groupName + ':invited', next);
},
function (uids, next) {
user.getUsersData(uids, next);
},
], next);
},
isMember: async.apply(Groups.isMember, options.uid, groupName),
isPending: async.apply(Groups.isPending, options.uid, groupName),
isInvited: async.apply(Groups.isInvited, options.uid, groupName),
isOwner: async.apply(Groups.ownership.isOwner, options.uid, groupName),
}, function (err, results) {
if (err) {
return callback(err);
} else if (!results.base) {
Groups.getOwnersAndMembers(groupName, options.uid, 0, stop, next);
},
pending: function (next) {
Groups.getUsersFromSet('group:' + groupName + ':pending', next);
},
invited: function (next) {
Groups.getUsersFromSet('group:' + groupName + ':invited', next);
},
isMember: async.apply(Groups.isMember, options.uid, groupName),
isPending: async.apply(Groups.isPending, options.uid, groupName),
isInvited: async.apply(Groups.isInvited, options.uid, groupName),
isOwner: async.apply(Groups.ownership.isOwner, options.uid, groupName),
}, next);
},
function (_results, next) {
results = _results;
if (!results.base) {
return callback(new Error('[[error:no-group]]'));
}
results.base['cover:url'] = results.base['cover:url'] || require('./coverPhoto').getDefaultGroupCover(groupName);
results.base['cover:position'] = validator.escape(String(results.base['cover:position'] || '50% 50%'));
results.base.labelColor = validator.escape(String(results.base.labelColor || '#000000'));
results.base.icon = validator.escape(String(results.base.icon || ''));
plugins.fireHook('filter:parse.raw', results.base.description, function (err, descriptionParsed) {
if (err) {
return callback(err);
}
Groups.escapeGroupData(results.base);
results.base.descriptionParsed = descriptionParsed;
results.base.userTitleEnabled = results.base.userTitleEnabled ? !!parseInt(results.base.userTitleEnabled, 10) : true;
results.base.createtimeISO = utils.toISOString(results.base.createtime);
results.base.members = results.members;
results.base.membersNextStart = stop + 1;
results.base.pending = results.pending.filter(Boolean);
results.base.invited = results.invited.filter(Boolean);
results.base.deleted = !!parseInt(results.base.deleted, 10);
results.base.hidden = !!parseInt(results.base.hidden, 10);
results.base.system = !!parseInt(results.base.system, 10);
results.base.memberCount = parseInt(results.base.memberCount, 10);
results.base.private = (results.base.private === null || results.base.private === undefined) ? true : !!parseInt(results.base.private, 10);
results.base.disableJoinRequests = parseInt(results.base.disableJoinRequests, 10) === 1;
results.base.isMember = results.isMember;
results.base.isPending = results.isPending;
results.base.isInvited = results.isInvited;
results.base.isOwner = results.isOwner;
plugins.fireHook('filter:group.get', { group: results.base }, function (err, data) {
callback(err, data ? data.group : null);
});
});
});
};
Groups.getOwners = function (groupName, callback) {
db.getSetMembers('group:' + groupName + ':owners', callback);
};
Groups.getOwnersAndMembers = function (groupName, uid, start, stop, callback) {
async.parallel({
owners: function (next) {
async.waterfall([
function (next) {
db.getSetMembers('group:' + groupName + ':owners', next);
},
function (uids, next) {
user.getUsers(uids, uid, next);
},
], next);
},
members: function (next) {
user.getUsersFromSet('group:' + groupName + ':members', uid, start, stop, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
plugins.fireHook('filter:parse.raw', results.base.description, next);
},
function (descriptionParsed, next) {
var groupData = results.base;
Groups.escapeGroupData(groupData);
groupData.descriptionParsed = descriptionParsed;
groupData.userTitleEnabled = groupData.userTitleEnabled ? !!parseInt(groupData.userTitleEnabled, 10) : true;
groupData.createtimeISO = utils.toISOString(groupData.createtime);
groupData.members = results.members;
groupData.membersNextStart = stop + 1;
groupData.pending = results.pending.filter(Boolean);
groupData.invited = results.invited.filter(Boolean);
groupData.deleted = !!parseInt(groupData.deleted, 10);
groupData.hidden = !!parseInt(groupData.hidden, 10);
groupData.system = !!parseInt(groupData.system, 10);
groupData.memberCount = parseInt(groupData.memberCount, 10);
groupData.private = (groupData.private === null || groupData.private === undefined) ? true : !!parseInt(groupData.private, 10);
groupData.disableJoinRequests = parseInt(groupData.disableJoinRequests, 10) === 1;
groupData.isMember = results.isMember;
groupData.isPending = results.isPending;
groupData.isInvited = results.isInvited;
groupData.isOwner = results.isOwner;
groupData['cover:url'] = groupData['cover:url'] || require('./coverPhoto').getDefaultGroupCover(groupName);
groupData['cover:position'] = validator.escape(String(groupData['cover:position'] || '50% 50%'));
groupData.labelColor = validator.escape(String(groupData.labelColor || '#000000'));
groupData.icon = validator.escape(String(groupData.icon || ''));
plugins.fireHook('filter:group.get', { group: groupData }, next);
},
function (results, next) {
next(null, results.group);
},
], callback);
};
Groups.getOwners = function (groupName, callback) {
db.getSetMembers('group:' + groupName + ':owners', callback);
};
Groups.getOwnersAndMembers = function (groupName, uid, start, stop, callback) {
async.waterfall([
function (next) {
async.parallel({
owners: function (next) {
async.waterfall([
function (next) {
db.getSetMembers('group:' + groupName + ':owners', next);
},
function (uids, next) {
user.getUsers(uids, uid, next);
},
], next);
},
members: function (next) {
user.getUsersFromSet('group:' + groupName + ':members', uid, start, stop, next);
},
}, next);
},
function (results, next) {
var ownerUids = [];
results.owners.forEach(function (user) {
if (user) {
@ -228,225 +210,94 @@ var utils = require('../public/src/utils');
});
results.members = results.owners.concat(results.members);
callback(null, results.members);
});
};
Groups.escapeGroupData = function (group) {
if (group) {
group.nameEncoded = encodeURIComponent(group.name);
group.displayName = validator.escape(String(group.name));
group.description = validator.escape(String(group.description || ''));
group.userTitle = validator.escape(String(group.userTitle || '')) || group.displayName;
}
};
Groups.getByGroupslug = function (slug, options, callback) {
db.getObjectField('groupslug:groupname', slug, function (err, groupName) {
if (err) {
return callback(err);
} else if (!groupName) {
return callback(new Error('[[error:no-group]]'));
next(null, results.members);
},
], callback);
};
Groups.escapeGroupData = function (group) {
if (group) {
group.nameEncoded = encodeURIComponent(group.name);
group.displayName = validator.escape(String(group.name));
group.description = validator.escape(String(group.description || ''));
group.userTitle = validator.escape(String(group.userTitle || '')) || group.displayName;
}
};
Groups.getByGroupslug = function (slug, options, callback) {
async.waterfall([
function (next) {
db.getObjectField('groupslug:groupname', slug, next);
},
function (groupName, next) {
if (!groupName) {
return next(new Error('[[error:no-group]]'));
}
Groups.get(groupName, options, next);
},
], callback);
};
Groups.get(groupName, options, callback);
});
};
Groups.getGroupNameByGroupSlug = function (slug, callback) {
db.getObjectField('groupslug:groupname', slug, callback);
};
Groups.getGroupFields = function (groupName, fields, callback) {
Groups.getMultipleGroupFields([groupName], fields, function (err, groups) {
callback(err, groups ? groups[0] : null);
});
};
Groups.getGroupNameByGroupSlug = function (slug, callback) {
db.getObjectField('groupslug:groupname', slug, callback);
};
Groups.getMultipleGroupFields = function (groups, fields, callback) {
db.getObjectsFields(groups.map(function (group) {
return 'group:' + group;
}), fields, callback);
};
Groups.isPrivate = function (groupName, callback) {
isFieldOn(groupName, 'private', callback);
};
Groups.setGroupField = function (groupName, field, value, callback) {
db.setObjectField('group:' + groupName, field, value, function (err) {
if (err) {
return callback(err);
}
plugins.fireHook('action:group.set', { field: field, value: value, type: 'set' });
callback();
});
};
Groups.isHidden = function (groupName, callback) {
isFieldOn(groupName, 'hidden', callback);
};
Groups.isPrivate = function (groupName, callback) {
db.getObjectField('group:' + groupName, 'private', function (err, isPrivate) {
if (err) {
return callback(err);
}
callback(null, parseInt(isPrivate, 10) !== 0);
});
};
Groups.isHidden = function (groupName, callback) {
db.getObjectField('group:' + groupName, 'hidden', function (err, isHidden) {
if (err) {
return callback(err);
}
function isFieldOn(groupName, field, callback) {
async.waterfall([
function (next) {
db.getObjectField('group:' + groupName, field, next);
},
function (value, next) {
next(null, parseInt(value, 10) === 1);
},
], callback);
}
callback(null, parseInt(isHidden, 10) === 1);
Groups.exists = function (name, callback) {
if (Array.isArray(name)) {
var slugs = name.map(function (groupName) {
return utils.slugify(groupName);
});
};
Groups.exists = function (name, callback) {
if (Array.isArray(name)) {
var slugs = name.map(function (groupName) {
return utils.slugify(groupName);
});
async.parallel([
function (next) {
next(null, slugs.map(function (slug) {
return ephemeralGroups.indexOf(slug) !== -1;
}));
},
async.apply(db.isSortedSetMembers, 'groups:createtime', name),
], function (err, results) {
if (err) {
return callback(err);
}
callback(null, name.map(function (n, index) {
return results[0][index] || results[1][index];
}));
});
} else {
var slug = utils.slugify(name);
async.parallel([
function (next) {
next(null, ephemeralGroups.indexOf(slug) !== -1);
},
async.apply(db.isSortedSetMember, 'groups:createtime', name),
], function (err, results) {
callback(err, !err ? (results[0] || results[1]) : null);
});
}
};
Groups.existsBySlug = function (slug, callback) {
if (Array.isArray(slug)) {
db.isObjectFields('groupslug:groupname', slug, callback);
} else {
db.isObjectField('groupslug:groupname', slug, callback);
}
};
Groups.getLatestMemberPosts = function (groupName, max, uid, callback) {
async.waterfall([
async.parallel([
function (next) {
Groups.getMembers(groupName, 0, -1, next);
},
function (uids, next) {
if (!Array.isArray(uids) || !uids.length) {
return callback(null, []);
}
var keys = uids.map(function (uid) {
return 'uid:' + uid + ':posts';
});
db.getSortedSetRevRange(keys, 0, max - 1, next);
},
function (pids, next) {
privileges.posts.filter('read', pids, uid, next);
},
function (pids, next) {
posts.getPostSummaryByPids(pids, uid, { stripTags: false }, next);
next(null, slugs.map(function (slug) {
return Groups.ephemeralGroups.indexOf(slug) !== -1;
}));
},
], callback);
};
Groups.getGroupData = function (groupName, callback) {
Groups.getGroupsData([groupName], function (err, groupsData) {
callback(err, Array.isArray(groupsData) && groupsData[0] ? groupsData[0] : null);
});
};
Groups.getGroupsData = function (groupNames, callback) {
if (!Array.isArray(groupNames) || !groupNames.length) {
return callback(null, []);
}
var keys = groupNames.map(function (groupName) {
return 'group:' + groupName;
});
var ephemeralIdx = groupNames.reduce(function (memo, cur, idx) {
if (ephemeralGroups.indexOf(cur) !== -1) {
memo.push(idx);
}
return memo;
}, []);
db.getObjects(keys, function (err, groupData) {
async.apply(db.isSortedSetMembers, 'groups:createtime', name),
], function (err, results) {
if (err) {
return callback(err);
}
if (ephemeralIdx.length) {
ephemeralIdx.forEach(function (idx) {
groupData[idx] = internals.getEphemeralGroup(groupNames[idx]);
});
}
groupData.forEach(function (group) {
if (group) {
Groups.escapeGroupData(group);
group.userTitleEnabled = group.userTitleEnabled ? parseInt(group.userTitleEnabled, 10) === 1 : true;
group.labelColor = validator.escape(String(group.labelColor || '#000000'));
group.icon = validator.escape(String(group.icon || ''));
group.createtimeISO = utils.toISOString(group.createtime);
group.hidden = parseInt(group.hidden, 10) === 1;
group.system = parseInt(group.system, 10) === 1;
group.private = (group.private === null || group.private === undefined) ? true : !!parseInt(group.private, 10);
group.disableJoinRequests = parseInt(group.disableJoinRequests, 10) === 1;
group['cover:url'] = group['cover:url'] || require('./coverPhoto').getDefaultGroupCover(group.name);
group['cover:thumb:url'] = group['cover:thumb:url'] || group['cover:url'];
group['cover:position'] = validator.escape(String(group['cover:position'] || '50% 50%'));
}
});
plugins.fireHook('filter:groups.get', { groups: groupData }, function (err, data) {
callback(err, data ? data.groups : null);
});
callback(null, name.map(function (n, index) {
return results[0][index] || results[1][index];
}));
});
};
Groups.getUserGroups = function (uids, callback) {
Groups.getUserGroupsFromSet('groups:visible:createtime', uids, callback);
};
Groups.getUserGroupsFromSet = function (set, uids, callback) {
async.waterfall([
} else {
var slug = utils.slugify(name);
async.parallel([
function (next) {
db.getSortedSetRevRange(set, 0, -1, next);
},
function (groupNames, next) {
async.map(uids, function (uid, next) {
Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) {
if (err) {
return next(err);
}
var memberOf = [];
isMembers.forEach(function (isMember, index) {
if (isMember) {
memberOf.push(groupNames[index]);
}
});
Groups.getGroupsData(memberOf, next);
});
}, next);
next(null, Groups.ephemeralGroups.indexOf(slug) !== -1);
},
], callback);
};
}(module.exports));
async.apply(db.isSortedSetMember, 'groups:createtime', name),
], function (err, results) {
callback(err, !err ? (results[0] || results[1]) : null);
});
}
};
Groups.existsBySlug = function (slug, callback) {
if (Array.isArray(slug)) {
db.isObjectFields('groupslug:groupname', slug, callback);
} else {
db.isObjectField('groupslug:groupname', slug, callback);
}
};

@ -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);
if (groupNames.length === 0) {
return callback(null, false);
}
Groups.isMemberOfGroups(uid, groupNames, function (err, isMembers) {
if (err) {
return callback(err);
async.waterfall([
function (next) {
db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, next);
},
function (groupNames, next) {
groupNames = Groups.removeEphemeralGroups(groupNames);
if (groupNames.length === 0) {
return callback(null, false);
}
callback(null, isMembers.indexOf(true) !== -1);
});
});
Groups.isMemberOfGroups(uid, groupNames, next);
},
function (isMembers, next) {
next(null, isMembers.indexOf(true) !== -1);
},
], callback);
};
Groups.isMemberOfGroupsList = function (uid, groupListKeys, callback) {
@ -446,19 +447,20 @@ module.exports = function (Groups) {
return 'group:' + groupName + ':members';
});
db.getSortedSetsMembers(sets, function (err, members) {
if (err) {
return callback(err);
}
var uniqueGroups = _.unique(_.flatten(members));
uniqueGroups = Groups.internals.removeEphemeralGroups(uniqueGroups);
Groups.isMemberOfGroups(uid, uniqueGroups, function (err, isMembers) {
if (err) {
return callback(err);
}
var uniqueGroups;
var members;
async.waterfall([
function (next) {
db.getSortedSetsMembers(sets, next);
},
function (_members, next) {
members = _members;
uniqueGroups = _.unique(_.flatten(members));
uniqueGroups = Groups.removeEphemeralGroups(uniqueGroups);
Groups.isMemberOfGroups(uid, uniqueGroups, next);
},
function (isMembers, next) {
var map = {};
uniqueGroups.forEach(function (groupName, index) {
@ -474,62 +476,63 @@ module.exports = function (Groups) {
return false;
});
callback(null, result);
});
});
next(null, result);
},
], callback);
};
Groups.isMembersOfGroupList = function (uids, groupListKey, callback) {
db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function (err, groupNames) {
if (err) {
return callback(err);
}
var groupNames;
var results = [];
uids.forEach(function () {
results.push(false);
});
var results = [];
uids.forEach(function () {
results.push(false);
});
async.waterfall([
function (next) {
db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, next);
},
function (_groupNames, next) {
groupNames = Groups.removeEphemeralGroups(_groupNames);
groupNames = Groups.internals.removeEphemeralGroups(groupNames);
if (groupNames.length === 0) {
return callback(null, results);
}
if (groupNames.length === 0) {
return callback(null, results);
}
async.each(groupNames, function (groupName, next) {
Groups.isMembers(uids, groupName, function (err, isMembers) {
if (err) {
return next(err);
}
async.map(groupNames, function (groupName, next) {
Groups.isMembers(uids, groupName, next);
}, next);
},
function (isGroupMembers, next) {
isGroupMembers.forEach(function (isMembers) {
results.forEach(function (isMember, index) {
if (!isMember && isMembers[index]) {
results[index] = true;
}
});
next();
});
}, function (err) {
callback(err, results);
});
});
next(null, results);
},
], callback);
};
Groups.isInvited = function (uid, groupName, callback) {
if (!uid) {
return callback(null, false);
return setImmediate(callback, null, false);
}
db.isSetMember('group:' + groupName + ':invited', uid, callback);
};
Groups.isPending = function (uid, groupName, callback) {
if (!uid) {
return callback(null, false);
return setImmediate(callback, null, false);
}
db.isSetMember('group:' + groupName + ':pending', uid, callback);
};
Groups.getPending = function (groupName, callback) {
if (!groupName) {
return callback(null, []);
return setImmediate(callback, null, []);
}
db.getSetMembers('group:' + groupName + ':pending', callback);
};

@ -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.parallel([
async.apply(db.sortedSetAdd, 'groups:visible:createtime', groupData.createtime, groupName),
async.apply(db.sortedSetAdd, 'groups:visible:memberCount', groupData.memberCount, groupName),
async.apply(db.sortedSetAdd, 'groups:visible:name', 0, groupName.toLowerCase() + ':' + groupName),
], callback);
});
async.waterfall([
function (next) {
db.getObjectFields('group:' + groupName, ['createtime', 'memberCount'], next);
},
function (groupData, next) {
async.parallel([
async.apply(db.sortedSetAdd, 'groups:visible:createtime', groupData.createtime, groupName),
async.apply(db.sortedSetAdd, 'groups:visible:memberCount', groupData.memberCount, groupName),
async.apply(db.sortedSetAdd, 'groups:visible:name', 0, groupName.toLowerCase() + ':' + groupName),
], next);
},
], callback);
}
}
@ -155,40 +157,48 @@ module.exports = function (Groups) {
function checkNameChange(currentName, newName, callback) {
if (currentName === newName) {
return callback();
return setImmediate(callback);
}
var currentSlug = utils.slugify(currentName);
var newSlug = utils.slugify(newName);
if (currentSlug === newSlug) {
return callback();
return setImmediate(callback);
}
Groups.existsBySlug(newSlug, function (err, exists) {
if (err || exists) {
return callback(err || new Error('[[error:group-already-exists]]'));
}
callback();
});
async.waterfall([
function (next) {
Groups.existsBySlug(newSlug, next);
},
function (exists, next) {
next(exists ? new Error('[[error:group-already-exists]]') : null);
},
], callback);
}
function renameGroup(oldName, newName, callback) {
if (oldName === newName || !newName || newName.length === 0) {
return callback();
return setImmediate(callback);
}
var group;
async.waterfall([
function (next) {
db.getObject('group:' + oldName, next);
},
function (_group, next) {
group = _group;
if (!group) {
return callback();
}
db.getObject('group:' + oldName, function (err, group) {
if (err || !group) {
return callback(err);
}
if (parseInt(group.system, 10) === 1) {
return callback();
}
Groups.exists(newName, function (err, exists) {
if (err || exists) {
return callback(err || new Error('[[error:group-already-exists]]'));
if (parseInt(group.system, 10) === 1) {
return callback(new Error('[[error:not-allowed-to-rename-system-group]]'));
}
Groups.exists(newName, next);
},
function (exists, next) {
if (exists) {
return callback(new Error('[[error:group-already-exists]]'));
}
async.series([
async.apply(db.setObjectField, 'group:' + oldName, 'name', newName),
async.apply(db.setObjectField, 'group:' + oldName, 'slug', utils.slugify(newName)),
@ -222,29 +232,33 @@ module.exports = function (Groups) {
next();
},
], callback);
});
], next);
},
], function (err) {
callback(err);
});
}
function renameGroupMember(group, oldName, newName, callback) {
db.isSortedSetMember(group, oldName, function (err, isMember) {
if (err || !isMember) {
return callback(err);
}
var score;
async.waterfall([
function (next) {
db.sortedSetScore(group, oldName, next);
},
function (_score, next) {
score = _score;
db.sortedSetRemove(group, oldName, next);
},
function (next) {
db.sortedSetAdd(group, score, newName, next);
},
], callback);
});
var score;
async.waterfall([
function (next) {
db.isSortedSetMember(group, oldName, next);
},
function (isMember, next) {
if (!isMember) {
return callback();
}
db.sortedSetScore(group, oldName, next);
},
function (_score, next) {
score = _score;
db.sortedSetRemove(group, oldName, next);
},
function (next) {
db.sortedSetAdd(group, score, newName, next);
},
], callback);
}
};

@ -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);
}
data.postData.content = translator.escape(data.postData.content);
async.waterfall([
function (next) {
plugins.fireHook('filter:parse.post', { postData: postData }, next);
},
function (data, next) {
data.postData.content = translator.escape(data.postData.content);
if (global.env === 'production' && data.postData.pid) {
cache.set(String(data.postData.pid), data.postData.content);
}
callback(null, data.postData);
});
if (global.env === 'production' && data.postData.pid) {
cache.set(String(data.postData.pid), data.postData.content);
}
next(null, data.postData);
},
], callback);
};
Posts.parseSignature = function (userData, uid, callback) {
@ -51,7 +47,6 @@ module.exports = function (Posts) {
var parsed;
var current = urlRegex.exec(content);
var absolute;
while (current !== null) {
if (current[1]) {
try {
@ -78,7 +73,7 @@ module.exports = function (Posts) {
};
function sanitizeSignature(signature) {
var string = S(signature);
var string = S(signature);
var tagsToStrip = [];
if (parseInt(meta.config['signatures:disableLinks'], 10) === 1) {

@ -96,7 +96,7 @@ module.exports = function (privileges) {
return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1;
});
groupNames = groups.getEphemeralGroups().concat(groupNames);
groupNames = groups.ephemeralGroups.concat(groupNames);
var registeredUsersIndex = groupNames.indexOf('registered-users');
if (registeredUsersIndex !== -1) {
groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]);
@ -155,34 +155,36 @@ module.exports = function (privileges) {
privileges.categories.get = function (cid, uid, callback) {
var privs = ['topics:create', 'topics:read', 'read'];
async.parallel({
privileges: function (next) {
helpers.isUserAllowedTo(privs, uid, cid, next);
},
isAdministrator: function (next) {
user.isAdministrator(uid, next);
async.waterfall([
function (next) {
async.parallel({
privileges: function (next) {
helpers.isUserAllowedTo(privs, uid, cid, next);
},
isAdministrator: function (next) {
user.isAdministrator(uid, next);
},
isModerator: function (next) {
user.isModerator(uid, cid, next);
},
}, next);
},
isModerator: function (next) {
user.isModerator(uid, cid, next);
function (results, next) {
var privData = _.object(privs, results.privileges);
var isAdminOrMod = results.isAdministrator || results.isModerator;
plugins.fireHook('filter:privileges.categories.get', {
'topics:create': privData['topics:create'] || isAdminOrMod,
'topics:read': privData['topics:read'] || isAdminOrMod,
read: privData.read || isAdminOrMod,
cid: cid,
uid: uid,
editable: isAdminOrMod,
view_deleted: isAdminOrMod,
isAdminOrMod: isAdminOrMod,
}, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
var privData = _.object(privs, results.privileges);
var isAdminOrMod = results.isAdministrator || results.isModerator;
plugins.fireHook('filter:privileges.categories.get', {
'topics:create': privData['topics:create'] || isAdminOrMod,
'topics:read': privData['topics:read'] || isAdminOrMod,
read: privData.read || isAdminOrMod,
cid: cid,
uid: uid,
editable: isAdminOrMod,
view_deleted: isAdminOrMod,
isAdminOrMod: isAdminOrMod,
}, callback);
});
], callback);
};
privileges.categories.isAdminOrMod = function (cid, uid, callback) {
@ -213,29 +215,29 @@ module.exports = function (privileges) {
return callback(null, false);
}
categories.getCategoryField(cid, 'disabled', function (err, disabled) {
if (err) {
return callback(err);
}
if (parseInt(disabled, 10) === 1) {
return callback(null, false);
}
helpers.some([
function (next) {
helpers.isUserAllowedTo(privilege, uid, [cid], function (err, results) {
next(err, Array.isArray(results) && results.length ? results[0] : false);
});
},
function (next) {
user.isModerator(uid, cid, next);
},
function (next) {
user.isAdministrator(uid, next);
},
], callback);
});
async.waterfall([
function (next) {
categories.getCategoryField(cid, 'disabled', next);
},
function (disabled, next) {
if (parseInt(disabled, 10) === 1) {
return callback(null, false);
}
helpers.some([
function (next) {
helpers.isUserAllowedTo(privilege, uid, [cid], function (err, results) {
next(err, Array.isArray(results) && results.length ? results[0] : false);
});
},
function (next) {
user.isModerator(uid, cid, next);
},
function (next) {
user.isAdministrator(uid, next);
},
], next);
},
], callback);
};
privileges.categories.filterCids = function (privilege, cids, uid, callback) {
@ -247,18 +249,19 @@ module.exports = function (privileges) {
return array.indexOf(cid) === index;
});
privileges.categories.getBase(privilege, cids, uid, function (err, results) {
if (err) {
return callback(err);
}
cids = cids.filter(function (cid, index) {
return !results.categories[index].disabled &&
(results.allowedTo[index] || results.isAdmin || results.isModerators[index]);
});
async.waterfall([
function (next) {
privileges.categories.getBase(privilege, cids, uid, next);
},
function (results, next) {
cids = cids.filter(function (cid, index) {
return !results.categories[index].disabled &&
(results.allowedTo[index] || results.isAdmin || results.isModerators[index]);
});
callback(null, cids.filter(Boolean));
});
next(null, cids.filter(Boolean));
},
], callback);
};
privileges.categories.getBase = function (privilege, cids, uid, callback) {
@ -287,26 +290,27 @@ module.exports = function (privileges) {
return array.indexOf(uid) === index;
});
async.parallel({
allowedTo: function (next) {
helpers.isUsersAllowedTo(privilege, uids, cid, next);
},
isModerators: function (next) {
user.isModerator(uids, cid, next);
async.waterfall([
function (next) {
async.parallel({
allowedTo: function (next) {
helpers.isUsersAllowedTo(privilege, uids, cid, next);
},
isModerators: function (next) {
user.isModerator(uids, cid, next);
},
isAdmin: function (next) {
user.isAdministrator(uids, next);
},
}, next);
},
isAdmin: function (next) {
user.isAdministrator(uids, next);
function (results, next) {
uids = uids.filter(function (uid, index) {
return results.allowedTo[index] || results.isModerators[index] || results.isAdmin[index];
});
next(null, uids);
},
}, function (err, results) {
if (err) {
return callback(err);
}
uids = uids.filter(function (uid, index) {
return results.allowedTo[index] || results.isModerators[index] || results.isAdmin[index];
});
callback(null, uids);
});
], callback);
};
privileges.categories.give = function (privileges, cid, groupName, callback) {
@ -324,23 +328,24 @@ module.exports = function (privileges) {
}
privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) {
async.parallel({
isAdministrator: function (next) {
user.isAdministrator(uid, next);
},
moderatorOfCurrent: function (next) {
user.isModerator(uid, currentCid, next);
async.waterfall([
function (next) {
async.parallel({
isAdministrator: function (next) {
user.isAdministrator(uid, next);
},
moderatorOfCurrent: function (next) {
user.isModerator(uid, currentCid, next);
},
moderatorOfTarget: function (next) {
user.isModerator(uid, targetCid, next);
},
}, next);
},
moderatorOfTarget: function (next) {
user.isModerator(uid, targetCid, next);
function (results, next) {
next(null, results.isAdministrator || (results.moderatorOfCurrent && results.moderatorOfTarget));
},
}, function (err, results) {
if (err) {
return callback(err);
}
callback(null, results.isAdministrator || (results.moderatorOfCurrent && results.moderatorOfTarget));
});
], callback);
};
privileges.categories.userPrivileges = function (cid, uid, callback) {

@ -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);
socket.emit('event:new_post', { posts: [result.postData] });
socket.emit('event:new_topic', result.topicData);
socketHelpers.notifyNew(socket.uid, 'newTopic', { posts: [result.postData], topic: result.topicData });
});
socketHelpers.notifyNew(socket.uid, 'newTopic', { posts: [result.postData], topic: result.topicData });
},
], callback);
};
SocketTopics.postcount = function (socket, tid, callback) {
@ -61,7 +64,7 @@ SocketTopics.createTopicFromPosts = function (socket, data, callback) {
};
SocketTopics.changeWatching = function (socket, data, callback) {
if (!data.tid || !data.type) {
if (!data || !data.tid || !data.type) {
return callback(new Error('[[error:invalid-data]]'));
}
var commands = ['follow', 'unfollow', 'ignore'];
@ -90,20 +93,23 @@ SocketTopics.isFollowed = function (socket, tid, callback) {
};
SocketTopics.search = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
topics.search(data.tid, data.term, callback);
};
SocketTopics.isModerator = function (socket, tid, callback) {
topics.getTopicField(tid, 'cid', function (err, cid) {
if (err) {
return callback(err);
}
user.isModerator(socket.uid, cid, callback);
});
async.waterfall([
function (next) {
topics.getTopicField(tid, 'cid', next);
},
function (cid, next) {
user.isModerator(socket.uid, cid, next);
},
], callback);
};
SocketTopics.getTopic = function (socket, tid, callback) {
apiController.getTopicData(tid, socket.uid, callback);
};
module.exports = SocketTopics;

@ -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,388 +6,334 @@ var _ = require('underscore');
var groups = require('./groups');
var plugins = require('./plugins');
var db = require('./database');
var topics = require('./topics');
var privileges = require('./privileges');
var meta = require('./meta');
(function (User) {
User.email = require('./user/email');
User.notifications = require('./user/notifications');
User.reset = require('./user/reset');
User.digest = require('./user/digest');
require('./user/data')(User);
require('./user/auth')(User);
require('./user/bans')(User);
require('./user/create')(User);
require('./user/posts')(User);
require('./user/topics')(User);
require('./user/categories')(User);
require('./user/follow')(User);
require('./user/profile')(User);
require('./user/admin')(User);
require('./user/delete')(User);
require('./user/settings')(User);
require('./user/search')(User);
require('./user/jobs')(User);
require('./user/picture')(User);
require('./user/approval')(User);
require('./user/invite')(User);
require('./user/password')(User);
require('./user/info')(User);
User.updateLastOnlineTime = function (uid, callback) {
callback = callback || function () {};
db.getObjectFields('user:' + uid, ['status', 'lastonline'], function (err, userData) {
var now = Date.now();
if (err || userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) {
return callback(err);
}
User.setUserField(uid, 'lastonline', now, callback);
});
};
User.updateOnlineUsers = function (uid, callback) {
callback = callback || function () {};
var User = module.exports;
User.email = require('./user/email');
User.notifications = require('./user/notifications');
User.reset = require('./user/reset');
User.digest = require('./user/digest');
require('./user/data')(User);
require('./user/auth')(User);
require('./user/bans')(User);
require('./user/create')(User);
require('./user/posts')(User);
require('./user/topics')(User);
require('./user/categories')(User);
require('./user/follow')(User);
require('./user/profile')(User);
require('./user/admin')(User);
require('./user/delete')(User);
require('./user/settings')(User);
require('./user/search')(User);
require('./user/jobs')(User);
require('./user/picture')(User);
require('./user/approval')(User);
require('./user/invite')(User);
require('./user/password')(User);
require('./user/info')(User);
require('./user/online')(User);
User.getUidsFromSet = function (set, start, stop, callback) {
if (set === 'users:online') {
var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1;
var now = Date.now();
async.waterfall([
function (next) {
db.sortedSetScore('users:online', uid, next);
},
function (userOnlineTime, next) {
if (now - parseInt(userOnlineTime, 10) < 300000) {
return callback();
}
db.sortedSetAdd('users:online', now, uid, next);
},
function (next) {
topics.pushUnreadCount(uid);
plugins.fireHook('action:user.online', { uid: uid, timestamp: now });
next();
},
], callback);
};
User.getUidsFromSet = function (set, start, stop, callback) {
if (set === 'users:online') {
var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1;
var now = Date.now();
db.getSortedSetRevRangeByScore(set, start, count, '+inf', now - 300000, callback);
} else {
db.getSortedSetRevRange(set, start, stop, callback);
}
};
User.getUsersFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
User.getUidsFromSet(set, start, stop, next);
},
function (uids, next) {
User.getUsers(uids, uid, next);
},
], callback);
};
User.getUsersWithFields = function (uids, fields, uid, callback) {
async.waterfall([
function (next) {
plugins.fireHook('filter:users.addFields', { fields: fields }, next);
},
function (data, next) {
data.fields = data.fields.filter(function (field, index, array) {
return array.indexOf(field) === index;
});
async.parallel({
userData: function (next) {
User.getUsersFields(uids, data.fields, next);
},
isAdmin: function (next) {
User.isAdministrator(uids, next);
},
}, next);
},
function (results, next) {
results.userData.forEach(function (user, index) {
if (user) {
user.status = User.getStatus(user);
user.administrator = results.isAdmin[index];
user.banned = parseInt(user.banned, 10) === 1;
user.banned_until = parseInt(user['banned:expire'], 10) || 0;
user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned';
user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1;
}
});
plugins.fireHook('filter:userlist.get', { users: results.userData, uid: uid }, next);
},
function (data, next) {
next(null, data.users);
},
], callback);
};
User.getUsers = function (uids, uid, callback) {
var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'flags',
'banned', 'banned:expire', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline'];
User.getUsersWithFields(uids, fields, uid, callback);
};
User.getStatus = function (userData) {
var isOnline = (Date.now() - parseInt(userData.lastonline, 10)) < 300000;
return isOnline ? (userData.status || 'online') : 'offline';
};
User.isOnline = function (uid, callback) {
if (Array.isArray(uid)) {
db.sortedSetScores('users:online', uid, function (err, lastonline) {
if (err) {
return callback(err);
}
var now = Date.now();
var isOnline = uid.map(function (uid, index) {
return now - lastonline[index] < 300000;
});
callback(null, isOnline);
db.getSortedSetRevRangeByScore(set, start, count, '+inf', now - 300000, callback);
} else {
db.getSortedSetRevRange(set, start, stop, callback);
}
};
User.getUsersFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
User.getUidsFromSet(set, start, stop, next);
},
function (uids, next) {
User.getUsers(uids, uid, next);
},
], callback);
};
User.getUsersWithFields = function (uids, fields, uid, callback) {
async.waterfall([
function (next) {
plugins.fireHook('filter:users.addFields', { fields: fields }, next);
},
function (data, next) {
data.fields = data.fields.filter(function (field, index, array) {
return array.indexOf(field) === index;
});
} else {
db.sortedSetScore('users:online', uid, function (err, lastonline) {
if (err) {
return callback(err);
async.parallel({
userData: function (next) {
User.getUsersFields(uids, data.fields, next);
},
isAdmin: function (next) {
User.isAdministrator(uids, next);
},
}, next);
},
function (results, next) {
results.userData.forEach(function (user, index) {
if (user) {
user.status = User.getStatus(user);
user.administrator = results.isAdmin[index];
user.banned = parseInt(user.banned, 10) === 1;
user.banned_until = parseInt(user['banned:expire'], 10) || 0;
user.banned_until_readable = user.banned_until ? new Date(user.banned_until).toString() : 'Not Banned';
user['email:confirmed'] = parseInt(user['email:confirmed'], 10) === 1;
}
var isOnline = Date.now() - parseInt(lastonline, 10) < 300000;
callback(null, isOnline);
});
}
};
User.exists = function (uid, callback) {
db.isSortedSetMember('users:joindate', uid, callback);
};
User.existsBySlug = function (userslug, callback) {
User.getUidByUserslug(userslug, function (err, exists) {
callback(err, !!exists);
});
};
User.getUidByUsername = function (username, callback) {
if (!username) {
return callback(null, 0);
}
db.sortedSetScore('username:uid', username, callback);
};
User.getUidsByUsernames = function (usernames, callback) {
db.sortedSetScores('username:uid', usernames, callback);
};
User.getUidByUserslug = function (userslug, callback) {
if (!userslug) {
return callback(null, 0);
}
db.sortedSetScore('userslug:uid', userslug, callback);
};
User.getUsernamesByUids = function (uids, callback) {
User.getUsersFields(uids, ['username'], function (err, users) {
if (err) {
return callback(err);
}
plugins.fireHook('filter:userlist.get', { users: results.userData, uid: uid }, next);
},
function (data, next) {
next(null, data.users);
},
], callback);
};
User.getUsers = function (uids, uid, callback) {
var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'flags',
'banned', 'banned:expire', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline'];
User.getUsersWithFields(uids, fields, uid, callback);
};
User.getStatus = function (userData) {
var isOnline = (Date.now() - parseInt(userData.lastonline, 10)) < 300000;
return isOnline ? (userData.status || 'online') : 'offline';
};
User.exists = function (uid, callback) {
db.isSortedSetMember('users:joindate', uid, callback);
};
User.existsBySlug = function (userslug, callback) {
User.getUidByUserslug(userslug, function (err, exists) {
callback(err, !!exists);
});
};
User.getUidByUsername = function (username, callback) {
if (!username) {
return callback(null, 0);
}
db.sortedSetScore('username:uid', username, callback);
};
User.getUidsByUsernames = function (usernames, callback) {
db.sortedSetScores('username:uid', usernames, callback);
};
User.getUidByUserslug = function (userslug, callback) {
if (!userslug) {
return callback(null, 0);
}
db.sortedSetScore('userslug:uid', userslug, callback);
};
User.getUsernamesByUids = function (uids, callback) {
async.waterfall([
function (next) {
User.getUsersFields(uids, ['username'], next);
},
function (users, next) {
users = users.map(function (user) {
return user.username;
});
callback(null, users);
});
};
User.getUsernameByUserslug = function (slug, callback) {
async.waterfall([
function (next) {
User.getUidByUserslug(slug, next);
},
function (uid, next) {
User.getUserField(uid, 'username', next);
},
], callback);
};
User.getUidByEmail = function (email, callback) {
db.sortedSetScore('email:uid', email.toLowerCase(), callback);
};
User.getUidsByEmails = function (emails, callback) {
emails = emails.map(function (email) {
return email && email.toLowerCase();
});
db.sortedSetScores('email:uid', emails, callback);
};
User.getUsernameByEmail = function (email, callback) {
db.sortedSetScore('email:uid', email.toLowerCase(), function (err, uid) {
if (err) {
return callback(err);
}
User.getUserField(uid, 'username', callback);
});
};
User.isModerator = function (uid, cid, callback) {
privileges.users.isModerator(uid, cid, callback);
};
User.isModeratorOfAnyCategory = function (uid, callback) {
User.getModeratedCids(uid, function (err, cids) {
callback(err, Array.isArray(cids) ? !!cids.length : false);
});
};
User.isAdministrator = function (uid, callback) {
privileges.users.isAdministrator(uid, callback);
};
User.isGlobalModerator = function (uid, callback) {
privileges.users.isGlobalModerator(uid, callback);
};
User.isPrivileged = function (uid, callback) {
async.parallel([
async.apply(User.isAdministrator, uid),
async.apply(User.isGlobalModerator, uid),
async.apply(User.isModeratorOfAnyCategory, uid),
], function (err, results) {
callback(err, results ? results.some(Boolean) : false);
});
};
User.isAdminOrGlobalMod = function (uid, callback) {
async.parallel({
isAdmin: async.apply(User.isAdministrator, uid),
isGlobalMod: async.apply(User.isGlobalModerator, uid),
}, function (err, results) {
callback(err, results ? (results.isAdmin || results.isGlobalMod) : false);
});
};
User.isAdminOrSelf = function (callerUid, uid, callback) {
if (parseInt(callerUid, 10) === parseInt(uid, 10)) {
return callback();
}
User.isAdministrator(callerUid, function (err, isAdmin) {
if (err || !isAdmin) {
return callback(err || new Error('[[error:no-privileges]]'));
}
callback();
});
};
User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) {
if (parseInt(callerUid, 10) === parseInt(uid, 10)) {
return callback();
}
User.isAdminOrGlobalMod(callerUid, function (err, isAdminOrGlobalMod) {
if (err || !isAdminOrGlobalMod) {
return callback(err || new Error('[[error:no-privileges]]'));
}
callback();
});
};
User.getAdminsandGlobalMods = function (callback) {
async.parallel({
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
mods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
}, function (err, results) {
if (err) {
return callback(err);
next(null, users);
},
], callback);
};
User.getUsernameByUserslug = function (slug, callback) {
async.waterfall([
function (next) {
User.getUidByUserslug(slug, next);
},
function (uid, next) {
User.getUserField(uid, 'username', next);
},
], callback);
};
User.getUidByEmail = function (email, callback) {
db.sortedSetScore('email:uid', email.toLowerCase(), callback);
};
User.getUidsByEmails = function (emails, callback) {
emails = emails.map(function (email) {
return email && email.toLowerCase();
});
db.sortedSetScores('email:uid', emails, callback);
};
User.getUsernameByEmail = function (email, callback) {
async.waterfall([
function (next) {
db.sortedSetScore('email:uid', email.toLowerCase(), next);
},
function (uid, next) {
User.getUserField(uid, 'username', next);
},
], callback);
};
User.isModerator = function (uid, cid, callback) {
privileges.users.isModerator(uid, cid, callback);
};
User.isModeratorOfAnyCategory = function (uid, callback) {
User.getModeratedCids(uid, function (err, cids) {
callback(err, Array.isArray(cids) ? !!cids.length : false);
});
};
User.isAdministrator = function (uid, callback) {
privileges.users.isAdministrator(uid, callback);
};
User.isGlobalModerator = function (uid, callback) {
privileges.users.isGlobalModerator(uid, callback);
};
User.isPrivileged = function (uid, callback) {
async.parallel([
async.apply(User.isAdministrator, uid),
async.apply(User.isGlobalModerator, uid),
async.apply(User.isModeratorOfAnyCategory, uid),
], function (err, results) {
callback(err, results ? results.some(Boolean) : false);
});
};
User.isAdminOrGlobalMod = function (uid, callback) {
async.parallel({
isAdmin: async.apply(User.isAdministrator, uid),
isGlobalMod: async.apply(User.isGlobalModerator, uid),
}, function (err, results) {
callback(err, results ? (results.isAdmin || results.isGlobalMod) : false);
});
};
User.isAdminOrSelf = function (callerUid, uid, callback) {
isSelfOrMethod(callerUid, uid, User.isAdministrator, callback);
};
User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) {
isSelfOrMethod(callerUid, uid, User.isAdminOrGlobalMod, callback);
};
function isSelfOrMethod(callerUid, uid, method, callback) {
if (parseInt(callerUid, 10) === parseInt(uid, 10)) {
return callback();
}
async.waterfall([
function (next) {
method(callerUid, next);
},
function (isPass, next) {
if (!isPass) {
return next(new Error('[[error:no-privileges]]'));
}
var uids = results.admins.concat(results.mods).filter(function (uid, index, array) {
return uid && array.indexOf(uid) === index;
next();
},
], callback);
}
User.getAdminsandGlobalMods = function (callback) {
async.waterfall([
function (next) {
async.parallel([
async.apply(groups.getMembers, 'administrators', 0, -1),
async.apply(groups.getMembers, 'Global Moderators', 0, -1),
], next);
},
function (results, next) {
User.getUsersData(_.union(results), next);
},
], callback);
};
User.getAdminsandGlobalModsandModerators = function (callback) {
async.waterfall([
function (next) {
async.parallel([
async.apply(groups.getMembers, 'administrators', 0, -1),
async.apply(groups.getMembers, 'Global Moderators', 0, -1),
async.apply(User.getModeratorUids),
], next);
},
function (results, next) {
User.getUsersData(_.union.apply(_, results), next);
},
], callback);
};
User.getModeratorUids = function (callback) {
async.waterfall([
async.apply(db.getSortedSetRange, 'categories:cid', 0, -1),
function (cids, next) {
var groupNames = cids.map(function (cid) {
return 'cid:' + cid + ':privileges:mods';
});
User.getUsersData(uids, callback);
});
};
User.getAdminsandGlobalModsandModerators = function (callback) {
async.parallel([
async.apply(groups.getMembers, 'administrators', 0, -1),
async.apply(groups.getMembers, 'Global Moderators', 0, -1),
async.apply(User.getModeratorUids),
], function (err, results) {
if (err) {
return callback(err);
}
User.getUsersData(_.union.apply(_, results), callback);
});
};
groups.getMembersOfGroups(groupNames, next);
},
function (memberSets, next) {
next(null, _.union.apply(_, memberSets));
},
], callback);
};
User.getModeratedCids = function (uid, callback) {
var cids;
async.waterfall([
function (next) {
db.getSortedSetRange('categories:cid', 0, -1, next);
},
function (_cids, next) {
cids = _cids;
User.isModerator(uid, cids, next);
},
function (isMods, next) {
cids = cids.filter(function (cid, index) {
return cid && isMods[index];
});
next(null, cids);
},
], callback);
};
User.addInterstitials = function (callback) {
plugins.registerHook('core', {
hook: 'filter:register.interstitial',
method: function (data, callback) {
if (meta.config.termsOfUse && !data.userData.acceptTos) {
data.interstitials.push({
template: 'partials/acceptTos',
data: {
termsOfUse: meta.config.termsOfUse,
},
callback: function (userData, formData, next) {
if (formData['agree-terms'] === 'on') {
userData.acceptTos = true;
}
User.getModeratorUids = function (callback) {
async.waterfall([
async.apply(db.getSortedSetRange, 'categories:cid', 0, -1),
function (cids, next) {
var groupNames = cids.map(function (cid) {
return 'cid:' + cid + ':privileges:mods';
next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]'));
},
});
}
groups.getMembersOfGroups(groupNames, function (err, memberSets) {
if (err) {
return next(err);
}
next(null, _.union.apply(_, memberSets));
});
},
], callback);
};
User.getModeratedCids = function (uid, callback) {
var cids;
async.waterfall([
function (next) {
db.getSortedSetRange('categories:cid', 0, -1, next);
},
function (_cids, next) {
cids = _cids;
User.isModerator(uid, cids, next);
},
function (isMods, next) {
cids = cids.filter(function (cid, index) {
return cid && isMods[index];
});
next(null, cids);
},
], callback);
};
User.addInterstitials = function (callback) {
plugins.registerHook('core', {
hook: 'filter:register.interstitial',
method: function (data, callback) {
if (meta.config.termsOfUse && !data.userData.acceptTos) {
data.interstitials.push({
template: 'partials/acceptTos',
data: {
termsOfUse: meta.config.termsOfUse,
},
callback: function (userData, formData, next) {
if (formData['agree-terms'] === 'on') {
userData.acceptTos = true;
}
next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]'));
},
});
}
callback(null, data);
},
});
callback(null, data);
},
});
callback();
};
callback();
};
}(exports));

@ -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,17 +64,24 @@ module.exports = function (User) {
addField('lastonline');
}
db.getObjectsFields(keys, fields, function (err, users) {
if (err) {
return callback(err);
}
users = uids.map(function (uid) {
return users[ref[uid]];
});
modifyUserData(users, fieldsToRemove, callback);
});
async.waterfall([
function (next) {
db.getObjectsFields(keys, fields, function (err, users) {
if (err) {
return callback(err);
}
users = uids.map(function (uid) {
return users[ref[uid]];
});
next(null, users);
});
},
function (users, next) {
modifyUserData(users, fieldsToRemove, next);
},
], callback);
};
User.getMultipleUserFields = function (uids, fields, callback) {
@ -105,17 +113,24 @@ module.exports = function (User) {
return 'user:' + uid;
});
db.getObjects(keys, function (err, users) {
if (err) {
return callback(err);
}
users = uids.map(function (uid) {
return users[ref[uid]];
});
modifyUserData(users, [], callback);
});
async.waterfall([
function (next) {
db.getObjects(keys, function (err, users) {
if (err) {
return callback(err);
}
users = uids.map(function (uid) {
return users[ref[uid]];
});
next(null, users);
});
},
function (users, next) {
modifyUserData(users, [], next);
},
], callback);
};
function modifyUserData(users, fieldsToRemove, callback) {
@ -178,51 +193,53 @@ module.exports = function (User) {
User.setUserField = function (uid, field, value, callback) {
callback = callback || function () {};
db.setObjectField('user:' + uid, field, value, function (err) {
if (err) {
return callback(err);
}
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'set' });
callback();
});
async.waterfall([
function (next) {
db.setObjectField('user:' + uid, field, value, next);
},
function (next) {
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'set' });
next();
},
], callback);
};
User.setUserFields = function (uid, data, callback) {
callback = callback || function () {};
db.setObject('user:' + uid, data, function (err) {
if (err) {
return callback(err);
}
for (var field in data) {
if (data.hasOwnProperty(field)) {
plugins.fireHook('action:user.set', { uid: uid, field: field, value: data[field], type: 'set' });
async.waterfall([
function (next) {
db.setObject('user:' + uid, data, next);
},
function (next) {
for (var field in data) {
if (data.hasOwnProperty(field)) {
plugins.fireHook('action:user.set', { uid: uid, field: field, value: data[field], type: 'set' });
}
}
}
callback();
});
next();
},
], callback);
};
User.incrementUserFieldBy = function (uid, field, value, callback) {
callback = callback || function () {};
db.incrObjectFieldBy('user:' + uid, field, value, function (err, value) {
if (err) {
return callback(err);
}
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'increment' });
callback(null, value);
});
incrDecrUserFieldBy(uid, field, value, 'increment', callback);
};
User.decrementUserFieldBy = function (uid, field, value, callback) {
callback = callback || function () {};
db.incrObjectFieldBy('user:' + uid, field, -value, function (err, value) {
if (err) {
return callback(err);
}
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: 'decrement' });
callback(null, value);
});
incrDecrUserFieldBy(uid, field, -value, 'decrement', callback);
};
function incrDecrUserFieldBy(uid, field, value, type, callback) {
callback = callback || function () {};
async.waterfall([
function (next) {
db.incrObjectFieldBy('user:' + uid, field, value, next);
},
function (value, next) {
plugins.fireHook('action:user.set', { uid: uid, field: field, value: value, type: type });
next(null, value);
},
], callback);
}
};

@ -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,21 +61,20 @@ module.exports.listen = function (callback) {
logger.init(app);
initializeNodeBB(function (err) {
if (err) {
return callback(err);
}
winston.info('NodeBB Ready');
async.waterfall([
initializeNodeBB,
function (next) {
winston.info('NodeBB Ready');
require('./socket.io').server.emit('event:nodebb.ready', {
'cache-buster': meta.config['cache-buster'],
});
require('./socket.io').server.emit('event:nodebb.ready', {
'cache-buster': meta.config['cache-buster'],
});
plugins.fireHook('action:nodebb.ready');
plugins.fireHook('action:nodebb.ready');
listen(callback);
});
listen(next);
},
], callback);
};
function initializeNodeBB(callback) {
@ -107,7 +106,9 @@ function initializeNodeBB(callback) {
meta.blacklist.load,
], next);
},
], callback);
], function (err) {
callback(err);
});
}
function setupExpressApp(app) {

@ -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,11 +498,23 @@ describe('Controllers', function () {
hidden: 0,
}, function (err) {
assert.ifError(err);
request(nconf.get('url') + '/groups/group-details', function (err, res, body) {
groups.join('group-details', fooUid, function (err) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
done();
topics.post({
uid: fooUid,
title: 'topic title',
content: 'test topic content',
cid: cid,
}, function (err) {
assert.ifError(err);
request(nconf.get('url') + '/api/groups/group-details', { json: true }, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
assert.equal(body.posts[0].content, 'test topic content');
done();
});
});
});
});
});
@ -532,6 +544,15 @@ describe('Controllers', function () {
});
});
it('should get recent posts', function (done) {
request(nconf.get('url') + '/api/recent/posts/month', function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
done();
});
});
it('should get post data', function (done) {
request(nconf.get('url') + '/api/post/pid/' + pid, function (err, res, body) {
assert.ifError(err);
@ -890,6 +911,42 @@ describe('Controllers', function () {
},
], done);
});
it('should 404 if user does not exist', function (done) {
request(nconf.get('url') + '/api/user/email/doesnotexist', function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 404);
assert(body);
done();
});
});
it('should load user by uid', function (done) {
request(nconf.get('url') + '/api/user/uid/' + fooUid, function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
done();
});
});
it('should load user by username', function (done) {
request(nconf.get('url') + '/api/user/username/foo', function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
done();
});
});
it('should load user by email', function (done) {
request(nconf.get('url') + '/api/user/email/foo@test.com', function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
done();
});
});
});
describe('account follow page', function () {
@ -943,7 +1000,7 @@ describe('Controllers', function () {
describe('post redirect', function () {
it('should 404 for invalid pid', function (done) {
request(nconf.get('url') + '/post/fail', function (err, res) {
request(nconf.get('url') + '/api/post/fail', function (err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 404);
done();

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