Merge branch 'master' into notif-abort

This commit is contained in:
Peter Jaszkowiak
2017-05-09 11:31:58 -06:00
committed by GitHub
776 changed files with 9680 additions and 5060 deletions

View File

@@ -16,17 +16,21 @@ var uniquevisitors = 0;
var isCategory = /^(?:\/api)?\/category\/(\d+)/;
new cronJob('*/10 * * * *', function () {
new cronJob('*/10 * * * * *', function () {
Analytics.writeData();
}, null, true);
Analytics.increment = function (keys) {
Analytics.increment = function (keys, callback) {
keys = Array.isArray(keys) ? keys : [keys];
keys.forEach(function (key) {
counters[key] = counters[key] || 0;
counters[key] += 1;
});
if (typeof callback === 'function') {
callback();
}
};
Analytics.pageView = function (payload) {

View File

@@ -21,6 +21,15 @@ exports.processSortedSet = function (setKey, process, options, callback) {
return callback(new Error('[[error:process-not-a-function]]'));
}
// Progress bar handling (upgrade scripts)
if (options.progress) {
db.sortedSetCard(setKey, function (err, total) {
if (!err) {
options.progress.total = total;
}
});
}
// use the fast path if possible
if (db.processSortedSet && typeof options.doneIf !== 'function' && !utils.isNumber(options.alwaysStartAt)) {
return db.processSortedSet(setKey, process, options.batch || DEFAULT_BATCH_SIZE, callback);
@@ -53,6 +62,7 @@ exports.processSortedSet = function (setKey, process, options, callback) {
}
start += utils.isNumber(options.alwaysStartAt) ? options.alwaysStartAt : batch + 1;
stop = start + batch;
next();
});
});

View File

@@ -9,7 +9,7 @@ process.on('message', function (msg) {
if (msg.type === 'hash') {
hashPassword(msg.password, msg.rounds);
} else if (msg.type === 'compare') {
bcrypt.compare(msg.password, msg.hash, done);
bcrypt.compare(String(msg.password || ''), String(msg.hash || ''), done);
}
});

View File

@@ -72,7 +72,7 @@ module.exports = function (Categories) {
next(null, category);
},
function (category, next) {
plugins.fireHook('action:category.create', category);
plugins.fireHook('action:category.create', { category: category });
next(null, category);
},
], callback);
@@ -138,9 +138,20 @@ module.exports = function (Categories) {
};
Categories.copyPrivilegesFrom = function (fromCid, toCid, callback) {
async.each(privileges.privilegeList, function (privilege, next) {
copyPrivilege(privilege, fromCid, toCid, next);
}, callback);
async.waterfall([
function (next) {
plugins.fireHook('filter:categories.copyPrivilegesFrom', {
privileges: privileges.privilegeList,
fromCid: fromCid,
toCid: toCid,
}, next);
},
function (data, next) {
async.each(data.privileges, function (privilege, next) {
copyPrivilege(privilege, data.fromCid, data.toCid, next);
}, next);
},
], callback);
};
function copyPrivilege(privilege, fromCid, toCid, callback) {

View File

@@ -30,7 +30,7 @@ module.exports = function (Categories) {
purgeCategory(cid, next);
},
function (next) {
plugins.fireHook('action:category.delete', cid);
plugins.fireHook('action:category.delete', { cid: cid, uid: uid });
next();
},
], callback);
@@ -58,7 +58,7 @@ module.exports = function (Categories) {
], next);
},
function (next) {
async.each(privileges.privilegeList, function (privilege, next) {
async.eachSeries(privileges.privilegeList, function (privilege, next) {
groups.destroy('cid:' + cid + ':privileges:' + privilege, next);
}, next);
},

View File

@@ -26,7 +26,7 @@ editController.get = function (req, res, callback) {
userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10);
userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads, 10) === 1;
userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1;
userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 128;
userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 200;
userData.groups = userData.groups.filter(function (group) {
return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users';

View File

@@ -4,14 +4,16 @@
var async = require('async');
var validator = require('validator');
var winston = require('winston');
var nconf = require('nconf');
var user = require('../../user');
var groups = require('../../groups');
var plugins = require('../../plugins');
var meta = require('../../meta');
var utils = require('../../utils');
var privileges = require('../../privileges');
var helpers = {};
var helpers = module.exports;
helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
async.waterfall([
@@ -60,6 +62,9 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
sso: function (next) {
plugins.fireHook('filter:auth.list', { uid: uid, associations: [] }, next);
},
canBanUser: function (next) {
privileges.users.canBanUser(callerUID, uid, next);
},
}, next);
},
function (results, next) {
@@ -109,7 +114,7 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
userData.isAdminOrGlobalModeratorOrModerator = isAdmin || isGlobalModerator || isModerator;
userData.isSelfOrAdminOrGlobalModerator = isSelf || isAdmin || isGlobalModerator;
userData.canEdit = isAdmin || (isGlobalModerator && !results.isTargetAdmin);
userData.canBan = isAdmin || (isGlobalModerator && !results.isTargetAdmin);
userData.canBan = results.canBanUser;
userData.canChangePassword = isAdmin || (isSelf && parseInt(meta.config['password:disableEdit'], 10) !== 1);
userData.isSelf = isSelf;
userData.isFollowing = results.isFollowing;
@@ -119,7 +124,13 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
userData['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1;
userData['email:confirmed'] = !!parseInt(userData['email:confirmed'], 10);
userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), isSelf);
userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), {
self: isSelf,
other: !isSelf,
moderator: isModerator,
globalMod: isGlobalModerator,
admin: isAdmin,
});
userData.sso = results.sso.associations;
userData.status = user.getStatus(userData);
@@ -138,7 +149,7 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
userData.birthday = validator.escape(String(userData.birthday || ''));
userData.moderationNote = validator.escape(String(userData.moderationNote || ''));
userData['cover:url'] = userData['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(userData.uid);
userData['cover:url'] = userData['cover:url'] ? (nconf.get('relative_path') + userData['cover:url']) : require('../../coverPhoto').getDefaultProfileCover(userData.uid);
userData['cover:position'] = validator.escape(String(userData['cover:position'] || '50% 50%'));
userData['username:disableEdit'] = !userData.isAdmin && parseInt(meta.config['username:disableEdit'], 10) === 1;
userData['email:disableEdit'] = !userData.isAdmin && parseInt(meta.config['email:disableEdit'], 10) === 1;
@@ -154,10 +165,29 @@ helpers.getBaseUser = function (userslug, callerUID, callback) {
helpers.getUserDataByUserSlug(userslug, callerUID, callback);
};
function filterLinks(links, self) {
return links.filter(function (link) {
return link && (link.public || self);
function filterLinks(links, states) {
return links.filter(function (link, index) {
// "public" is the old property, if visibility is defined, discard `public`
if (link.hasOwnProperty('public') && !link.hasOwnProperty('visibility')) {
winston.warn('[account/profileMenu (' + link.id + ')] Use of the `.public` property is deprecated, use `visibility` now');
return link && (link.public || states.self);
}
// Default visibility
link.visibility = Object.assign({
self: true,
other: true,
moderator: true,
globalMod: true,
admin: true,
}, link.visibility);
// Iterate through states and permit if every test passes (or is not defined)
var permit = Object.keys(states).some(function (state) {
return states[state] === link.visibility[state];
});
links[index].public = permit;
return permit;
});
}
module.exports = helpers;

View File

@@ -2,14 +2,19 @@
var async = require('async');
var db = require('../../database');
var user = require('../../user');
var helpers = require('../helpers');
var accountHelpers = require('./helpers');
var pagination = require('../../pagination');
var infoController = {};
var infoController = module.exports;
infoController.get = function (req, res, callback) {
var userData;
var page = Math.max(1, req.query.page || 1);
var itemsPerPage = 10;
async.waterfall([
function (next) {
accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
@@ -19,11 +24,27 @@ infoController.get = function (req, res, callback) {
if (!userData) {
return callback();
}
var start = (page - 1) * itemsPerPage;
var stop = start + itemsPerPage - 1;
async.parallel({
history: async.apply(user.getModerationHistory, userData.uid),
sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID),
usernames: async.apply(user.getHistory, 'user:' + userData.uid + ':usernames'),
emails: async.apply(user.getHistory, 'user:' + userData.uid + ':emails'),
notes: function (next) {
if (!userData.isAdminOrGlobalModeratorOrModerator) {
return setImmediate(next);
}
async.parallel({
notes: function (next) {
user.getModerationNotes(userData.uid, start, stop, next);
},
count: function (next) {
db.sortedSetCard('uid:' + userData.uid + ':moderation:notes', next);
},
}, next);
},
}, next);
},
], function (err, data) {
@@ -35,11 +56,15 @@ infoController.get = function (req, res, callback) {
userData.sessions = data.sessions;
userData.usernames = data.usernames;
userData.emails = data.emails;
if (userData.isAdminOrGlobalModeratorOrModerator) {
userData.moderationNotes = data.notes.notes;
var pageCount = Math.ceil(data.notes.count / itemsPerPage);
userData.pagination = pagination.create(page, pageCount, req.query);
}
userData.title = '[[pages:account/info]]';
userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[user:account_info]]' }]);
res.render('account/info', userData);
});
};
module.exports = infoController;

View File

@@ -1,23 +1,91 @@
'use strict';
var async = require('async');
var user = require('../../user');
var helpers = require('../helpers');
var plugins = require('../../plugins');
var pagination = require('../../pagination');
var notificationsController = {};
var notificationsController = module.exports;
notificationsController.get = function (req, res, next) {
user.notifications.getAll(req.uid, 0, 39, function (err, notifications) {
if (err) {
return next(err);
}
res.render('notifications', {
notifications: notifications,
nextStart: 40,
title: '[[pages:notifications]]',
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:notifications]]' }]),
});
});
var regularFilters = [
{ name: '[[notifications:all]]', filter: '' },
{ name: '[[notifications:topics]]', filter: 'new-topic' },
{ name: '[[notifications:replies]]', filter: 'new-reply' },
{ name: '[[notifications:chat]]', filter: 'new-chat' },
{ name: '[[notifications:follows]]', filter: 'follow' },
{ name: '[[notifications:upvote]]', filter: 'upvote' },
];
var moderatorFilters = [
{ name: '[[notifications:new-flags]]', filter: 'new-post-flag' },
{ name: '[[notifications:my-flags]]', filter: 'my-flags' },
{ name: '[[notifications:bans]]', filter: 'ban' },
];
var filter = req.query.filter || '';
var page = Math.max(1, req.query.page || 1);
var itemsPerPage = 20;
var start = (page - 1) * itemsPerPage;
var stop = start + itemsPerPage - 1;
var selectedFilter;
var pageCount = 1;
var allFilters = [];
async.waterfall([
function (next) {
async.parallel({
filters: function (next) {
plugins.fireHook('filter:notifications.addFilters', {
regularFilters: regularFilters,
moderatorFilters: moderatorFilters,
uid: req.uid,
}, next);
},
isPrivileged: function (next) {
user.isPrivileged(req.uid, next);
},
}, next);
},
function (data, _next) {
allFilters = data.filters.regularFilters;
if (data.isPrivileged) {
allFilters = allFilters.concat([
{ separator: true },
]).concat(data.filters.moderatorFilters);
}
selectedFilter = allFilters.find(function (filterData) {
filterData.selected = filterData.filter === filter;
return filterData.selected;
});
if (!selectedFilter) {
return next();
}
user.notifications.getAll(req.uid, selectedFilter.filter, _next);
},
function (nids, next) {
pageCount = nids.length / itemsPerPage;
nids = nids.slice(start, stop + 1);
user.notifications.getNotifications(nids, req.uid, next);
},
function (notifications) {
res.render('notifications', {
notifications: notifications,
pagination: pagination.create(page, pageCount, req.query),
filters: allFilters,
regularFilters: regularFilters,
moderatorFilters: moderatorFilters,
selectedFilter: selectedFilter,
title: '[[pages:notifications]]',
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:notifications]]' }]),
});
},
], next);
};
module.exports = notificationsController;

View File

@@ -82,9 +82,6 @@ profileController.get = function (req, res, callback) {
var pageCount = Math.ceil(userData.postcount / itemsPerPage);
userData.pagination = pagination.create(page, pageCount, req.query);
userData['cover:url'] = userData['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(userData.uid);
userData['cover:position'] = userData['cover:position'] || '50% 50%';
if (!parseInt(userData.profileviews, 10)) {
userData.profileviews = 1;
}

View File

@@ -4,7 +4,6 @@ var adminController = {
dashboard: require('./admin/dashboard'),
categories: require('./admin/categories'),
tags: require('./admin/tags'),
flags: require('./admin/flags'),
blacklist: require('./admin/blacklist'),
groups: require('./admin/groups'),
appearance: require('./admin/appearance'),

View File

@@ -5,6 +5,7 @@ var cacheController = {};
cacheController.get = function (req, res) {
var postCache = require('../../posts/cache');
var groupCache = require('../../groups').cache;
var userSettingsCache = require('../../user').settingsCache;
var avgPostSize = 0;
var percentFull = 0;
@@ -21,6 +22,12 @@ cacheController.get = function (req, res) {
percentFull: percentFull,
avgPostSize: avgPostSize,
},
userSettingsCache: {
length: userSettingsCache.length,
max: userSettingsCache.max,
itemCount: userSettingsCache.itemCount,
percentFull: ((userSettingsCache.length / userSettingsCache.max) * 100).toFixed(2),
},
groupCache: {
length: groupCache.length,
max: groupCache.max,

View File

@@ -1,102 +0,0 @@
'use strict';
var async = require('async');
var validator = require('validator');
var posts = require('../../posts');
var user = require('../../user');
var categories = require('../../categories');
var analytics = require('../../analytics');
var pagination = require('../../pagination');
var flagsController = {};
var itemsPerPage = 20;
flagsController.get = function (req, res, next) {
var byUsername = req.query.byUsername || '';
var cid = req.query.cid || 0;
var sortBy = req.query.sortBy || 'count';
var page = parseInt(req.query.page, 10) || 1;
async.parallel({
categories: function (next) {
categories.buildForSelect(req.uid, next);
},
flagData: function (next) {
getFlagData(req, res, next);
},
analytics: function (next) {
analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
},
assignees: async.apply(user.getAdminsandGlobalModsandModerators),
}, function (err, results) {
if (err) {
return next(err);
}
// Minimise data set for assignees so tjs does less work
results.assignees = results.assignees.map(function (userObj) {
return {
uid: userObj.uid,
username: userObj.username,
};
});
// If res.locals.cids is populated, then slim down the categories list
if (res.locals.cids) {
results.categories = results.categories.filter(function (category) {
return res.locals.cids.indexOf(String(category.cid)) !== -1;
});
}
var pageCount = Math.max(1, Math.ceil(results.flagData.count / itemsPerPage));
results.categories.forEach(function (category) {
category.selected = parseInt(category.cid, 10) === parseInt(cid, 10);
});
var data = {
posts: results.flagData.posts,
assignees: results.assignees,
analytics: results.analytics,
categories: results.categories,
byUsername: validator.escape(String(byUsername)),
sortByCount: sortBy === 'count',
sortByTime: sortBy === 'time',
pagination: pagination.create(page, pageCount, req.query),
};
res.render('admin/manage/flags', data);
});
};
function getFlagData(req, res, callback) {
var sortBy = req.query.sortBy || 'count';
var byUsername = req.query.byUsername || '';
var cid = req.query.cid || res.locals.cids || 0;
var page = parseInt(req.query.page, 10) || 1;
var start = (page - 1) * itemsPerPage;
var stop = start + itemsPerPage - 1;
var sets = [sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'];
async.waterfall([
function (next) {
if (byUsername) {
user.getUidByUsername(byUsername, next);
} else {
process.nextTick(next, null, 0);
}
},
function (uid, next) {
if (uid) {
sets.push('uid:' + uid + ':flag:pids');
}
posts.getFlags(sets, cid, req.uid, start, stop, next);
},
], callback);
}
module.exports = flagsController;

View File

@@ -16,22 +16,30 @@ var info = {};
infoController.get = function (req, res) {
info = {};
pubsub.publish('sync:node:info:start');
var timeoutMS = 1000;
setTimeout(function () {
var data = [];
Object.keys(info).forEach(function (key) {
data.push(info[key]);
});
data.sort(function (a, b) {
if (a.os.hostname < b.os.hostname) {
if (a.id < b.id) {
return -1;
}
if (a.os.hostname > b.os.hostname) {
if (a.id > b.id) {
return 1;
}
return 0;
});
res.render('admin/development/info', { info: data, infoJSON: JSON.stringify(data, null, 4), host: os.hostname(), port: nconf.get('port') });
}, 500);
res.render('admin/development/info', {
info: data,
infoJSON: JSON.stringify(data, null, 4),
host: os.hostname(),
port: nconf.get('port'),
nodeCount: data.length,
timeout: timeoutMS,
});
}, timeoutMS);
};
pubsub.on('sync:node:info:start', function () {
@@ -39,7 +47,8 @@ pubsub.on('sync:node:info:start', function () {
if (err) {
return winston.error(err);
}
pubsub.publish('sync:node:info:end', { data: data, id: os.hostname() + ':' + nconf.get('port') });
data.id = os.hostname() + ':' + nconf.get('port');
pubsub.publish('sync:node:info:end', { data: data, id: data.id });
});
});

View File

@@ -18,6 +18,7 @@ languagesController.get = function (req, res, next) {
res.render('admin/general/languages', {
languages: languages,
autoDetectLang: parseInt(meta.config.autoDetectLang, 10) === 1,
});
});
};

View File

@@ -7,9 +7,9 @@ var themesController = {};
themesController.get = function (req, res, next) {
var themeDir = path.join(__dirname, '../../../node_modules/' + req.params.theme);
file.exists(themeDir, function (exists) {
if (!exists) {
return next();
file.exists(themeDir, function (err, exists) {
if (err || !exists) {
return next(err);
}
var themeConfig = require(path.join(themeDir, 'theme.json'));

View File

@@ -5,6 +5,7 @@ var path = require('path');
var async = require('async');
var nconf = require('nconf');
var winston = require('winston');
var mime = require('mime');
var meta = require('../../meta');
var file = require('../../file');
@@ -102,6 +103,11 @@ uploadsController.uploadLogo = function (req, res, next) {
uploadsController.uploadSound = function (req, res, next) {
var uploadedFile = req.files.files[0];
var mimeType = mime.lookup(uploadedFile.name);
if (!/^audio\//.test(mimeType)) {
return next(Error('[[error:invalid-data]]'));
}
file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function (err) {
if (err) {
return next(err);

View File

@@ -127,8 +127,10 @@ function getUsers(set, section, min, max, req, res, next) {
count: function (next) {
if (byScore) {
db.sortedSetCount(set, min, max, next);
} else {
} else if (set === 'users:banned' || set === 'users:notvalidated') {
db.sortedSetCard(set, next);
} else {
db.getObjectField('global', 'userCount', next);
}
},
users: function (next) {

View File

@@ -13,10 +13,11 @@ var user = require('../user');
var plugins = require('../plugins');
var utils = require('../utils');
var Password = require('../password');
var translator = require('../translator');
var sockets = require('../socket.io');
var authenticationController = {};
var authenticationController = module.exports;
authenticationController.register = function (req, res) {
var registrationType = meta.config.registrationType || 'normal';
@@ -330,7 +331,7 @@ authenticationController.onSuccessfulLogin = function (req, uid, callback) {
// Force session check for all connected socket.io clients with the same session id
sockets.in('sess_' + req.sessionID).emit('checkSession', uid);
plugins.fireHook('action:user.loggedIn', uid);
plugins.fireHook('action:user.loggedIn', { uid: uid, req: req });
callback();
});
};
@@ -344,21 +345,21 @@ authenticationController.localLogin = function (req, username, password, next) {
var uid;
var userData = {};
if (!password || !utils.isPasswordValid(password)) {
return next(new Error('[[error:invalid-password]]'));
}
if (password.length > 4096) {
return next(new Error('[[error:password-too-long]]'));
}
async.waterfall([
function (next) {
user.isPasswordValid(password, next);
},
function (next) {
user.getUidByUserslug(userslug, next);
},
function (_uid, next) {
if (!_uid) {
return next(new Error('[[error:no-user]]'));
}
uid = _uid;
user.auth.logAttempt(uid, req.ip, next);
},
function (next) {
async.parallel({
userData: function (next) {
db.getObjectFields('user:' + uid, ['password', 'passwordExpiry'], next);
@@ -379,31 +380,19 @@ authenticationController.localLogin = function (req, username, password, next) {
if (!result.isAdmin && parseInt(meta.config.allowLocalLogin, 10) === 0) {
return next(new Error('[[error:local-login-disabled]]'));
}
if (!userData || !userData.password) {
return next(new Error('[[error:invalid-user-data]]'));
}
if (result.banned) {
// Retrieve ban reason and show error
return user.getLatestBanInfo(uid, function (err, banInfo) {
if (err) {
if (err.message === 'no-ban-info') {
next(new Error('[[error:user-banned]]'));
} else {
next(err);
}
} else if (banInfo.reason) {
next(new Error('[[error:user-banned-reason, ' + banInfo.reason + ']]'));
} else {
next(new Error('[[error:user-banned]]'));
}
});
return banUser(uid, next);
}
user.auth.logAttempt(uid, req.ip, next);
},
function (next) {
Password.compare(password, userData.password, next);
},
function (passwordMatch, next) {
if (!passwordMatch) {
return next(new Error('[[error:invalid-password]]'));
return next(new Error('[[error:invalid-login-credentials]]'));
}
user.auth.clearLoginAttempts(uid);
next(null, userData, '[[success:authentication-successful]]');
@@ -437,5 +426,23 @@ authenticationController.logout = function (req, res, next) {
}
};
function banUser(uid, next) {
user.getLatestBanInfo(uid, function (err, banInfo) {
if (err) {
if (err.message === 'no-ban-info') {
return next(new Error('[[error:user-banned]]'));
}
module.exports = authenticationController;
return next(err);
}
if (!banInfo.reason) {
translator.translate('[[user:info.banned-no-reason]]', function (translated) {
banInfo.reason = translated;
next(new Error(banInfo.expiry ? '[[error:user-banned-reason-until, ' + banInfo.expiry_readable + ', ' + banInfo.reason + ']]' : '[[error:user-banned-reason, ' + banInfo.reason + ']]'));
});
} else {
next(new Error(banInfo.expiry ? '[[error:user-banned-reason-until, ' + banInfo.expiry_readable + ', ' + banInfo.reason + ']]' : '[[error:user-banned-reason, ' + banInfo.reason + ']]'));
}
});
}

View File

@@ -21,17 +21,6 @@ categoriesController.list = function (req, res, next) {
content: 'website',
}];
var ogImage = meta.config['og:image'] || meta.config['brand:logo'] || '';
if (ogImage) {
if (!ogImage.startsWith('http')) {
ogImage = nconf.get('url') + ogImage;
}
res.locals.metaTags.push({
property: 'og:image',
content: ogImage,
});
}
var categoryData;
async.waterfall([
function (next) {

View File

@@ -370,7 +370,3 @@ Controllers.termsOfUse = function (req, res, next) {
termsOfUse: meta.config.termsOfUse,
});
};
Controllers.ping = function (req, res) {
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
};

View File

@@ -3,24 +3,125 @@
var async = require('async');
var user = require('../user');
var adminFlagsController = require('./admin/flags');
var categories = require('../categories');
var flags = require('../flags');
var analytics = require('../analytics');
var modsController = {};
var modsController = {
flags: {},
};
modsController.flagged = function (req, res, next) {
modsController.flags.list = function (req, res, next) {
async.parallel({
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
moderatedCids: async.apply(user.getModeratedCids, req.uid),
}, function (err, results) {
if (err || !(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
if (err) {
return next(err);
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
return next(new Error('[[error:no-privileges]]'));
}
if (!results.isAdminOrGlobalMod && results.moderatedCids.length) {
res.locals.cids = results.moderatedCids;
}
adminFlagsController.get(req, res, next);
// Parse query string params for filters
var hasFilter = false;
var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick'];
var filters = valid.reduce(function (memo, cur) {
if (req.query.hasOwnProperty(cur)) {
memo[cur] = req.query[cur];
}
return memo;
}, {});
hasFilter = !!Object.keys(filters).length;
if (res.locals.cids) {
if (!filters.cid) {
// If mod and no cid filter, add filter for their modded categories
filters.cid = res.locals.cids;
} else if (Array.isArray(filters.cid)) {
// Remove cids they do not moderate
filters.cid = filters.cid.filter(function (cid) {
return res.locals.cids.indexOf(String(cid)) !== -1;
});
} else if (res.locals.cids.indexOf(String(filters.cid)) === -1) {
filters.cid = res.locals.cids;
hasFilter = false;
}
}
async.parallel({
flags: async.apply(flags.list, filters, req.uid),
analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30),
categories: async.apply(categories.buildForSelect, req.uid),
}, function (err, data) {
if (err) {
return next(err);
}
// If res.locals.cids is populated, then slim down the categories list
if (res.locals.cids) {
data.categories = data.categories.filter(function (category) {
return res.locals.cids.indexOf(String(category.cid)) !== -1;
});
}
// Minimal returned set for templates.js
data.categories = data.categories.reduce(function (memo, cur) {
if (!res.locals.cids) {
memo[cur.cid] = cur.name;
return memo;
}
// If mod, remove categories they can't moderate
if (res.locals.cids.indexOf(String(cur.cid)) !== -1) {
memo[cur.cid] = cur.name;
}
return memo;
}, {});
res.render('flags/list', {
flags: data.flags,
analytics: data.analytics,
categories: data.categories,
hasFilter: hasFilter,
filters: filters,
title: '[[pages:flags]]',
});
});
});
};
modsController.flags.detail = function (req, res, next) {
async.parallel({
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
moderatedCids: async.apply(user.getModeratedCids, req.uid),
flagData: async.apply(flags.get, req.params.flagId),
assignees: async.apply(user.getAdminsandGlobalModsandModerators),
}, function (err, results) {
if (err || !results.flagData) {
return next(err || new Error('[[error:invalid-data]]'));
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
return next(new Error('[[error:no-privileges]]'));
}
res.render('flags/detail', Object.assign(results.flagData, {
assignees: results.assignees,
type_bool: ['post', 'user', 'empty'].reduce(function (memo, cur) {
if (cur !== 'empty') {
memo[cur] = results.flagData.type === cur && !!Object.keys(results.flagData.target).length;
} else {
memo[cur] = !Object.keys(results.flagData.target).length;
}
return memo;
}, {}),
title: '[[pages:flag-details, ' + req.params.flagId + ']]',
}));
});
};

View File

@@ -38,7 +38,7 @@ searchController.search = function (req, res, next) {
repliesFilter: req.query.repliesFilter,
timeRange: req.query.timeRange,
timeFilter: req.query.timeFilter,
sortBy: req.query.sortBy,
sortBy: req.query.sortBy || meta.config.searchDefaultSortBy || '',
sortDirection: req.query.sortDirection,
page: page,
uid: req.uid,
@@ -60,13 +60,14 @@ 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';
searchData.title = '[[global:header.search]]';
searchData.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[global:search]]' }]);
searchData.expandSearch = !req.query.term;
searchData.searchDefaultSortBy = meta.config.searchDefaultSortBy || '';
res.render('search', searchData);
});

View File

@@ -1,8 +1,6 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var validator = require('validator');
var user = require('../user');
@@ -63,10 +61,6 @@ tagsController.getTag = function (req, res, next) {
property: 'og:title',
content: tag,
},
{
property: 'og:url',
content: nconf.get('url') + '/tags/' + tag,
},
];
templateData.topics = topics;

View File

@@ -206,11 +206,6 @@ topicsController.get = function (req, res, callback) {
property: 'og:type',
content: 'article',
},
{
property: 'og:url',
content: nconf.get('url') + '/topic/' + topicData.slug + (req.params.post_index ? ('/' + req.params.post_index) : ''),
noEscape: true,
},
{
property: 'og:image',
content: ogImageUrl,

View File

@@ -40,7 +40,15 @@ unreadController.get = function (req, res, next) {
settings = results.settings;
var start = Math.max(0, (page - 1) * settings.topicsPerPage);
var stop = start + settings.topicsPerPage - 1;
topics.getUnreadTopics(cid, req.uid, start, stop, filter, next);
var cutoff = req.session.unreadCutoff ? req.session.unreadCutoff : topics.unreadCutoff();
topics.getUnreadTopics({
cid: cid,
uid: req.uid,
start: start,
stop: stop,
filter: filter,
cutoff: cutoff,
}, next);
},
], function (err, data) {
if (err) {

View File

@@ -33,7 +33,7 @@ uploadsController.upload = function (req, res, filesIterator) {
return res.status(500).json({ path: req.path, error: err.message });
}
res.status(200).send(images);
res.status(200).json(images);
});
};
@@ -208,20 +208,19 @@ uploadsController.uploadFile = function (uid, uploadedFile, callback) {
return callback(new Error('[[error:file-too-big, ' + meta.config.maximumFileSize + ']]'));
}
if (meta.config.hasOwnProperty('allowedFileExtensions')) {
var allowed = file.allowedExtensions();
var extension = file.typeToExtension(uploadedFile.type);
if (!extension || (allowed.length > 0 && allowed.indexOf(extension) === -1)) {
return callback(new Error('[[error:invalid-file-type, ' + allowed.join('&#44; ') + ']]'));
}
var allowed = file.allowedExtensions();
var extension = path.extname(uploadedFile.name).toLowerCase();
if (!extension || extension === '.' || (allowed.length > 0 && allowed.indexOf(extension) === -1)) {
return callback(new Error('[[error:invalid-file-type, ' + allowed.join('&#44; ') + ']]'));
}
saveFileToLocal(uploadedFile, callback);
};
function saveFileToLocal(uploadedFile, callback) {
var extension = file.typeToExtension(uploadedFile.type);
if (!extension) {
var extension = path.extname(uploadedFile.name);
if (!extension || extension === '.') {
return callback(new Error('[[error:invalid-extension]]'));
}
var filename = uploadedFile.name || 'upload';

View File

@@ -1,239 +1,244 @@
'use strict';
(function (module) {
var winston = require('winston');
var async = require('async');
var nconf = require('nconf');
var session = require('express-session');
var _ = require('underscore');
var semver = require('semver');
var db;
_.mixin(require('underscore.deep'));
var winston = require('winston');
var async = require('async');
var nconf = require('nconf');
var session = require('express-session');
var _ = require('underscore');
var semver = require('semver');
var db;
module.questions = [
{
name: 'mongo:host',
description: 'Host IP or address of your MongoDB instance',
default: nconf.get('mongo:host') || '127.0.0.1',
},
{
name: 'mongo:port',
description: 'Host port of your MongoDB instance',
default: nconf.get('mongo:port') || 27017,
},
{
name: 'mongo:username',
description: 'MongoDB username',
default: nconf.get('mongo:username') || '',
},
{
name: 'mongo:password',
description: 'Password of your MongoDB database',
hidden: true,
default: nconf.get('mongo:password') || '',
before: function (value) { value = value || nconf.get('mongo:password') || ''; return value; },
},
{
name: 'mongo:database',
description: 'MongoDB database name',
default: nconf.get('mongo:database') || 'nodebb',
},
];
_.mixin(require('underscore.deep'));
module.helpers = module.helpers || {};
module.helpers.mongo = require('./mongo/helpers');
var mongoModule = module.exports;
module.init = function (callback) {
callback = callback || function () { };
mongoModule.questions = [
{
name: 'mongo:host',
description: 'Host IP or address of your MongoDB instance',
default: nconf.get('mongo:host') || '127.0.0.1',
},
{
name: 'mongo:port',
description: 'Host port of your MongoDB instance',
default: nconf.get('mongo:port') || 27017,
},
{
name: 'mongo:username',
description: 'MongoDB username',
default: nconf.get('mongo:username') || '',
},
{
name: 'mongo:password',
description: 'Password of your MongoDB database',
hidden: true,
default: nconf.get('mongo:password') || '',
before: function (value) { value = value || nconf.get('mongo:password') || ''; return value; },
},
{
name: 'mongo:database',
description: 'MongoDB database name',
default: nconf.get('mongo:database') || 'nodebb',
},
];
var mongoClient = require('mongodb').MongoClient;
mongoModule.helpers = mongoModule.helpers || {};
mongoModule.helpers.mongo = require('./mongo/helpers');
var usernamePassword = '';
if (nconf.get('mongo:username') && nconf.get('mongo:password')) {
usernamePassword = nconf.get('mongo:username') + ':' + encodeURIComponent(nconf.get('mongo:password')) + '@';
}
mongoModule.init = function (callback) {
callback = callback || function () { };
// Sensible defaults for Mongo, if not set
if (!nconf.get('mongo:host')) {
nconf.set('mongo:host', '127.0.0.1');
}
if (!nconf.get('mongo:port')) {
nconf.set('mongo:port', 27017);
}
if (!nconf.get('mongo:database')) {
nconf.set('mongo:database', 'nodebb');
}
var mongoClient = require('mongodb').MongoClient;
var hosts = nconf.get('mongo:host').split(',');
var ports = nconf.get('mongo:port').toString().split(',');
var servers = [];
var usernamePassword = '';
if (nconf.get('mongo:username') && nconf.get('mongo:password')) {
usernamePassword = nconf.get('mongo:username') + ':' + encodeURIComponent(nconf.get('mongo:password')) + '@';
}
for (var i = 0; i < hosts.length; i += 1) {
servers.push(hosts[i] + ':' + ports[i]);
}
// Sensible defaults for Mongo, if not set
if (!nconf.get('mongo:host')) {
nconf.set('mongo:host', '127.0.0.1');
}
if (!nconf.get('mongo:port')) {
nconf.set('mongo:port', 27017);
}
if (!nconf.get('mongo:database')) {
nconf.set('mongo:database', 'nodebb');
}
var connString = 'mongodb://' + usernamePassword + servers.join() + '/' + nconf.get('mongo:database');
var hosts = nconf.get('mongo:host').split(',');
var ports = nconf.get('mongo:port').toString().split(',');
var servers = [];
var connOptions = {
poolSize: 10,
reconnectTries: 3600,
reconnectInterval: 1000,
autoReconnect: true,
};
for (var i = 0; i < hosts.length; i += 1) {
servers.push(hosts[i] + ':' + ports[i]);
}
connOptions = _.deepExtend(connOptions, nconf.get('mongo:options') || {});
var connString = 'mongodb://' + usernamePassword + servers.join() + '/' + nconf.get('mongo:database');
mongoClient.connect(connString, connOptions, function (err, _db) {
if (err) {
winston.error('NodeBB could not connect to your Mongo database. Mongo returned the following error: ' + err.message);
return callback(err);
}
db = _db;
module.client = db;
require('./mongo/main')(db, module);
require('./mongo/hash')(db, module);
require('./mongo/sets')(db, module);
require('./mongo/sorted')(db, module);
require('./mongo/list')(db, module);
if (nconf.get('mongo:password') && nconf.get('mongo:username')) {
db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) {
callback(err);
});
} else {
winston.warn('You have no mongo password setup!');
callback();
}
});
var connOptions = {
poolSize: 10,
reconnectTries: 3600,
reconnectInterval: 1000,
autoReconnect: true,
};
module.initSessionStore = function (callback) {
var meta = require('../meta');
var sessionStore;
connOptions = _.deepExtend(connOptions, nconf.get('mongo:options') || {});
var ttl = meta.getSessionTTLSeconds();
mongoClient.connect(connString, connOptions, function (err, _db) {
if (err) {
winston.error('NodeBB could not connect to your Mongo database. Mongo returned the following error: ' + err.message);
return callback(err);
}
if (nconf.get('redis')) {
sessionStore = require('connect-redis')(session);
var rdb = require('./redis');
rdb.client = rdb.connect();
db = _db;
module.sessionStore = new sessionStore({
client: rdb.client,
ttl: ttl,
mongoModule.client = db;
require('./mongo/main')(db, mongoModule);
require('./mongo/hash')(db, mongoModule);
require('./mongo/sets')(db, mongoModule);
require('./mongo/sorted')(db, mongoModule);
require('./mongo/list')(db, mongoModule);
if (nconf.get('mongo:password') && nconf.get('mongo:username')) {
db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) {
callback(err);
});
} else if (nconf.get('mongo')) {
sessionStore = require('connect-mongo')(session);
module.sessionStore = new sessionStore({
db: db,
ttl: ttl,
});
}
callback();
};
module.createIndices = function (callback) {
function createIndex(collection, index, options, callback) {
module.client.collection(collection).createIndex(index, options, callback);
}
if (!module.client) {
winston.warn('[database/createIndices] database not initialized');
return callback();
}
winston.info('[database] Checking database indices.');
async.series([
async.apply(createIndex, 'objects', { _key: 1, score: -1 }, { background: true }),
async.apply(createIndex, 'objects', { _key: 1, value: -1 }, { background: true, unique: true, sparse: true }),
async.apply(createIndex, 'objects', { expireAt: 1 }, { expireAfterSeconds: 0, background: true }),
], function (err) {
if (err) {
winston.error('Error creating index ' + err.message);
return callback(err);
}
winston.info('[database] Checking database indices done!');
} else {
winston.warn('You have no mongo password setup!');
callback();
});
};
module.checkCompatibility = function (callback) {
var mongoPkg = require('mongodb/package.json');
if (semver.lt(mongoPkg.version, '2.0.0')) {
return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.'));
}
});
};
mongoModule.initSessionStore = function (callback) {
var meta = require('../meta');
var sessionStore;
var ttl = meta.getSessionTTLSeconds();
if (nconf.get('redis')) {
sessionStore = require('connect-redis')(session);
var rdb = require('./redis');
rdb.client = rdb.connect();
mongoModule.sessionStore = new sessionStore({
client: rdb.client,
ttl: ttl,
});
} else if (nconf.get('mongo')) {
sessionStore = require('connect-mongo')(session);
mongoModule.sessionStore = new sessionStore({
db: db,
ttl: ttl,
});
}
callback();
};
mongoModule.createIndices = function (callback) {
function createIndex(collection, index, options, callback) {
mongoModule.client.collection(collection).createIndex(index, options, callback);
}
if (!mongoModule.client) {
winston.warn('[database/createIndices] database not initialized');
return callback();
}
winston.info('[database] Checking database indices.');
async.series([
async.apply(createIndex, 'objects', { _key: 1, score: -1 }, { background: true }),
async.apply(createIndex, 'objects', { _key: 1, value: -1 }, { background: true, unique: true, sparse: true }),
async.apply(createIndex, 'objects', { expireAt: 1 }, { expireAfterSeconds: 0, background: true }),
], function (err) {
if (err) {
winston.error('Error creating index ' + err.message);
return callback(err);
}
winston.info('[database] Checking database indices done!');
callback();
};
});
};
module.info = function (db, callback) {
if (!db) {
return callback();
}
async.parallel({
serverStatus: function (next) {
db.command({ serverStatus: 1 }, next);
},
stats: function (next) {
db.command({ dbStats: 1 }, next);
},
listCollections: function (next) {
db.listCollections().toArray(function (err, items) {
if (err) {
return next(err);
}
async.map(items, function (collection, next) {
db.collection(collection.name).stats(next);
}, next);
});
},
}, function (err, results) {
if (err) {
return callback(err);
}
var stats = results.stats;
var scale = 1024 * 1024;
mongoModule.checkCompatibility = function (callback) {
var mongoPkg = require('mongodb/package.json');
results.listCollections = results.listCollections.map(function (collectionInfo) {
return {
name: collectionInfo.ns,
count: collectionInfo.count,
size: collectionInfo.size,
avgObjSize: collectionInfo.avgObjSize,
storageSize: collectionInfo.storageSize,
totalIndexSize: collectionInfo.totalIndexSize,
indexSizes: collectionInfo.indexSizes,
};
if (semver.lt(mongoPkg.version, '2.0.0')) {
return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.'));
}
callback();
};
mongoModule.info = function (db, callback) {
if (!db) {
return callback();
}
async.parallel({
serverStatus: function (next) {
db.command({ serverStatus: 1 }, next);
},
stats: function (next) {
db.command({ dbStats: 1 }, next);
},
listCollections: function (next) {
db.listCollections().toArray(function (err, items) {
if (err) {
return next(err);
}
async.map(items, function (collection, next) {
db.collection(collection.name).stats(next);
}, next);
});
},
}, function (err, results) {
if (err) {
return callback(err);
}
var stats = results.stats;
var scale = 1024 * 1024 * 1024;
stats.mem = results.serverStatus.mem;
stats.collectionData = results.listCollections;
stats.network = results.serverStatus.network;
stats.raw = JSON.stringify(stats, null, 4);
stats.avgObjSize = stats.avgObjSize.toFixed(2);
stats.dataSize = (stats.dataSize / scale).toFixed(2);
stats.storageSize = (stats.storageSize / scale).toFixed(2);
stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0;
stats.indexSize = (stats.indexSize / scale).toFixed(2);
stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1';
stats.host = results.serverStatus.host;
stats.version = results.serverStatus.version;
stats.uptime = results.serverStatus.uptime;
stats.mongo = true;
callback(null, stats);
results.listCollections = results.listCollections.map(function (collectionInfo) {
return {
name: collectionInfo.ns,
count: collectionInfo.count,
size: collectionInfo.size,
avgObjSize: collectionInfo.avgObjSize,
storageSize: collectionInfo.storageSize,
totalIndexSize: collectionInfo.totalIndexSize,
indexSizes: collectionInfo.indexSizes,
};
});
};
module.close = function () {
db.close();
};
}(exports));
stats.mem = results.serverStatus.mem;
stats.mem = results.serverStatus.mem;
stats.mem.resident = (stats.mem.resident / 1024).toFixed(2);
stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2);
stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2);
stats.collectionData = results.listCollections;
stats.network = results.serverStatus.network;
stats.raw = JSON.stringify(stats, null, 4);
stats.avgObjSize = stats.avgObjSize.toFixed(2);
stats.dataSize = (stats.dataSize / scale).toFixed(2);
stats.storageSize = (stats.storageSize / scale).toFixed(2);
stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0;
stats.indexSize = (stats.indexSize / scale).toFixed(2);
stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1';
stats.host = results.serverStatus.host;
stats.version = results.serverStatus.version;
stats.uptime = results.serverStatus.uptime;
stats.mongo = true;
callback(null, stats);
});
};
mongoModule.close = function () {
db.close();
};

View File

@@ -8,7 +8,9 @@ module.exports = function (db, module) {
if (!key || !data) {
return callback();
}
if (data.hasOwnProperty('')) {
delete data[''];
}
db.collection('objects').update({ _key: key }, { $set: data }, { upsert: true, w: 1 }, function (err) {
callback(err);
});

View File

@@ -241,7 +241,7 @@ module.exports = function (db, module) {
module.sortedSetScore = function (key, value, callback) {
if (!key) {
return callback();
return callback(null, null);
}
value = helpers.valueToString(value);
db.collection('objects').findOne({ _key: key, value: value }, { fields: { _id: 0, score: 1 } }, function (err, result) {
@@ -274,7 +274,7 @@ module.exports = function (db, module) {
module.sortedSetScores = function (key, values, callback) {
if (!key) {
return callback();
return callback(null, null);
}
values = values.map(helpers.valueToString);
db.collection('objects').find({ _key: key, value: { $in: values } }, { _id: 0, value: 1, score: 1 }).toArray(function (err, result) {

View File

@@ -1,166 +1,164 @@
'use strict';
(function (module) {
var winston = require('winston');
var nconf = require('nconf');
var semver = require('semver');
var session = require('express-session');
var redis;
var redisClient;
var _ = require('underscore');
var winston = require('winston');
var nconf = require('nconf');
var semver = require('semver');
var session = require('express-session');
var redis = require('redis');
var redisClient;
module.questions = [
{
name: 'redis:host',
description: 'Host IP or address of your Redis instance',
default: nconf.get('redis:host') || '127.0.0.1',
},
{
name: 'redis:port',
description: 'Host port of your Redis instance',
default: nconf.get('redis:port') || 6379,
},
{
name: 'redis:password',
description: 'Password of your Redis database',
hidden: true,
default: nconf.get('redis:password') || '',
before: function (value) { value = value || nconf.get('redis:password') || ''; return value; },
},
{
name: 'redis:database',
description: 'Which database to use (0..n)',
default: nconf.get('redis:database') || 0,
},
];
_.mixin(require('underscore.deep'));
module.init = function (callback) {
try {
redis = require('redis');
} catch (err) {
winston.error('Unable to initialize Redis! Is Redis installed? Error :' + err.message);
process.exit();
}
var redisModule = module.exports;
redisClient = module.connect();
redisModule.questions = [
{
name: 'redis:host',
description: 'Host IP or address of your Redis instance',
default: nconf.get('redis:host') || '127.0.0.1',
},
{
name: 'redis:port',
description: 'Host port of your Redis instance',
default: nconf.get('redis:port') || 6379,
},
{
name: 'redis:password',
description: 'Password of your Redis database',
hidden: true,
default: nconf.get('redis:password') || '',
before: function (value) { value = value || nconf.get('redis:password') || ''; return value; },
},
{
name: 'redis:database',
description: 'Which database to use (0..n)',
default: nconf.get('redis:database') || 0,
},
];
module.client = redisClient;
redisModule.init = function (callback) {
redisClient = redisModule.connect();
require('./redis/main')(redisClient, module);
require('./redis/hash')(redisClient, module);
require('./redis/sets')(redisClient, module);
require('./redis/sorted')(redisClient, module);
require('./redis/list')(redisClient, module);
redisModule.client = redisClient;
if (typeof callback === 'function') {
callback();
}
};
require('./redis/main')(redisClient, redisModule);
require('./redis/hash')(redisClient, redisModule);
require('./redis/sets')(redisClient, redisModule);
require('./redis/sorted')(redisClient, redisModule);
require('./redis/list')(redisClient, redisModule);
module.initSessionStore = function (callback) {
var meta = require('../meta');
var sessionStore = require('connect-redis')(session);
if (typeof callback === 'function') {
callback();
}
};
module.sessionStore = new sessionStore({
client: module.client,
ttl: meta.getSessionTTLSeconds(),
});
redisModule.initSessionStore = function (callback) {
var meta = require('../meta');
var sessionStore = require('connect-redis')(session);
if (typeof callback === 'function') {
callback();
}
};
redisModule.sessionStore = new sessionStore({
client: redisModule.client,
ttl: meta.getSessionTTLSeconds(),
});
module.connect = function (options) {
var redis_socket_or_host = nconf.get('redis:host');
var cxn;
if (typeof callback === 'function') {
callback();
}
};
if (!redis) {
redis = require('redis');
}
redisModule.connect = function (options) {
var redis_socket_or_host = nconf.get('redis:host');
var cxn;
options = options || {};
if (nconf.get('redis:password')) {
options.auth_pass = nconf.get('redis:password');
}
if (!redis) {
redis = require('redis');
}
if (redis_socket_or_host && redis_socket_or_host.indexOf('/') >= 0) {
/* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */
cxn = redis.createClient(nconf.get('redis:host'), options);
} else {
/* Else, connect over tcp/ip */
cxn = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host'), options);
}
options = options || {};
cxn.on('error', function (err) {
winston.error(err.stack);
process.exit(1);
});
if (nconf.get('redis:password')) {
options.auth_pass = nconf.get('redis:password');
}
if (nconf.get('redis:password')) {
cxn.auth(nconf.get('redis:password'));
}
options = _.deepExtend(options, nconf.get('redis:options') || {});
var dbIdx = parseInt(nconf.get('redis:database'), 10);
if (dbIdx) {
cxn.select(dbIdx, function (error) {
if (error) {
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + error.message);
process.exit();
}
});
}
if (redis_socket_or_host && redis_socket_or_host.indexOf('/') >= 0) {
/* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */
cxn = redis.createClient(nconf.get('redis:host'), options);
} else {
/* Else, connect over tcp/ip */
cxn = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host'), options);
}
return cxn;
};
cxn.on('error', function (err) {
winston.error(err.stack);
process.exit(1);
});
module.createIndices = function (callback) {
setImmediate(callback);
};
if (nconf.get('redis:password')) {
cxn.auth(nconf.get('redis:password'));
}
module.checkCompatibility = function (callback) {
module.info(module.client, function (err, info) {
if (err) {
return callback(err);
var dbIdx = parseInt(nconf.get('redis:database'), 10);
if (dbIdx) {
cxn.select(dbIdx, function (error) {
if (error) {
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + error.message);
process.exit();
}
if (semver.lt(info.redis_version, '2.8.9')) {
return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
}
callback();
});
};
}
module.close = function () {
redisClient.quit();
};
return cxn;
};
module.info = function (cxn, callback) {
if (!cxn) {
return callback();
redisModule.createIndices = function (callback) {
setImmediate(callback);
};
redisModule.checkCompatibility = function (callback) {
redisModule.info(redisModule.client, function (err, info) {
if (err) {
return callback(err);
}
cxn.info(function (err, data) {
if (err) {
return callback(err);
if (semver.lt(info.redis_version, '2.8.9')) {
return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
}
callback();
});
};
redisModule.close = function () {
redisClient.quit();
};
redisModule.info = function (cxn, callback) {
if (!cxn) {
return callback();
}
cxn.info(function (err, data) {
if (err) {
return callback(err);
}
var lines = data.toString().split('\r\n').sort();
var redisData = {};
lines.forEach(function (line) {
var parts = line.split(':');
if (parts[1]) {
redisData[parts[0]] = parts[1];
}
var lines = data.toString().split('\r\n').sort();
var redisData = {};
lines.forEach(function (line) {
var parts = line.split(':');
if (parts[1]) {
redisData[parts[0]] = parts[1];
}
});
redisData.raw = JSON.stringify(redisData, null, 4);
redisData.redis = true;
callback(null, redisData);
});
};
redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2);
redisData.raw = JSON.stringify(redisData, null, 4);
redisData.redis = true;
module.helpers = module.helpers || {};
module.helpers.redis = require('./redis/helpers');
}(exports));
callback(null, redisData);
});
};
redisModule.helpers = redisModule.helpers || {};
redisModule.helpers.redis = require('./redis/helpers');

View File

@@ -8,11 +8,17 @@ module.exports = function (redisClient, module) {
if (!key || !data) {
return callback();
}
if (data.hasOwnProperty('')) {
delete data[''];
}
Object.keys(data).forEach(function (key) {
if (data[key] === undefined) {
delete data[key];
}
});
redisClient.hmset(key, data, function (err) {
callback(err);
});

View File

@@ -1,6 +1,6 @@
'use strict';
var helpers = {};
var helpers = module.exports;
helpers.multiKeys = function (redisClient, command, keys, callback) {
callback = callback || function () {};
@@ -15,7 +15,7 @@ helpers.multiKeysValue = function (redisClient, command, keys, value, callback)
callback = callback || function () {};
var multi = redisClient.multi();
for (var i = 0; i < keys.length; i += 1) {
multi[command](keys[i], value);
multi[command](String(keys[i]), String(value));
}
multi.exec(callback);
};
@@ -24,7 +24,7 @@ helpers.multiKeyValues = function (redisClient, command, key, values, callback)
callback = callback || function () {};
var multi = redisClient.multi();
for (var i = 0; i < values.length; i += 1) {
multi[command](key, values[i]);
multi[command](String(key), String(values[i]));
}
multi.exec(callback);
};
@@ -35,5 +35,3 @@ helpers.resultsToBool = function (results) {
}
return results;
};
module.exports = helpers;

View File

@@ -124,8 +124,12 @@ module.exports = function (redisClient, module) {
};
module.sortedSetScore = function (key, value, callback) {
if (!key || value === undefined) {
return callback(null, null);
}
redisClient.zscore(key, value, function (err, score) {
callback(err, !err ? parseFloat(score) : undefined);
callback(err, !err ? parseFloat(score) : null);
});
};

View File

@@ -3,6 +3,9 @@
module.exports = function (redisClient, module) {
module.sortedSetAdd = function (key, score, value, callback) {
callback = callback || function () {};
if (!key) {
return setImmediate(callback);
}
if (Array.isArray(score) && Array.isArray(value)) {
return sortedSetAddMulti(key, score, value, callback);
}
@@ -33,10 +36,15 @@ module.exports = function (redisClient, module) {
module.sortedSetsAdd = function (keys, score, value, callback) {
callback = callback || function () {};
if (!Array.isArray(keys) || !keys.length) {
return callback();
}
var multi = redisClient.multi();
for (var i = 0; i < keys.length; i += 1) {
multi.zadd(keys[i], score, value);
if (keys[i]) {
multi.zadd(keys[i], score, value);
}
}
multi.exec(function (err) {

View File

@@ -81,7 +81,7 @@ file.allowedExtensions = function () {
if (!extension.startsWith('.')) {
extension = '.' + extension;
}
return extension;
return extension.toLowerCase();
});
if (allowedExtensions.indexOf('.jpg') !== -1 && allowedExtensions.indexOf('.jpeg') === -1) {
@@ -92,20 +92,28 @@ file.allowedExtensions = function () {
};
file.exists = function (path, callback) {
fs.stat(path, function (err, stat) {
callback(!err && stat);
fs.stat(path, function (err) {
if (err) {
if (err.code === 'ENOENT') {
return callback(null, false);
}
return callback(err);
}
return callback(null, true);
});
};
file.existsSync = function (path) {
var exists = false;
try {
exists = fs.statSync(path);
fs.statSync(path);
} catch (err) {
exists = false;
if (err.code === 'ENOENT') {
return false;
}
throw err;
}
return !!exists;
return true;
};
file.link = function link(filePath, destPath, cb) {

686
src/flags.js Normal file
View File

@@ -0,0 +1,686 @@
'use strict';
var async = require('async');
var db = require('./database');
var user = require('./user');
var groups = require('./groups');
var meta = require('./meta');
var notifications = require('./notifications');
var analytics = require('./analytics');
var topics = require('./topics');
var posts = require('./posts');
var privileges = require('./privileges');
var plugins = require('./plugins');
var utils = require('../public/src/utils');
var _ = require('underscore');
var S = require('string');
var Flags = {};
Flags.get = function (flagId, callback) {
async.waterfall([
// First stage
async.apply(async.parallel, {
base: async.apply(db.getObject.bind(db), 'flag:' + flagId),
history: async.apply(Flags.getHistory, flagId),
notes: async.apply(Flags.getNotes, flagId),
}),
function (data, next) {
// Second stage
async.parallel({
userObj: async.apply(user.getUserFields, data.base.uid, ['username', 'userslug', 'picture']),
targetObj: async.apply(Flags.getTarget, data.base.type, data.base.targetId, data.base.uid),
}, function (err, payload) {
// Final object return construction
next(err, Object.assign(data.base, {
datetimeISO: new Date(parseInt(data.base.datetime, 10)).toISOString(),
target_readable: data.base.type.charAt(0).toUpperCase() + data.base.type.slice(1) + ' ' + data.base.targetId,
target: payload.targetObj,
history: data.history,
notes: data.notes,
reporter: payload.userObj,
}));
});
},
], callback);
};
Flags.list = function (filters, uid, callback) {
if (typeof filters === 'function' && !uid && !callback) {
callback = filters;
filters = {};
}
var sets = [];
var orSets = [];
var prepareSets = function (setPrefix, value) {
if (!Array.isArray(value)) {
sets.push(setPrefix + value);
} else if (value.length) {
value.forEach(function (x) {
orSets.push(setPrefix + x);
});
}
};
if (Object.keys(filters).length > 0) {
for (var type in filters) {
if (filters.hasOwnProperty(type)) {
switch (type) {
case 'type':
prepareSets('flags:byType:', filters[type]);
break;
case 'state':
prepareSets('flags:byState:', filters[type]);
break;
case 'reporterId':
prepareSets('flags:byReporter:', filters[type]);
break;
case 'assignee':
prepareSets('flags:byAssignee:', filters[type]);
break;
case 'targetUid':
prepareSets('flags:byTargetUid:', filters[type]);
break;
case 'cid':
prepareSets('flags:byCid:', filters[type]);
break;
case 'quick':
switch (filters.quick) {
case 'mine':
sets.push('flags:byAssignee:' + uid);
break;
}
break;
}
}
}
}
sets = (sets.length || orSets.length) ? sets : ['flags:datetime']; // No filter default
async.waterfall([
function (next) {
if (sets.length === 1) {
db.getSortedSetRevRange(sets[0], 0, -1, next);
} else if (sets.length > 1) {
db.getSortedSetRevIntersect({ sets: sets, start: 0, stop: -1, aggregate: 'MAX' }, next);
} else {
next(null, []);
}
},
function (flagIds, next) {
// Find flags according to "or" rules, if any
if (orSets.length) {
db.getSortedSetRevUnion({ sets: orSets, start: 0, stop: -1, aggregate: 'MAX' }, function (err, _flagIds) {
if (err) {
return next(err);
}
if (sets.length) {
// If flag ids are already present, return a subset of flags that are in both sets
next(null, _.intersection(flagIds, _flagIds));
} else {
// Otherwise, return all flags returned via orSets
next(null, _.union(flagIds, _flagIds));
}
});
} else {
setImmediate(next, null, flagIds);
}
},
function (flagIds, next) {
async.map(flagIds, function (flagId, next) {
async.waterfall([
async.apply(db.getObject, 'flag:' + flagId),
function (flagObj, next) {
user.getUserFields(flagObj.uid, ['username', 'picture'], function (err, userObj) {
next(err, Object.assign(flagObj, {
reporter: {
username: userObj.username,
picture: userObj.picture,
'icon:bgColor': userObj['icon:bgColor'],
'icon:text': userObj['icon:text'],
},
}));
});
},
], function (err, flagObj) {
if (err) {
return next(err);
}
switch (flagObj.state) {
case 'open':
flagObj.labelClass = 'info';
break;
case 'wip':
flagObj.labelClass = 'warning';
break;
case 'resolved':
flagObj.labelClass = 'success';
break;
case 'rejected':
flagObj.labelClass = 'danger';
break;
}
next(null, Object.assign(flagObj, {
target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId,
datetimeISO: new Date(parseInt(flagObj.datetime, 10)).toISOString(),
}));
});
}, next);
},
], callback);
};
Flags.validate = function (payload, callback) {
async.parallel({
targetExists: async.apply(Flags.targetExists, payload.type, payload.id),
target: async.apply(Flags.getTarget, payload.type, payload.id, payload.uid),
reporter: async.apply(user.getUserData, payload.uid),
}, function (err, data) {
if (err) {
return callback(err);
}
if (data.target.deleted) {
return callback(new Error('[[error:post-deleted]]'));
} else if (parseInt(data.reporter.banned, 10)) {
return callback(new Error('[[error:user-banned]]'));
}
switch (payload.type) {
case 'post':
privileges.posts.canEdit(payload.id, payload.uid, function (err, editable) {
if (err) {
return callback(err);
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
// Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
if (!editable.flag && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
}
callback();
});
break;
case 'user':
privileges.users.canEdit(payload.uid, payload.id, function (err, editable) {
if (err) {
return callback(err);
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
// Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply)
if (!editable && parseInt(data.reporter.reputation, 10) < minimumReputation) {
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
}
callback();
});
break;
default:
callback(new Error('[[error:invalid-data]]'));
break;
}
});
};
Flags.getNotes = function (flagId, callback) {
async.waterfall([
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':notes', 0, -1),
function (notes, next) {
var uids = [];
var noteObj;
notes = notes.map(function (note) {
try {
noteObj = JSON.parse(note.value);
uids.push(noteObj[0]);
return {
uid: noteObj[0],
content: noteObj[1],
datetime: note.score,
datetimeISO: new Date(parseInt(note.score, 10)).toISOString(),
};
} catch (e) {
return next(e);
}
});
next(null, notes, uids);
},
function (notes, uids, next) {
user.getUsersFields(uids, ['username', 'userslug', 'picture'], function (err, users) {
if (err) {
return next(err);
}
next(null, notes.map(function (note, idx) {
note.user = users[idx];
return note;
}));
});
},
], callback);
};
Flags.create = function (type, id, uid, reason, timestamp, callback) {
var targetUid;
var targetCid;
var doHistoryAppend = false;
// timestamp is optional
if (typeof timestamp === 'function' && !callback) {
callback = timestamp;
timestamp = Date.now();
doHistoryAppend = true;
}
async.waterfall([
function (next) {
async.parallel([
// Sanity checks
async.apply(Flags.exists, type, id, uid),
async.apply(Flags.targetExists, type, id),
// Extra data for zset insertion
async.apply(Flags.getTargetUid, type, id),
async.apply(Flags.getTargetCid, type, id),
], function (err, checks) {
if (err) {
return next(err);
}
targetUid = checks[2] || null;
targetCid = checks[3] || null;
if (checks[0]) {
return next(new Error('[[error:already-flagged]]'));
} else if (!checks[1]) {
return next(new Error('[[error:invalid-data]]'));
}
next();
});
},
async.apply(db.incrObjectField, 'global', 'nextFlagId'),
function (flagId, next) {
var tasks = [
async.apply(db.setObject.bind(db), 'flag:' + flagId, {
flagId: flagId,
type: type,
targetId: id,
description: reason,
uid: uid,
datetime: timestamp,
}),
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', timestamp, flagId), // by time, the default
async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, timestamp, flagId), // by reporter
async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, timestamp, flagId), // by flag type
async.apply(db.sortedSetAdd.bind(db), 'flags:hash', flagId, [type, id, uid].join(':')), // save zset for duplicate checking
async.apply(analytics.increment, 'flags'), // some fancy analytics
];
if (targetUid) {
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid
}
if (targetCid) {
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byCid:' + targetCid, timestamp, flagId)); // by target cid
}
if (type === 'post') {
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byPid:' + id, timestamp, flagId)); // by target pid
if (targetUid) {
tasks.push(async.apply(db.sortedSetIncrBy.bind(db), 'users:flags', 1, targetUid));
}
}
async.parallel(tasks, function (err) {
if (err) {
return next(err);
}
if (doHistoryAppend) {
Flags.update(flagId, uid, { state: 'open' });
}
next(null, flagId);
});
},
async.apply(Flags.get),
], callback);
};
Flags.exists = function (type, id, uid, callback) {
db.isSortedSetMember('flags:hash', [type, id, uid].join(':'), callback);
};
Flags.getTarget = function (type, id, uid, callback) {
async.waterfall([
async.apply(Flags.targetExists, type, id),
function (exists, next) {
if (exists) {
switch (type) {
case 'post':
async.waterfall([
async.apply(posts.getPostsByPids, [id], uid),
function (posts, next) {
topics.addPostData(posts, uid, next);
},
], function (err, posts) {
next(err, posts[0]);
});
break;
case 'user':
user.getUsersData([id], function (err, users) {
next(err, users ? users[0] : undefined);
});
break;
default:
next(new Error('[[error:invalid-data]]'));
break;
}
} else {
// Target used to exist (otherwise flag creation'd fail), but no longer
next(null, {});
}
},
], callback);
};
Flags.targetExists = function (type, id, callback) {
switch (type) {
case 'post':
posts.exists(id, callback);
break;
case 'user':
user.exists(id, callback);
break;
default:
callback(new Error('[[error:invalid-data]]'));
break;
}
};
Flags.getTargetUid = function (type, id, callback) {
switch (type) {
case 'post':
posts.getPostField(id, 'uid', callback);
break;
default:
setImmediate(callback, null, id);
break;
}
};
Flags.getTargetCid = function (type, id, callback) {
switch (type) {
case 'post':
posts.getCidByPid(id, callback);
break;
default:
setImmediate(callback, null, id);
break;
}
};
Flags.update = function (flagId, uid, changeset, callback) {
// Retrieve existing flag data to compare for history-saving purposes
var fields = ['state', 'assignee'];
var tasks = [];
var now = changeset.datetime || Date.now();
var notifyAssignee = function (assigneeId, next) {
if (assigneeId === '') {
// Do nothing
return next();
}
// Notify assignee of this update
notifications.create({
type: 'my-flags',
bodyShort: '[[notifications:flag_assigned_to_you, ' + flagId + ']]',
bodyLong: '',
path: '/flags/' + flagId,
nid: 'flags:assign:' + flagId + ':uid:' + assigneeId,
from: uid,
}, function (err, notification) {
if (err || !notification) {
return next(err);
}
notifications.push(notification, [assigneeId], next);
});
};
async.waterfall([
async.apply(db.getObjectFields.bind(db), 'flag:' + flagId, fields),
function (current, next) {
for (var prop in changeset) {
if (changeset.hasOwnProperty(prop)) {
if (current[prop] === changeset[prop]) {
delete changeset[prop];
} else {
// Add tasks as necessary
switch (prop) {
case 'state':
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byState:' + changeset[prop], now, flagId));
tasks.push(async.apply(db.sortedSetRemove.bind(db), 'flags:byState:' + current[prop], flagId));
break;
case 'assignee':
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byAssignee:' + changeset[prop], now, flagId));
tasks.push(async.apply(notifyAssignee, changeset[prop]));
break;
}
}
}
}
if (!Object.keys(changeset).length) {
// No changes
return next();
}
// Save new object to db (upsert)
tasks.push(async.apply(db.setObject, 'flag:' + flagId, changeset));
// Append history
tasks.push(async.apply(Flags.appendHistory, flagId, uid, changeset));
// Fire plugin hook
tasks.push(async.apply(plugins.fireHook, 'action:flag.update', { flagId: flagId, changeset: changeset, uid: uid }));
async.parallel(tasks, function (err) {
return next(err);
});
},
], callback);
};
Flags.getHistory = function (flagId, callback) {
var history;
var uids = [];
async.waterfall([
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':history', 0, -1),
function (_history, next) {
history = _history.map(function (entry) {
try {
entry.value = JSON.parse(entry.value);
} catch (e) {
return callback(e);
}
uids.push(entry.value[0]);
// Deserialise changeset
var changeset = entry.value[1];
if (changeset.hasOwnProperty('state')) {
changeset.state = changeset.state === undefined ? '' : '[[flags:state-' + changeset.state + ']]';
}
return {
uid: entry.value[0],
fields: changeset,
datetime: entry.score,
datetimeISO: new Date(parseInt(entry.score, 10)).toISOString(),
};
});
user.getUsersFields(uids, ['username', 'userslug', 'picture'], next);
},
], function (err, users) {
if (err) {
return callback(err);
}
// Append user data to each history event
history = history.map(function (event, idx) {
event.user = users[idx];
return event;
});
callback(null, history);
});
};
Flags.appendHistory = function (flagId, uid, changeset, callback) {
var payload;
var datetime = changeset.datetime || Date.now();
delete changeset.datetime;
try {
payload = JSON.stringify([uid, changeset, datetime]);
} catch (e) {
return callback(e);
}
db.sortedSetAdd('flag:' + flagId + ':history', datetime, payload, callback);
};
Flags.appendNote = function (flagId, uid, note, datetime, callback) {
if (typeof datetime === 'function' && !callback) {
callback = datetime;
datetime = Date.now();
}
var payload;
try {
payload = JSON.stringify([uid, note]);
} catch (e) {
return callback(e);
}
async.waterfall([
async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', datetime, payload),
async.apply(Flags.appendHistory, flagId, uid, {
notes: null,
datetime: datetime,
}),
], callback);
};
Flags.notify = function (flagObj, uid, callback) {
// Notify administrators, mods, and other associated people
if (!callback) {
callback = function () {};
}
switch (flagObj.type) {
case 'post':
async.parallel({
post: function (next) {
async.waterfall([
async.apply(posts.getPostData, flagObj.targetId),
async.apply(posts.parsePost),
], next);
},
title: async.apply(topics.getTitleByPid, flagObj.targetId),
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
moderators: function (next) {
async.waterfall([
async.apply(posts.getCidByPid, flagObj.targetId),
function (cid, next) {
groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, next);
},
], next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
var title = S(results.title).decodeHTMLEntities().s;
var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
notifications.create({
type: 'new-post-flag',
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]',
bodyLong: flagObj.description,
pid: flagObj.targetId,
path: '/post/' + flagObj.targetId,
nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid,
from: uid,
mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId,
topicTitle: results.title,
}, function (err, notification) {
if (err || !notification) {
return callback(err);
}
plugins.fireHook('action:flag.create', {
flag: flagObj,
});
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), callback);
});
});
break;
case 'user':
async.parallel({
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
}, function (err, results) {
if (err) {
return callback(err);
}
notifications.create({
bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reporter.username + ', ' + flagObj.target.username + ']]',
bodyLong: flagObj.description,
path: '/uid/' + flagObj.targetId,
nid: 'flag:user:' + flagObj.targetId + ':uid:' + uid,
from: uid,
mergeId: 'notifications:user_flagged_user|' + flagObj.targetId,
}, function (err, notification) {
if (err || !notification) {
return callback(err);
}
plugins.fireHook('action:flag.create', {
flag: flagObj,
});
notifications.push(notification, results.admins.concat(results.globalMods), callback);
});
});
break;
default:
callback(new Error('[[error:invalid-data]]'));
break;
}
};
module.exports = Flags;

View File

@@ -2,6 +2,7 @@
var async = require('async');
var fs = require('fs');
var path = require('path');
var Jimp = require('jimp');
var mime = require('mime');
var winston = require('winston');
@@ -37,8 +38,9 @@ module.exports = function (Groups) {
},
function (_tempPath, next) {
tempPath = _tempPath;
uploadsController.uploadGroupCover(uid, {
name: 'groupCover',
name: 'groupCover' + path.extname(tempPath),
path: tempPath,
type: type,
}, next);
@@ -52,7 +54,7 @@ module.exports = function (Groups) {
},
function (next) {
uploadsController.uploadGroupCover(uid, {
name: 'groupCoverThumb',
name: 'groupCoverThumb' + path.extname(tempPath),
path: tempPath,
type: type,
}, next);

View File

@@ -69,7 +69,7 @@ module.exports = function (Groups) {
async.series(tasks, next);
},
function (results, next) {
plugins.fireHook('action:group.create', groupData);
plugins.fireHook('action:group.create', { group: groupData });
next(null, groupData);
},
], callback);

View File

@@ -4,6 +4,7 @@ var async = require('async');
var plugins = require('../plugins');
var utils = require('../utils');
var db = require('./../database');
var batch = require('../batch');
module.exports = function (Groups) {
Groups.destroy = function (groupName, callback) {
@@ -16,8 +17,6 @@ module.exports = function (Groups) {
}
var groupObj = groupsData[0];
plugins.fireHook('action:group.destroy', groupObj);
async.parallel([
async.apply(db.delete, 'group:' + groupName),
async.apply(db.sortedSetRemove, 'groups:createtime', groupName),
@@ -28,22 +27,24 @@ module.exports = function (Groups) {
async.apply(db.delete, 'group:' + groupName + ':pending'),
async.apply(db.delete, 'group:' + groupName + ':invited'),
async.apply(db.delete, 'group:' + groupName + ':owners'),
async.apply(db.delete, 'group:' + groupName + ':member:pids'),
async.apply(db.deleteObjectField, 'groupslug:groupname', utils.slugify(groupName)),
function (next) {
db.getSortedSetRange('groups:createtime', 0, -1, function (err, groups) {
if (err) {
return next(err);
}
async.each(groups, function (group, next) {
db.sortedSetRemove('group:' + group + ':members', groupName, next);
}, next);
});
batch.processSortedSet('groups:createtime', function (groupNames, next) {
var keys = groupNames.map(function (group) {
return 'group:' + group + ':members';
});
db.sortedSetsRemove(keys, groupName, next);
}, {
batch: 500,
}, next);
},
], function (err) {
if (err) {
return callback(err);
}
Groups.resetCache();
plugins.fireHook('action:group.destroy', { group: groupObj });
callback();
});
});

View File

@@ -7,21 +7,61 @@ var privileges = require('../privileges');
var posts = require('../posts');
module.exports = function (Groups) {
Groups.onNewPostMade = function (postData, callback) {
if (!parseInt(postData.uid, 10)) {
return setImmediate(callback);
}
var groupNames;
async.waterfall([
function (next) {
Groups.getUserGroupMembership('groups:visible:createtime', [postData.uid], next);
},
function (_groupNames, next) {
groupNames = _groupNames[0];
var keys = groupNames.map(function (groupName) {
return 'group:' + groupName + ':member:pids';
});
db.sortedSetsAdd(keys, postData.timestamp, postData.pid, next);
},
function (next) {
async.each(groupNames, function (groupName, next) {
truncateMemberPosts(groupName, next);
}, next);
},
], callback);
};
function truncateMemberPosts(groupName, callback) {
async.waterfall([
function (next) {
db.getSortedSetRevRange('group:' + groupName + ':member:pids', 10, 10, next);
},
function (lastPid, next) {
lastPid = lastPid[0];
if (!parseInt(lastPid, 10)) {
return callback();
}
db.sortedSetScore('group:' + groupName + ':member:pids', lastPid, next);
},
function (score, next) {
db.sortedSetsRemoveRangeByScore(['group:' + groupName + ':member:pids'], '-inf', score, next);
},
], callback);
}
Groups.getLatestMemberPosts = function (groupName, max, uid, callback) {
async.waterfall([
function (next) {
Groups.getMembers(groupName, 0, -1, next);
},
function (uids, next) {
if (!Array.isArray(uids) || !uids.length) {
return callback(null, []);
}
var keys = uids.map(function (uid) {
return 'uid:' + uid + ':posts';
});
db.getSortedSetRevRange(keys, 0, max - 1, next);
db.getSortedSetRevRange('group:' + groupName + ':member:pids', 0, max - 1, next);
},
function (pids, next) {
if (!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
privileges.posts.filter('read', pids, uid, next);
},
function (pids, next) {

View File

@@ -22,6 +22,19 @@ module.exports = function (Groups) {
};
Groups.getUserGroupsFromSet = function (set, uids, callback) {
async.waterfall([
function (next) {
Groups.getUserGroupMembership(set, uids, next);
},
function (memberOf, next) {
async.map(memberOf, function (memberOf, next) {
Groups.getGroupsData(memberOf, next);
}, next);
},
], callback);
};
Groups.getUserGroupMembership = function (set, uids, callback) {
async.waterfall([
function (next) {
db.getSortedSetRevRange(set, 0, -1, next);
@@ -40,7 +53,7 @@ module.exports = function (Groups) {
}
});
Groups.getGroupsData(memberOf, next);
next(null, memberOf);
},
], next);
}, next);

View File

@@ -507,12 +507,11 @@ install.setup = function (callback) {
setCopyrightWidget,
function (next) {
var upgrade = require('./upgrade');
upgrade.check(function (err, uptodate) {
if (err) {
upgrade.check(function (err) {
if (err && err.message === 'schema-out-of-date') {
upgrade.run(next);
} else if (err) {
return next(err);
}
if (!uptodate) {
upgrade.upgrade(next);
} else {
next();
}

View File

@@ -89,7 +89,7 @@ Languages.list = function (callback) {
// filter out invalid ones
languages = languages.filter(function (lang) {
return lang.code && lang.name && lang.dir;
return lang && lang.code && lang.name && lang.dir;
});
listCache = languages;

View File

@@ -102,7 +102,7 @@ Messaging.isNewSet = function (uid, roomId, timestamp, callback) {
},
function (messages, next) {
if (messages && messages.length) {
next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + (1000 * 60 * 5));
next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + Messaging.newMessageCutoff);
} else {
next(null, true);
}

View File

@@ -30,9 +30,11 @@ module.exports = function (Messaging) {
if (!content) {
return callback(new Error('[[error:invalid-chat-message]]'));
}
content = String(content);
if (content.length > (meta.config.maximumChatMessageLength || 1000)) {
return callback(new Error('[[error:chat-message-too-long]]'));
var maximumChatMessageLength = (meta.config.maximumChatMessageLength || 1000);
if (content.length > maximumChatMessageLength) {
return callback(new Error('[[error:chat-message-too-long, ' + maximumChatMessageLength + ']]'));
}
callback();
};
@@ -52,7 +54,7 @@ module.exports = function (Messaging) {
function (_mid, next) {
mid = _mid;
message = {
content: content,
content: String(content),
timestamp: timestamp,
fromuid: fromuid,
roomId: roomId,

View File

@@ -9,6 +9,8 @@ var utils = require('../utils');
var plugins = require('../plugins');
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);
@@ -81,7 +83,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) {
@@ -116,7 +118,7 @@ module.exports = function (Messaging) {
}
if (
(parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000 * 60 * 5)) ||
(parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + Messaging.newMessageCutoff) ||
(parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10))
) {
// If it's been 5 minutes, this is a new set of messages

View File

@@ -79,6 +79,7 @@ module.exports = function (Messaging) {
}
notifications.create({
type: 'new-chat',
bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
bodyLong: messageObj.content,
nid: 'chat_' + fromuid + '_' + roomId,

View File

@@ -19,14 +19,17 @@ module.exports = function (Meta) {
winston.verbose('Checking dependencies for outdated modules');
async.every(modules, function (module, next) {
async.each(modules, function (module, next) {
fs.readFile(path.join(__dirname, '../../node_modules/', module, 'package.json'), {
encoding: 'utf-8',
}, function (err, pkgData) {
// If a bundled plugin/theme is not present, skip the dep check (#3384)
if (err && err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
return next(true);
if (err) {
// If a bundled plugin/theme is not present, skip the dep check (#3384)
if (err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
return next();
}
return next(err);
}
try {
@@ -34,20 +37,24 @@ module.exports = function (Meta) {
} catch (e) {
process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n');
depsMissing = true;
return next(true);
return next();
}
var ok = !semver.validRange(pkg.dependencies[module]) || semver.satisfies(pkgData.version, pkg.dependencies[module]);
if (ok || (pkgData._resolved && pkgData._resolved.indexOf('//github.com') !== -1)) {
next(true);
next();
} else {
process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n');
depsOutdated = true;
next(true);
next();
}
});
}, function () {
}, function (err) {
if (err) {
return callback(err);
}
if (depsMissing) {
callback(new Error('dependencies-missing'));
} else if (depsOutdated) {

View File

@@ -1,7 +1,9 @@
'use strict';
var async = require('async');
var winston = require('winston');
var validator = require('validator');
var cronJob = require('cron').CronJob;
var db = require('../database');
var analytics = require('../analytics');
@@ -9,17 +11,45 @@ var analytics = require('../analytics');
module.exports = function (Meta) {
Meta.errors = {};
var counters = {};
new cronJob('0 * * * * *', function () {
Meta.errors.writeData();
}, null, true);
Meta.errors.writeData = function () {
var dbQueue = [];
if (Object.keys(counters).length > 0) {
for (var key in counters) {
if (counters.hasOwnProperty(key)) {
dbQueue.push(async.apply(db.sortedSetIncrBy, 'errors:404', counters[key], key));
}
}
counters = {};
async.series(dbQueue, function (err) {
if (err) {
winston.error(err);
}
});
}
};
Meta.errors.log404 = function (route, callback) {
callback = callback || function () {};
if (!route) {
return setImmediate(callback);
}
route = route.replace(/\/$/, ''); // remove trailing slashes
analytics.increment('errors:404');
db.sortedSetIncrBy('errors:404', 1, route, callback);
counters[route] = counters[route] || 0;
counters[route] += 1;
setImmediate(callback);
};
Meta.errors.get = function (escape, callback) {
async.waterfall([
function (next) {
db.getSortedSetRevRangeWithScores('errors:404', 0, -1, next);
db.getSortedSetRevRangeWithScores('errors:404', 0, 199, next);
},
function (data, next) {
data = data.map(function (nfObject) {

View File

@@ -54,7 +54,6 @@ module.exports = function (Meta) {
'public/src/client/unread.js',
'public/src/client/topic.js',
'public/src/client/topic/events.js',
'public/src/client/topic/flag.js',
'public/src/client/topic/fork.js',
'public/src/client/topic/move.js',
'public/src/client/topic/posts.js',
@@ -78,6 +77,8 @@ module.exports = function (Meta) {
'public/src/modules/taskbar.js',
'public/src/modules/helpers.js',
'public/src/modules/string.js',
'public/src/modules/flags.js',
'public/src/modules/storage.js',
],
// modules listed below are built (/src/modules) so they can be defined anonymously

View File

@@ -27,9 +27,7 @@ function getTranslationTree(callback) {
});
// Filter out plugins with invalid paths
async.filter(paths, file.exists, function (paths) {
next(null, paths);
});
async.filter(paths, file.exists, next);
},
function (paths, next) {
async.map(paths, Plugins.loadPluginInfo, next);

View File

@@ -1,13 +1,12 @@
'use strict';
var path = require('path');
var nconf = require('nconf');
var fs = require('fs');
var winston = require('winston');
module.exports = function (Meta) {
Meta.logs = {
path: path.join(nconf.get('base_dir'), 'logs', 'output.log'),
path: path.join(__dirname, '..', '..', 'logs', 'output.log'),
};
Meta.logs.get = function (callback) {

View File

@@ -16,13 +16,21 @@ Minifier.js.minify = function (scripts, minify, callback) {
});
async.filter(scripts, function (script, next) {
file.exists(script, function (exists) {
file.exists(script, function (err, exists) {
if (err) {
return next(err);
}
if (!exists) {
console.warn('[minifier] file not found, ' + script);
}
next(exists);
next(null, exists);
});
}, function (scripts) {
}, function (err, scripts) {
if (err) {
return callback(err);
}
if (minify) {
minifyScripts(scripts, callback);
} else {

View File

@@ -9,7 +9,7 @@ var plugins = require('../plugins');
module.exports = function (Meta) {
Meta.tags = {};
Meta.tags.parse = function (meta, link, callback) {
Meta.tags.parse = function (req, meta, link, callback) {
async.parallel({
tags: function (next) {
var defaultTags = [{
@@ -120,7 +120,23 @@ module.exports = function (Meta) {
return tag;
});
addDescription(meta);
addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
var ogUrl = nconf.get('url') + req.path;
addIfNotExists(meta, 'property', 'og:url', ogUrl);
addIfNotExists(meta, 'name', 'description', Meta.config.description);
addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
var ogImage = Meta.config['og:image'] || Meta.config['brand:logo'] || '';
if (ogImage && !ogImage.startsWith('http')) {
ogImage = nconf.get('url') + ogImage;
}
addIfNotExists(meta, 'property', 'og:image', ogImage);
if (ogImage) {
addIfNotExists(meta, 'property', 'og:image:width', 200);
addIfNotExists(meta, 'property', 'og:image:height', 200);
}
link = results.links.concat(link || []);
@@ -131,19 +147,20 @@ module.exports = function (Meta) {
});
};
function addDescription(meta) {
var hasDescription = false;
function addIfNotExists(meta, keyName, tagName, value) {
var exists = false;
meta.forEach(function (tag) {
if (tag.name === 'description') {
hasDescription = true;
if (tag[keyName] === tagName) {
exists = true;
}
});
if (!hasDescription && Meta.config.description) {
meta.push({
name: 'description',
content: validator.escape(String(Meta.config.description)),
});
if (!exists && value) {
var data = {
content: validator.escape(String(value)),
};
data[keyName] = tagName;
meta.push(data);
}
}
};

View File

@@ -27,18 +27,28 @@ module.exports = function (Meta) {
async.filter(files, function (file, next) {
fs.stat(path.join(themePath, file), function (err, fileStat) {
if (err) {
return next(false);
if (err.code === 'ENOENT') {
return next(null, false);
}
return next(err);
}
next((fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
});
}, function (themes) {
}, function (err, themes) {
if (err) {
return callback(err);
}
async.map(themes, function (theme, next) {
var config = path.join(themePath, theme, 'theme.json');
fs.readFile(config, function (err, file) {
if (err) {
return next();
if (err.code === 'ENOENT') {
return next(null, null);
}
return next(err);
}
try {
var configObj = JSON.parse(file.toString());

View File

@@ -41,6 +41,7 @@ module.exports = function (middleware) {
middleware.renderHeader = function (req, res, data, callback) {
var registrationType = meta.config.registrationType || 'normal';
res.locals.config = res.locals.config || {};
var templateValues = {
title: meta.config.title || '',
description: meta.config.description || '',
@@ -97,7 +98,7 @@ module.exports = function (middleware) {
db.get('uid:' + req.uid + ':confirm:email:sent', next);
},
navigation: async.apply(navigation.get),
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags),
tags: async.apply(meta.tags.parse, req, res.locals.metaTags, res.locals.linkTags),
banned: async.apply(user.isBanned, req.uid),
banReason: async.apply(user.getBannedReason, req.uid),
}, next);
@@ -105,7 +106,7 @@ module.exports = function (middleware) {
function (results, next) {
if (results.banned) {
req.logout();
return res.redirect('/?banned=' + (results.banReason || 'no-reason'));
return res.redirect('/');
}
results.user.isAdmin = results.isAdmin;
@@ -133,6 +134,7 @@ module.exports = function (middleware) {
templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : '';
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
templateValues.defaultLang = meta.config.defaultLang || 'en-GB';
templateValues.userLang = res.locals.config.userLang;
templateValues.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1;
templateValues.privateTagListing = parseInt(meta.config.privateTagListing, 10) === 1;

View File

@@ -73,6 +73,29 @@ middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) {
], next);
};
middleware.ensureSelfOrPrivileged = 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 (parseInt(req.user.uid, 10) === parseInt(res.locals.uid, 10)) {
return next();
}
user.isPrivileged(req.uid, function (err, ok) {
if (err) {
return next(err);
} else if (ok) {
return next();
}
controllers.helpers.notAllowed(req, res);
});
} else {
controllers.helpers.notAllowed(req, res);
}
};
middleware.pageView = function (req, res, next) {
analytics.pageView({
ip: req.ip,

View File

@@ -15,42 +15,44 @@ var batch = require('./batch');
var plugins = require('./plugins');
var utils = require('./utils');
(function (Notifications) {
Notifications.init = function () {
winston.verbose('[notifications.init] Registering jobs.');
new cron('*/30 * * * *', Notifications.prune, null, true);
};
var Notifications = module.exports;
Notifications.get = function (nid, callback) {
Notifications.getMultiple([nid], function (err, notifications) {
callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null);
});
};
Notifications.startJobs = function () {
winston.verbose('[notifications.init] Registering jobs.');
new cron('*/30 * * * *', Notifications.prune, null, true);
};
Notifications.getMultiple = function (nids, callback) {
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
Notifications.get = function (nid, callback) {
Notifications.getMultiple([nid], function (err, notifications) {
callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null);
});
};
db.getObjects(keys, function (err, notifications) {
if (err) {
return callback(err);
}
Notifications.getMultiple = function (nids, callback) {
if (!nids.length) {
return setImmediate(callback, null, []);
}
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
notifications = notifications.filter(Boolean);
if (!notifications.length) {
return callback(null, []);
}
var notifications;
async.waterfall([
function (next) {
db.getObjects(keys, next);
},
function (_notifications, next) {
notifications = _notifications;
var userKeys = notifications.map(function (notification) {
return notification.from;
return notification && notification.from;
});
User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], function (err, usersData) {
if (err) {
return callback(err);
}
notifications.forEach(function (notification, index) {
User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], next);
},
function (usersData, next) {
notifications.forEach(function (notification, index) {
if (notification) {
notification.datetimeISO = utils.toISOString(notification.datetime);
if (notification.bodyLong) {
@@ -66,449 +68,449 @@ var utils = require('./utils');
} else if (notification.image === 'brand:logo' || !notification.image) {
notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png';
}
});
callback(null, notifications);
}
});
});
};
Notifications.filterExists = function (nids, callback) {
// Removes nids that have been pruned
db.isSortedSetMembers('notifications', nids, function (err, exists) {
if (err) {
return callback(err);
}
next(null, notifications);
},
], callback);
};
Notifications.filterExists = function (nids, callback) {
async.waterfall([
function (next) {
db.isSortedSetMembers('notifications', nids, next);
},
function (exists, next) {
nids = nids.filter(function (notifId, idx) {
return exists[idx];
});
callback(null, nids);
});
};
next(null, nids);
},
], callback);
};
Notifications.findRelated = function (mergeIds, set, callback) {
// A related notification is one in a zset that has the same mergeId
var _nids;
Notifications.findRelated = function (mergeIds, set, callback) {
// A related notification is one in a zset that has the same mergeId
var _nids;
async.waterfall([
async.apply(db.getSortedSetRevRange, set, 0, -1),
function (nids, next) {
_nids = nids;
async.waterfall([
async.apply(db.getSortedSetRevRange, set, 0, -1),
function (nids, next) {
_nids = nids;
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(keys, ['mergeId'], next);
},
], function (err, sets) {
if (err) {
return callback(err);
}
sets = sets.map(function (set) {
return set.mergeId;
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
callback(null, _nids.filter(function (nid, idx) {
return mergeIds.indexOf(sets[idx]) !== -1;
}));
});
};
Notifications.create = function (data, callback) {
if (!data.nid) {
return callback(new Error('no-notification-id'));
db.getObjectsFields(keys, ['mergeId'], next);
},
], function (err, sets) {
if (err) {
return callback(err);
}
data.importance = data.importance || 5;
db.getObject('notifications:' + data.nid, function (err, oldNotification) {
if (err) {
return callback(err);
sets = sets.map(function (set) {
return set.mergeId;
});
callback(null, _nids.filter(function (nid, idx) {
return mergeIds.indexOf(sets[idx]) !== -1;
}));
});
};
Notifications.create = function (data, callback) {
if (!data.nid) {
return callback(new Error('no-notification-id'));
}
data.importance = data.importance || 5;
db.getObject('notifications:' + data.nid, function (err, oldNotification) {
if (err) {
return callback(err);
}
if (oldNotification) {
if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
return callback(null, null);
}
}
if (oldNotification) {
if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
return callback(null, null);
}
}
var now = Date.now();
data.datetime = now;
async.parallel([
function (next) {
db.sortedSetAdd('notifications', now, data.nid, next);
},
function (next) {
db.setObject('notifications:' + data.nid, data, next);
},
], function (err) {
callback(err, data);
});
var now = Date.now();
data.datetime = now;
async.parallel([
function (next) {
db.sortedSetAdd('notifications', now, data.nid, next);
},
function (next) {
db.setObject('notifications:' + data.nid, data, next);
},
], function (err) {
callback(err, data);
});
};
});
};
Notifications.push = function (notification, uids, callback) {
callback = callback || function () {};
Notifications.push = function (notification, uids, callback) {
callback = callback || function () {};
if (!notification || !notification.nid) {
return callback();
}
if (!Array.isArray(uids)) {
uids = [uids];
}
uids = uids.filter(function (uid, index, array) {
return parseInt(uid, 10) && array.indexOf(uid) === index;
});
if (!uids.length) {
return callback();
}
setTimeout(function () {
batch.processArray(uids, function (uids, next) {
pushToUids(uids, notification, next);
}, { interval: 1000 }, function (err) {
if (err) {
winston.error(err.stack);
}
});
}, 1000);
callback();
};
function pushToUids(uids, notification, callback) {
var oneWeekAgo = Date.now() - 604800000;
var unreadKeys = [];
var readKeys = [];
async.waterfall([
function (next) {
plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next);
},
function (data, next) {
if (!data || !data.notification || !data.uids || !data.uids.length) {
return callback();
}
uids = data.uids;
notification = data.notification;
uids.forEach(function (uid) {
unreadKeys.push('uid:' + uid + ':notifications:unread');
readKeys.push('uid:' + uid + ':notifications:read');
});
db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next);
},
function (next) {
db.sortedSetsRemove(readKeys, notification.nid, next);
},
function (next) {
db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next);
},
function (next) {
db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next);
},
function (next) {
var websockets = require('./socket.io');
if (websockets.server) {
uids.forEach(function (uid) {
websockets.in('uid_' + uid).emit('event:new_notification', notification);
});
}
plugins.fireHook('action:notification.pushed', { notification: notification, uids: uids });
next();
},
], callback);
if (!notification || !notification.nid) {
return callback();
}
Notifications.pushGroup = function (notification, groupName, callback) {
callback = callback || function () {};
groups.getMembers(groupName, 0, -1, function (err, members) {
if (err || !Array.isArray(members) || !members.length) {
return callback(err);
}
if (!Array.isArray(uids)) {
uids = [uids];
}
Notifications.push(notification, members, callback);
});
};
uids = uids.filter(function (uid, index, array) {
return parseInt(uid, 10) && array.indexOf(uid) === index;
});
Notifications.pushGroups = function (notification, groupNames, callback) {
callback = callback || function () {};
groups.getMembersOfGroups(groupNames, function (err, groupMembers) {
if (!uids.length) {
return callback();
}
setTimeout(function () {
batch.processArray(uids, function (uids, next) {
pushToUids(uids, notification, next);
}, { interval: 1000 }, function (err) {
if (err) {
return callback(err);
winston.error(err.stack);
}
});
}, 1000);
callback();
};
function pushToUids(uids, notification, callback) {
var oneWeekAgo = Date.now() - 604800000;
var unreadKeys = [];
var readKeys = [];
async.waterfall([
function (next) {
plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next);
},
function (data, next) {
if (!data || !data.notification || !data.uids || !data.uids.length) {
return callback();
}
uids = data.uids;
notification = data.notification;
uids.forEach(function (uid) {
unreadKeys.push('uid:' + uid + ':notifications:unread');
readKeys.push('uid:' + uid + ':notifications:read');
});
db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next);
},
function (next) {
db.sortedSetsRemove(readKeys, notification.nid, next);
},
function (next) {
db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next);
},
function (next) {
db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next);
},
function (next) {
var websockets = require('./socket.io');
if (websockets.server) {
uids.forEach(function (uid) {
websockets.in('uid_' + uid).emit('event:new_notification', notification);
});
}
var members = _.unique(_.flatten(groupMembers));
plugins.fireHook('action:notification.pushed', { notification: notification, uids: uids });
next();
},
], callback);
}
Notifications.push(notification, members, callback);
});
};
Notifications.pushGroup = function (notification, groupName, callback) {
callback = callback || function () {};
groups.getMembers(groupName, 0, -1, function (err, members) {
if (err || !Array.isArray(members) || !members.length) {
return callback(err);
}
Notifications.rescind = function (nid, callback) {
callback = callback || function () {};
Notifications.push(notification, members, callback);
});
};
Notifications.pushGroups = function (notification, groupNames, callback) {
callback = callback || function () {};
groups.getMembersOfGroups(groupNames, function (err, groupMembers) {
if (err) {
return callback(err);
}
var members = _.unique(_.flatten(groupMembers));
Notifications.push(notification, members, callback);
});
};
Notifications.rescind = function (nid, callback) {
callback = callback || function () {};
async.parallel([
async.apply(db.sortedSetRemove, 'notifications', nid),
async.apply(db.delete, 'notifications:' + nid),
], function (err) {
if (err) {
winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message);
} else {
winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"');
}
callback(err, nid);
});
};
Notifications.markRead = function (nid, uid, callback) {
callback = callback || function () {};
if (!parseInt(uid, 10) || !nid) {
return callback();
}
Notifications.markReadMultiple([nid], uid, callback);
};
Notifications.markUnread = function (nid, uid, callback) {
callback = callback || function () {};
if (!parseInt(uid, 10) || !nid) {
return callback();
}
db.getObject('notifications:' + nid, function (err, notification) {
if (err || !notification) {
return callback(err || new Error('[[error:no-notification]]'));
}
notification.datetime = notification.datetime || Date.now();
async.parallel([
async.apply(db.sortedSetRemove, 'notifications', nid),
async.apply(db.delete, 'notifications:' + nid),
async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
], callback);
});
};
Notifications.markReadMultiple = function (nids, uid, callback) {
callback = callback || function () {};
nids = nids.filter(Boolean);
if (!Array.isArray(nids) || !nids.length) {
return callback();
}
var notificationKeys = nids.map(function (nid) {
return 'notifications:' + nid;
});
async.waterfall([
async.apply(db.getObjectsFields, notificationKeys, ['mergeId']),
function (mergeIds, next) {
// Isolate mergeIds and find related notifications
mergeIds = mergeIds.map(function (set) {
return set.mergeId;
}).reduce(function (memo, mergeId, idx, arr) {
if (mergeId && idx === arr.indexOf(mergeId)) {
memo.push(mergeId);
}
return memo;
}, []);
Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next);
},
function (relatedNids, next) {
notificationKeys = _.union(nids, relatedNids).map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next);
},
], function (err, notificationData) {
if (err) {
return callback(err);
}
// Filter out notifications that didn't exist
notificationData = notificationData.filter(function (notification) {
return notification && notification.nid;
});
// Extract nid
nids = notificationData.map(function (notification) {
return notification.nid;
});
var datetimes = notificationData.map(function (notification) {
return (notification && notification.datetime) || Date.now();
});
async.parallel([
function (next) {
db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
},
function (next) {
db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next);
},
], function (err) {
if (err) {
winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message);
} else {
winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"');
}
callback(err, nid);
callback(err);
});
};
});
};
Notifications.markRead = function (nid, uid, callback) {
callback = callback || function () {};
if (!parseInt(uid, 10) || !nid) {
return callback();
}
Notifications.markReadMultiple([nid], uid, callback);
};
Notifications.markUnread = function (nid, uid, callback) {
callback = callback || function () {};
if (!parseInt(uid, 10) || !nid) {
return callback();
Notifications.markAllRead = function (uid, callback) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
if (err) {
return callback(err);
}
db.getObject('notifications:' + nid, function (err, notification) {
if (err || !notification) {
return callback(err || new Error('[[error:no-notification]]'));
}
notification.datetime = notification.datetime || Date.now();
async.parallel([
async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
], callback);
});
};
Notifications.markReadMultiple = function (nids, uid, callback) {
callback = callback || function () {};
nids = nids.filter(Boolean);
if (!Array.isArray(nids) || !nids.length) {
return callback();
}
var notificationKeys = nids.map(function (nid) {
Notifications.markReadMultiple(nids, uid, callback);
});
};
Notifications.prune = function () {
var week = 604800000;
var cutoffTime = Date.now() - week;
db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) {
if (err) {
return winston.error(err.message);
}
if (!Array.isArray(nids) || !nids.length) {
return;
}
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
async.waterfall([
async.apply(db.getObjectsFields, notificationKeys, ['mergeId']),
function (mergeIds, next) {
// Isolate mergeIds and find related notifications
mergeIds = mergeIds.map(function (set) {
return set.mergeId;
}).reduce(function (memo, mergeId, idx, arr) {
if (mergeId && idx === arr.indexOf(mergeId)) {
memo.push(mergeId);
}
return memo;
}, []);
Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next);
async.parallel([
function (next) {
db.sortedSetRemove('notifications', nids, next);
},
function (relatedNids, next) {
notificationKeys = _.union(nids, relatedNids).map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next);
function (next) {
db.deleteAll(keys, next);
},
], function (err, notificationData) {
], function (err) {
if (err) {
return callback(err);
return winston.error('Encountered error pruning notifications: ' + err.message);
}
// Filter out notifications that didn't exist
notificationData = notificationData.filter(function (notification) {
return notification && notification.nid;
});
// Extract nid
nids = notificationData.map(function (notification) {
return notification.nid;
});
var datetimes = notificationData.map(function (notification) {
return (notification && notification.datetime) || Date.now();
});
async.parallel([
function (next) {
db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
},
function (next) {
db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next);
},
], function (err) {
callback(err);
});
});
};
});
};
Notifications.markAllRead = function (uid, callback) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
if (err) {
return callback(err);
Notifications.merge = function (notifications, callback) {
// When passed a set of notification objects, merge any that can be merged
var mergeIds = [
'notifications:upvoted_your_post_in',
'notifications:user_started_following_you',
'notifications:user_posted_to',
'notifications:user_flagged_post_in',
'notifications:user_flagged_user',
'new_register',
];
var isolated;
var differentiators;
var differentiator;
var modifyIndex;
var set;
notifications = mergeIds.reduce(function (notifications, mergeId) {
isolated = notifications.filter(function (notifObj) {
if (!notifObj || !notifObj.hasOwnProperty('mergeId')) {
return false;
}
if (!Array.isArray(nids) || !nids.length) {
return callback();
}
Notifications.markReadMultiple(nids, uid, callback);
return notifObj.mergeId.split('|')[0] === mergeId;
});
};
Notifications.prune = function () {
var week = 604800000;
if (isolated.length <= 1) {
return notifications; // Nothing to merge
}
var cutoffTime = Date.now() - week;
db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) {
if (err) {
return winston.error(err.message);
// Each isolated mergeId may have multiple differentiators, so process each separately
differentiators = isolated.reduce(function (cur, next) {
differentiator = next.mergeId.split('|')[1] || 0;
if (cur.indexOf(differentiator) === -1) {
cur.push(differentiator);
}
if (!Array.isArray(nids) || !nids.length) {
return;
}
return cur;
}, []);
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
async.parallel([
function (next) {
db.sortedSetRemove('notifications', nids, next);
},
function (next) {
db.deleteAll(keys, next);
},
], function (err) {
if (err) {
return winston.error('Encountered error pruning notifications: ' + err.message);
}
});
});
};
Notifications.merge = function (notifications, callback) {
// When passed a set of notification objects, merge any that can be merged
var mergeIds = [
'notifications:upvoted_your_post_in',
'notifications:user_started_following_you',
'notifications:user_posted_to',
'notifications:user_flagged_post_in',
'new_register',
];
var isolated;
var differentiators;
var differentiator;
var modifyIndex;
var set;
notifications = mergeIds.reduce(function (notifications, mergeId) {
isolated = notifications.filter(function (notifObj) {
if (!notifObj || !notifObj.hasOwnProperty('mergeId')) {
return false;
}
return notifObj.mergeId.split('|')[0] === mergeId;
});
if (isolated.length <= 1) {
return notifications; // Nothing to merge
}
// Each isolated mergeId may have multiple differentiators, so process each separately
differentiators = isolated.reduce(function (cur, next) {
differentiator = next.mergeId.split('|')[1] || 0;
if (cur.indexOf(differentiator) === -1) {
cur.push(differentiator);
}
return cur;
}, []);
differentiators.forEach(function (differentiator) {
if (differentiator === 0 && differentiators.length === 1) {
set = isolated;
} else {
set = isolated.filter(function (notifObj) {
return notifObj.mergeId === (mergeId + '|' + differentiator);
});
}
modifyIndex = notifications.indexOf(set[0]);
if (modifyIndex === -1 || set.length === 1) {
return notifications;
}
switch (mergeId) {
// intentional fall-through
case 'notifications:upvoted_your_post_in':
case 'notifications:user_started_following_you':
case 'notifications:user_posted_to':
case 'notifications:user_flagged_post_in':
var usernames = set.map(function (notifObj) {
return notifObj && notifObj.user && notifObj.user.username;
}).filter(function (username, idx, array) {
return array.indexOf(username) === idx;
});
var numUsers = usernames.length;
var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s;
var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
titleEscaped = titleEscaped ? (', ' + titleEscaped) : '';
if (numUsers === 2) {
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + titleEscaped + ']]';
} else if (numUsers > 2) {
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]';
}
notifications[modifyIndex].path = set[set.length - 1].path;
break;
case 'new_register':
notifications[modifyIndex].bodyShort = '[[notifications:' + mergeId + '_multiple, ' + set.length + ']]';
break;
}
// Filter out duplicates
notifications = notifications.filter(function (notifObj, idx) {
if (!notifObj || !notifObj.mergeId) {
return true;
}
return !(notifObj.mergeId === (mergeId + (differentiator ? '|' + differentiator : '')) && idx !== modifyIndex);
differentiators.forEach(function (differentiator) {
if (differentiator === 0 && differentiators.length === 1) {
set = isolated;
} else {
set = isolated.filter(function (notifObj) {
return notifObj.mergeId === (mergeId + '|' + differentiator);
});
}
modifyIndex = notifications.indexOf(set[0]);
if (modifyIndex === -1 || set.length === 1) {
return notifications;
}
switch (mergeId) {
// intentional fall-through
case 'notifications:upvoted_your_post_in':
case 'notifications:user_started_following_you':
case 'notifications:user_posted_to':
case 'notifications:user_flagged_post_in':
case 'notifications:user_flagged_user':
var usernames = set.map(function (notifObj) {
return notifObj && notifObj.user && notifObj.user.username;
}).filter(function (username, idx, array) {
return array.indexOf(username) === idx;
});
var numUsers = usernames.length;
var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s;
var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
titleEscaped = titleEscaped ? (', ' + titleEscaped) : '';
if (numUsers === 2) {
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + titleEscaped + ']]';
} else if (numUsers > 2) {
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]';
}
notifications[modifyIndex].path = set[set.length - 1].path;
break;
case 'new_register':
notifications[modifyIndex].bodyShort = '[[notifications:' + mergeId + '_multiple, ' + set.length + ']]';
break;
}
// Filter out duplicates
notifications = notifications.filter(function (notifObj, idx) {
if (!notifObj || !notifObj.mergeId) {
return true;
}
return !(notifObj.mergeId === (mergeId + (differentiator ? '|' + differentiator : '')) && idx !== modifyIndex);
});
return notifications;
}, notifications);
plugins.fireHook('filter:notifications.merge', {
notifications: notifications,
}, function (err, data) {
callback(err, data.notifications);
});
};
}(exports));
return notifications;
}, notifications);
plugins.fireHook('filter:notifications.merge', {
notifications: notifications,
}, function (err, data) {
callback(err, data.notifications);
});
};

View File

@@ -1,6 +1,7 @@
'use strict';
var qs = require('querystring');
var _ = require('underscore');
var pagination = {};
@@ -37,7 +38,7 @@ pagination.create = function (currentPage, pageCount, queryObj) {
return a - b;
});
queryObj = queryObj || {};
queryObj = _.clone(queryObj || {});
delete queryObj._;

View File

@@ -9,6 +9,9 @@
};
module.compare = function (password, hash, callback) {
if (!hash || !password) {
return setImmediate(callback, null, false);
}
forkChild({ type: 'compare', password: password, hash: hash }, callback);
};

View File

@@ -159,9 +159,7 @@ var middleware;
});
// Filter out plugins with invalid paths
async.filter(paths, file.exists, function (paths) {
next(null, paths);
});
async.filter(paths, file.exists, next);
},
function (paths, next) {
async.map(paths, Plugins.loadPluginInfo, next);
@@ -341,11 +339,15 @@ var middleware;
async.filter(dirs, function (dir, callback) {
fs.stat(dir, function (err, stats) {
callback(!err && stats.isDirectory());
if (err) {
if (err.code === 'ENOENT') {
return callback(null, false);
}
return callback(err);
}
callback(null, stats.isDirectory());
});
}, function (plugins) {
next(null, plugins);
});
}, next);
},
function (files, next) {

View File

@@ -8,6 +8,7 @@ module.exports = function (Plugins) {
'filter:user.custom_fields': null, // remove in v1.1.0
'filter:post.save': 'filter:post.create',
'filter:user.profileLinks': 'filter:user.profileMenu',
'action:post.flag': 'action:flag.create',
};
/*
`data` is an object consisting of (* is required):

View File

@@ -49,8 +49,8 @@ module.exports = function (Plugins) {
},
function (next) {
meta.reloadRequired = true;
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', id);
next();
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', { id: id });
setImmediate(next);
},
], function (err) {
if (err) {
@@ -67,8 +67,8 @@ module.exports = function (Plugins) {
};
function toggleInstall(id, version, callback) {
var type;
var installed;
var type;
async.waterfall([
function (next) {
Plugins.isInstalled(id, next);
@@ -85,7 +85,7 @@ module.exports = function (Plugins) {
});
return;
}
next();
setImmediate(next);
},
function (next) {
runNpmCommand(type, id, version || 'latest', next);
@@ -94,8 +94,8 @@ module.exports = function (Plugins) {
Plugins.get(id, next);
},
function (pluginData, next) {
Plugins.fireHook('action:plugin.' + type, id);
next(null, pluginData);
Plugins.fireHook('action:plugin.' + type, { id: id, version: version });
setImmediate(next, null, pluginData);
},
], callback);
}

View File

@@ -29,9 +29,7 @@ module.exports = function (Plugins) {
return path.join(__dirname, '../../node_modules/', plugin);
});
async.filter(plugins, file.exists, function (plugins) {
next(null, plugins);
});
async.filter(plugins, file.exists, next);
},
], callback);
};
@@ -166,13 +164,13 @@ module.exports = function (Plugins) {
var realPath = pluginData.staticDirs[mappedPath];
var staticDir = path.join(pluginPath, realPath);
file.exists(staticDir, function (exists) {
file.exists(staticDir, function (err, exists) {
if (exists) {
Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir;
} else {
winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + mappedPath + ' => ' + staticDir + '\' not found.');
}
callback();
callback(err);
});
}
}

View File

@@ -10,259 +10,277 @@ var topics = require('./topics');
var privileges = require('./privileges');
var plugins = require('./plugins');
(function (Posts) {
require('./posts/create')(Posts);
require('./posts/delete')(Posts);
require('./posts/edit')(Posts);
require('./posts/parse')(Posts);
require('./posts/user')(Posts);
require('./posts/topics')(Posts);
require('./posts/category')(Posts);
require('./posts/summary')(Posts);
require('./posts/recent')(Posts);
require('./posts/flags')(Posts);
require('./posts/tools')(Posts);
require('./posts/votes')(Posts);
require('./posts/bookmarks')(Posts);
var Posts = module.exports;
Posts.exists = function (pid, callback) {
db.isSortedSetMember('posts:pid', pid, callback);
};
require('./posts/create')(Posts);
require('./posts/delete')(Posts);
require('./posts/edit')(Posts);
require('./posts/parse')(Posts);
require('./posts/user')(Posts);
require('./posts/topics')(Posts);
require('./posts/category')(Posts);
require('./posts/summary')(Posts);
require('./posts/recent')(Posts);
require('./posts/tools')(Posts);
require('./posts/votes')(Posts);
require('./posts/bookmarks')(Posts);
Posts.getPidsFromSet = function (set, start, stop, reverse, callback) {
if (isNaN(start) || isNaN(stop)) {
return callback(null, []);
}
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback);
};
Posts.exists = function (pid, callback) {
db.isSortedSetMember('posts:pid', pid, callback);
};
Posts.getPostsByPids = function (pids, uid, callback) {
if (!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
Posts.getPidsFromSet = function (set, start, stop, reverse, callback) {
if (isNaN(start) || isNaN(stop)) {
return callback(null, []);
}
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback);
};
var keys = [];
Posts.getPostsByPids = function (pids, uid, callback) {
if (!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
for (var x = 0, numPids = pids.length; x < numPids; x += 1) {
keys.push('post:' + pids[x]);
}
async.waterfall([
function (next) {
db.getObjects(keys, next);
},
function (posts, next) {
async.map(posts, function (post, next) {
if (!post) {
return next();
}
post.upvotes = parseInt(post.upvotes, 10) || 0;
post.downvotes = parseInt(post.downvotes, 10) || 0;
post.votes = post.upvotes - post.downvotes;
post.timestampISO = utils.toISOString(post.timestamp);
post.editedISO = parseInt(post.edited, 10) !== 0 ? utils.toISOString(post.edited) : '';
Posts.parsePost(post, next);
}, next);
},
function (posts, next) {
plugins.fireHook('filter:post.getPosts', { posts: posts, uid: uid }, next);
},
function (data, next) {
if (!data || !Array.isArray(data.posts)) {
return next(null, []);
async.waterfall([
function (next) {
var keys = pids.map(function (pid) {
return 'post:' + pid;
});
db.getObjects(keys, next);
},
function (posts, next) {
async.map(posts, function (post, next) {
if (!post) {
return next();
}
data.posts = data.posts.filter(Boolean);
next(null, data.posts);
},
], callback);
};
Posts.getPostSummariesFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
db.getSortedSetRevRange(set, start, stop, next);
},
function (pids, next) {
privileges.posts.filter('read', pids, uid, next);
},
function (pids, next) {
Posts.getPostSummaryByPids(pids, uid, { stripTags: false }, next);
},
function (posts, next) {
next(null, { posts: posts, nextStart: stop + 1 });
},
], callback);
};
Posts.getPostData = function (pid, callback) {
db.getObject('post:' + pid, function (err, data) {
if (err) {
return callback(err);
post.upvotes = parseInt(post.upvotes, 10) || 0;
post.downvotes = parseInt(post.downvotes, 10) || 0;
post.votes = post.upvotes - post.downvotes;
post.timestampISO = utils.toISOString(post.timestamp);
post.editedISO = parseInt(post.edited, 10) !== 0 ? utils.toISOString(post.edited) : '';
Posts.parsePost(post, next);
}, next);
},
function (posts, next) {
plugins.fireHook('filter:post.getPosts', { posts: posts, uid: uid }, next);
},
function (data, next) {
if (!data || !Array.isArray(data.posts)) {
return next(null, []);
}
data.posts = data.posts.filter(Boolean);
next(null, data.posts);
},
], callback);
};
plugins.fireHook('filter:post.get', data, callback);
});
};
Posts.getPostSummariesFromSet = function (set, uid, start, stop, callback) {
async.waterfall([
function (next) {
db.getSortedSetRevRange(set, start, stop, next);
},
function (pids, next) {
privileges.posts.filter('read', pids, uid, next);
},
function (pids, next) {
Posts.getPostSummaryByPids(pids, uid, { stripTags: false }, next);
},
function (posts, next) {
next(null, { posts: posts, nextStart: stop + 1 });
},
], callback);
};
Posts.getPostField = function (pid, field, callback) {
Posts.getPostFields(pid, [field], function (err, data) {
if (err) {
return callback(err);
}
Posts.getPostData = function (pid, callback) {
async.waterfall([
function (next) {
db.getObject('post:' + pid, next);
},
function (data, next) {
plugins.fireHook('filter:post.getPostData', { post: data }, next);
},
function (data, next) {
next(null, data.post);
},
], callback);
};
callback(null, data[field]);
});
};
Posts.getPostFields = function (pid, fields, callback) {
db.getObjectFields('post:' + pid, fields, function (err, data) {
if (err) {
return callback(err);
}
Posts.getPostField = function (pid, field, callback) {
async.waterfall([
function (next) {
Posts.getPostFields(pid, [field], next);
},
function (data, next) {
next(null, data[field]);
},
], callback);
};
Posts.getPostFields = function (pid, fields, callback) {
async.waterfall([
function (next) {
db.getObjectFields('post:' + pid, fields, next);
},
function (data, next) {
data.pid = pid;
plugins.fireHook('filter:post.getFields', { posts: [data], fields: fields }, function (err, data) {
callback(err, (data && Array.isArray(data.posts) && data.posts.length) ? data.posts[0] : null);
});
});
};
plugins.fireHook('filter:post.getFields', { posts: [data], fields: fields }, next);
},
function (data, next) {
next(null, (data && Array.isArray(data.posts) && data.posts.length) ? data.posts[0] : null);
},
], callback);
};
Posts.getPostsFields = function (pids, fields, callback) {
if (!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
Posts.getPostsFields = function (pids, fields, callback) {
if (!Array.isArray(pids) || !pids.length) {
return callback(null, []);
}
var keys = pids.map(function (pid) {
return 'post:' + pid;
});
var keys = pids.map(function (pid) {
return 'post:' + pid;
});
db.getObjectsFields(keys, fields, function (err, posts) {
if (err) {
return callback(err);
}
plugins.fireHook('filter:post.getFields', { posts: posts, fields: fields }, function (err, data) {
callback(err, (data && Array.isArray(data.posts)) ? data.posts : null);
});
});
};
async.waterfall([
function (next) {
db.getObjectsFields(keys, fields, next);
},
function (posts, next) {
plugins.fireHook('filter:post.getFields', { posts: posts, fields: fields }, next);
},
function (data, next) {
next(null, (data && Array.isArray(data.posts)) ? data.posts : null);
},
], callback);
};
Posts.setPostField = function (pid, field, value, callback) {
db.setObjectField('post:' + pid, field, value, function (err) {
if (err) {
return callback(err);
}
Posts.setPostField = function (pid, field, value, callback) {
async.waterfall([
function (next) {
db.setObjectField('post:' + pid, field, value, next);
},
function (next) {
var data = {
pid: pid,
};
data[field] = value;
plugins.fireHook('action:post.setFields', data);
callback();
});
};
plugins.fireHook('action:post.setFields', { data: data });
next();
},
], callback);
};
Posts.setPostFields = function (pid, data, callback) {
db.setObject('post:' + pid, data, function (err) {
if (err) {
return callback(err);
}
Posts.setPostFields = function (pid, data, callback) {
async.waterfall([
function (next) {
db.setObject('post:' + pid, data, next);
},
function (next) {
data.pid = pid;
plugins.fireHook('action:post.setFields', data);
callback();
});
};
plugins.fireHook('action:post.setFields', { data: data });
next();
},
], callback);
};
Posts.getPidIndex = function (pid, tid, topicPostSort, callback) {
var set = topicPostSort === 'most_votes' ? 'tid:' + tid + ':posts:votes' : 'tid:' + tid + ':posts';
db.sortedSetRank(set, pid, function (err, index) {
Posts.getPidIndex = function (pid, tid, topicPostSort, callback) {
async.waterfall([
function (next) {
var set = topicPostSort === 'most_votes' ? 'tid:' + tid + ':posts:votes' : 'tid:' + tid + ':posts';
db.sortedSetRank(set, pid, next);
},
function (index, next) {
if (!utils.isNumber(index)) {
return callback(err, 0);
return next(null, 0);
}
callback(err, parseInt(index, 10) + 1);
});
};
next(null, parseInt(index, 10) + 1);
},
], callback);
};
Posts.getPostIndices = function (posts, uid, callback) {
if (!Array.isArray(posts) || !posts.length) {
return callback(null, []);
}
Posts.getPostIndices = function (posts, uid, callback) {
if (!Array.isArray(posts) || !posts.length) {
return callback(null, []);
}
async.waterfall([
function (next) {
user.getSettings(uid, next);
},
function (settings, next) {
var byVotes = settings.topicPostSort === 'most_votes';
var sets = posts.map(function (post) {
return byVotes ? 'tid:' + post.tid + ':posts:votes' : 'tid:' + post.tid + ':posts';
});
async.waterfall([
function (next) {
user.getSettings(uid, next);
},
function (settings, next) {
var byVotes = settings.topicPostSort === 'most_votes';
var sets = posts.map(function (post) {
return byVotes ? 'tid:' + post.tid + ':posts:votes' : 'tid:' + post.tid + ':posts';
});
var uniqueSets = _.uniq(sets);
var method = 'sortedSetsRanks';
if (uniqueSets.length === 1) {
method = 'sortedSetRanks';
sets = uniqueSets[0];
}
var uniqueSets = _.uniq(sets);
var method = 'sortedSetsRanks';
if (uniqueSets.length === 1) {
method = 'sortedSetRanks';
sets = uniqueSets[0];
}
var pids = posts.map(function (post) {
return post.pid;
});
var pids = posts.map(function (post) {
return post.pid;
});
db[method](sets, pids, next);
},
function (indices, next) {
for (var i = 0; i < indices.length; i += 1) {
indices[i] = utils.isNumber(indices[i]) ? parseInt(indices[i], 10) + 1 : 0;
}
db[method](sets, pids, next);
},
function (indices, next) {
for (var i = 0; i < indices.length; i += 1) {
indices[i] = utils.isNumber(indices[i]) ? parseInt(indices[i], 10) + 1 : 0;
}
next(null, indices);
},
], callback);
};
next(null, indices);
},
], callback);
};
Posts.updatePostVoteCount = function (postData, callback) {
if (!postData || !postData.pid || !postData.tid) {
return callback();
}
async.parallel([
function (next) {
if (postData.uid) {
if (postData.votes > 0) {
db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', postData.votes, postData.pid, next);
} else {
db.sortedSetRemove('uid:' + postData.uid + ':posts:votes', postData.pid, next);
}
Posts.updatePostVoteCount = function (postData, callback) {
if (!postData || !postData.pid || !postData.tid) {
return callback();
}
async.parallel([
function (next) {
if (postData.uid) {
if (postData.votes > 0) {
db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', postData.votes, postData.pid, next);
} else {
next();
db.sortedSetRemove('uid:' + postData.uid + ':posts:votes', postData.pid, next);
}
},
function (next) {
async.waterfall([
function (next) {
topics.getTopicField(postData.tid, 'mainPid', next);
},
function (mainPid, next) {
if (parseInt(mainPid, 10) === parseInt(postData.pid, 10)) {
return next();
}
db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', postData.votes, postData.pid, next);
},
], next);
},
function (next) {
Posts.setPostFields(postData.pid, { upvotes: postData.upvotes, downvotes: postData.downvotes }, next);
},
], function (err) {
callback(err);
});
};
Posts.modifyPostByPrivilege = function (post, isAdminOrMod) {
if (post.deleted && !(isAdminOrMod || post.selfPost)) {
post.content = '[[topic:post_is_deleted]]';
if (post.user) {
post.user.signature = '';
} else {
next();
}
},
function (next) {
async.waterfall([
function (next) {
topics.getTopicField(postData.tid, 'mainPid', next);
},
function (mainPid, next) {
if (parseInt(mainPid, 10) === parseInt(postData.pid, 10)) {
return next();
}
db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', postData.votes, postData.pid, next);
},
], next);
},
function (next) {
db.sortedSetAdd('posts:votes', postData.votes, postData.pid, next);
},
function (next) {
Posts.setPostFields(postData.pid, { upvotes: postData.upvotes, downvotes: postData.downvotes }, next);
},
], function (err) {
callback(err);
});
};
Posts.modifyPostByPrivilege = function (post, isAdminOrMod) {
if (post.deleted && !(isAdminOrMod || post.selfPost)) {
post.content = '[[topic:post_is_deleted]]';
if (post.user) {
post.user.signature = '';
}
};
}(exports));
}
};

View File

@@ -9,6 +9,7 @@ var plugins = require('../plugins');
var user = require('../user');
var topics = require('../topics');
var categories = require('../categories');
var groups = require('../groups');
var utils = require('../utils');
module.exports = function (Posts) {
@@ -82,6 +83,9 @@ module.exports = function (Posts) {
categories.onNewPostMade(topicData.cid, topicData.pinned, postData, next);
});
},
function (next) {
groups.onNewPostMade(postData, next);
},
function (next) {
db.sortedSetAdd('posts:pid', timestamp, postData.pid, next);
},
@@ -101,13 +105,13 @@ module.exports = function (Posts) {
if (err) {
return next(err);
}
plugins.fireHook('filter:post.get', postData, next);
plugins.fireHook('filter:post.get', { post: postData, uid: data.uid }, next);
});
},
function (postData, next) {
postData.isMain = isMain;
plugins.fireHook('action:post.save', _.clone(postData));
next(null, postData);
function (data, next) {
data.post.isMain = isMain;
plugins.fireHook('action:post.save', { post: _.clone(data.post) });
next(null, data.post);
},
], callback);
};

View File

@@ -6,6 +6,7 @@ var _ = require('underscore');
var db = require('../database');
var topics = require('../topics');
var user = require('../user');
var groups = require('../groups');
var notifications = require('../notifications');
var plugins = require('../plugins');
@@ -27,6 +28,7 @@ module.exports = function (Posts) {
topics.getTopicFields(_post.tid, ['tid', 'cid', 'pinned'], next);
},
function (topicData, next) {
postData.cid = topicData.cid;
async.parallel([
function (next) {
updateTopicTimestamp(topicData, next);
@@ -40,7 +42,7 @@ module.exports = function (Posts) {
], next);
},
function (results, next) {
plugins.fireHook('action:post.delete', pid);
plugins.fireHook('action:post.delete', { post: _.clone(postData), uid: uid });
next(null, postData);
},
], callback);
@@ -77,7 +79,7 @@ module.exports = function (Posts) {
], next);
},
function (results, next) {
plugins.fireHook('action:post.restore', _.clone(postData));
plugins.fireHook('action:post.restore', { post: _.clone(postData), uid: uid });
next(null, postData);
},
], callback);
@@ -141,19 +143,22 @@ module.exports = function (Posts) {
deletePostFromReplies(pid, next);
},
function (next) {
db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next);
deletePostFromGroups(pid, next);
},
function (next) {
Posts.dismissFlag(pid, next);
db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next);
},
], function (err) {
if (err) {
return next(err);
}
plugins.fireHook('action:post.purge', pid);
db.delete('post:' + pid, next);
next(err);
});
},
function (next) {
Posts.getPostData(pid, next);
},
function (postData, next) {
plugins.fireHook('action:post.purge', { post: postData, uid: uid });
db.delete('post:' + pid, next);
},
], callback);
};
@@ -293,4 +298,26 @@ module.exports = function (Posts) {
], callback);
});
}
function deletePostFromGroups(pid, callback) {
async.waterfall([
function (next) {
Posts.getPostField(pid, 'uid', next);
},
function (uid, next) {
if (!parseInt(uid, 10)) {
return callback();
}
groups.getUserGroupMembership('groups:visible:createtime', [uid], next);
},
function (groupNames, next) {
groupNames = groupNames[0];
var keys = groupNames.map(function (groupName) {
return 'group:' + groupName + ':member:pids';
});
db.sortedSetsRemove(keys, pid, next);
},
], callback);
}
};

View File

@@ -65,7 +65,7 @@ module.exports = function (Posts) {
postData.cid = results.topic.cid;
postData.topic = results.topic;
plugins.fireHook('action:post.edit', _.clone(postData));
plugins.fireHook('action:post.edit', { post: _.clone(postData), uid: data.uid });
cache.del(String(postData.pid));
pubsub.publish('post:edit', String(postData.pid));
@@ -85,7 +85,7 @@ module.exports = function (Posts) {
async.parallel({
topic: function (next) {
topics.getTopicFields(tid, ['cid', 'title'], next);
topics.getTopicFields(tid, ['cid', 'title', 'timestamp'], next);
},
isMain: function (next) {
Posts.isMain(data.pid, next);
@@ -136,7 +136,8 @@ module.exports = function (Posts) {
function (tags, next) {
topicData.tags = data.tags;
topicData.oldTitle = results.topic.title;
plugins.fireHook('action:topic.edit', topicData);
topicData.timestamp = results.topic.timestamp;
plugins.fireHook('action:topic.edit', { topic: topicData, uid: data.uid });
next(null, {
tid: tid,
cid: results.topic.cid,

View File

@@ -1,418 +0,0 @@
'use strict';
var async = require('async');
var winston = require('winston');
var db = require('../database');
var user = require('../user');
var analytics = require('../analytics');
module.exports = function (Posts) {
Posts.flag = function (post, uid, reason, callback) {
if (!parseInt(uid, 10) || !reason) {
return callback();
}
async.waterfall([
function (next) {
async.parallel({
hasFlagged: async.apply(Posts.isFlaggedByUser, post.pid, uid),
exists: async.apply(Posts.exists, post.pid),
}, next);
},
function (results, next) {
if (!results.exists) {
return next(new Error('[[error:no-post]]'));
}
if (results.hasFlagged) {
return next(new Error('[[error:already-flagged]]'));
}
var now = Date.now();
async.parallel([
function (next) {
db.sortedSetAdd('posts:flagged', now, post.pid, next);
},
function (next) {
db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
},
function (next) {
db.incrObjectField('post:' + post.pid, 'flags', next);
},
function (next) {
db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
},
function (next) {
db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
},
function (next) {
if (parseInt(post.uid, 10)) {
async.parallel([
async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid),
async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'),
async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid),
], next);
} else {
next();
}
},
], next);
},
function (data, next) {
openNewFlag(post.pid, uid, next);
},
], function (err) {
if (err) {
return callback(err);
}
analytics.increment('flags');
callback();
});
};
function openNewFlag(pid, uid, callback) {
db.sortedSetScore('posts:flags:count', pid, function (err, count) {
if (err) {
return callback(err);
}
if (count === 1) { // Only update state on new flag
Posts.updateFlagData(uid, pid, {
state: 'open',
}, callback);
} else {
callback();
}
});
}
Posts.isFlaggedByUser = function (pid, uid, callback) {
db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback);
};
Posts.dismissFlag = function (pid, callback) {
async.waterfall([
function (next) {
db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next);
},
function (postData, next) {
if (!postData.pid) {
return callback();
}
async.parallel([
function (next) {
if (parseInt(postData.uid, 10)) {
if (parseInt(postData.flags, 10) > 0) {
async.parallel([
async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid),
async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags),
], next);
} else {
next();
}
} else {
next();
}
},
function (next) {
db.sortedSetsRemove([
'posts:flagged',
'posts:flags:count',
'uid:' + postData.uid + ':flag:pids',
], pid, next);
},
function (next) {
async.series([
function (next) {
db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function (err, uids) {
if (err) {
return next(err);
}
async.each(uids, function (uid, next) {
var nid = 'post_flag:' + pid + ':uid:' + uid;
async.parallel([
async.apply(db.delete, 'notifications:' + nid),
async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid),
], next);
}, next);
});
},
async.apply(db.delete, 'pid:' + pid + ':flag:uids'),
], next);
},
async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason'),
async.apply(db.deleteObjectFields, 'post:' + pid, ['flag:state', 'flag:assignee', 'flag:notes', 'flag:history']),
], next);
},
function (results, next) {
db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next);
},
], callback);
};
Posts.dismissAllFlags = function (callback) {
db.getSortedSetRange('posts:flagged', 0, -1, function (err, pids) {
if (err) {
return callback(err);
}
async.eachSeries(pids, Posts.dismissFlag, callback);
});
};
Posts.dismissUserFlags = function (uid, callback) {
db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function (err, pids) {
if (err) {
return callback(err);
}
async.eachSeries(pids, Posts.dismissFlag, callback);
});
};
Posts.getFlags = function (set, cid, uid, start, stop, callback) {
async.waterfall([
function (next) {
if (Array.isArray(set)) {
db.getSortedSetRevIntersect({ sets: set, start: start, stop: -1, aggregate: 'MAX' }, next);
} else {
db.getSortedSetRevRange(set, start, -1, next);
}
},
function (pids, next) {
if (cid) {
Posts.filterPidsByCid(pids, cid, next);
} else {
process.nextTick(next, null, pids);
}
},
function (pids, next) {
getFlaggedPostsWithReasons(pids, uid, next);
},
function (posts, next) {
var count = posts.length;
var end = stop - start + 1;
next(null, { posts: posts.slice(0, stop === -1 ? undefined : end), count: count });
},
], callback);
};
function getFlaggedPostsWithReasons(pids, uid, callback) {
async.waterfall([
function (next) {
async.parallel({
uidsReasons: function (next) {
async.map(pids, function (pid, next) {
db.getSortedSetRange('pid:' + pid + ':flag:uid:reason', 0, -1, next);
}, next);
},
posts: function (next) {
Posts.getPostSummaryByPids(pids, uid, { stripTags: false, extraFields: ['flags', 'flag:assignee', 'flag:state', 'flag:notes', 'flag:history'] }, next);
},
}, next);
},
function (results, next) {
async.map(results.uidsReasons, function (uidReasons, next) {
async.map(uidReasons, function (uidReason, next) {
var uid = uidReason.split(':')[0];
var reason = uidReason.substr(uidReason.indexOf(':') + 1);
user.getUserFields(uid, ['username', 'userslug', 'picture'], function (err, userData) {
next(err, { user: userData, reason: reason });
});
}, next);
}, function (err, reasons) {
if (err) {
return callback(err);
}
results.posts.forEach(function (post, index) {
if (post) {
post.flagReasons = reasons[index];
}
});
next(null, results.posts);
});
},
async.apply(Posts.expandFlagHistory),
function (posts, next) {
// Parse out flag data into its own object inside each post hash
async.map(posts, function (postObj, next) {
for (var prop in postObj) {
if (postObj.hasOwnProperty(prop)) {
postObj.flagData = postObj.flagData || {};
if (prop.startsWith('flag:')) {
postObj.flagData[prop.slice(5)] = postObj[prop];
if (prop === 'flag:state') {
switch (postObj[prop]) {
case 'open':
postObj.flagData.labelClass = 'info';
break;
case 'wip':
postObj.flagData.labelClass = 'warning';
break;
case 'resolved':
postObj.flagData.labelClass = 'success';
break;
case 'rejected':
postObj.flagData.labelClass = 'danger';
break;
}
}
delete postObj[prop];
}
}
}
if (postObj.flagData.assignee) {
user.getUserFields(parseInt(postObj.flagData.assignee, 10), ['username', 'picture'], function (err, userData) {
if (err) {
return next(err);
}
postObj.flagData.assigneeUser = userData;
next(null, postObj);
});
} else {
setImmediate(next.bind(null, null, postObj));
}
}, next);
},
], callback);
}
Posts.updateFlagData = function (uid, pid, flagObj, callback) {
// Retrieve existing flag data to compare for history-saving purposes
var changes = [];
var changeset = {};
var prop;
Posts.getPostData(pid, function (err, postData) {
if (err) {
return callback(err);
}
// Track new additions
for (prop in flagObj) {
if (flagObj.hasOwnProperty(prop) && !postData.hasOwnProperty('flag:' + prop) && flagObj[prop].length) {
changes.push(prop);
}
}
// Track changed items
for (prop in postData) {
if (
postData.hasOwnProperty(prop) && prop.startsWith('flag:') &&
flagObj.hasOwnProperty(prop.slice(5)) &&
postData[prop] !== flagObj[prop.slice(5)]
) {
changes.push(prop.slice(5));
}
}
changeset = changes.reduce(function (memo, prop) {
memo['flag:' + prop] = flagObj[prop];
return memo;
}, {});
// Append changes to history string
if (changes.length) {
try {
var history = JSON.parse(postData['flag:history'] || '[]');
changes.forEach(function (property) {
switch (property) {
case 'assignee': // intentional fall-through
case 'state':
history.unshift({
uid: uid,
type: property,
value: flagObj[property],
timestamp: Date.now(),
});
break;
case 'notes':
history.unshift({
uid: uid,
type: property,
timestamp: Date.now(),
});
}
});
changeset['flag:history'] = JSON.stringify(history);
} catch (e) {
winston.warn('[posts/updateFlagData] Unable to deserialise post flag history, likely malformed data');
}
}
// Save flag data into post hash
if (changes.length) {
Posts.setPostFields(pid, changeset, callback);
} else {
setImmediate(callback);
}
});
};
Posts.expandFlagHistory = function (posts, callback) {
// Expand flag history
async.map(posts, function (post, next) {
var history;
try {
history = JSON.parse(post['flag:history'] || '[]');
} catch (e) {
winston.warn('[posts/getFlags] Unable to deserialise post flag history, likely malformed data');
return callback(e);
}
async.map(history, function (event, next) {
event.timestampISO = new Date(event.timestamp).toISOString();
async.parallel([
function (next) {
user.getUserFields(event.uid, ['username', 'picture'], function (err, userData) {
if (err) {
return next(err);
}
event.user = userData;
next();
});
},
function (next) {
if (event.type === 'assignee') {
user.getUserField(parseInt(event.value, 10), 'username', function (err, username) {
if (err) {
return next(err);
}
event.label = username || 'Unknown user';
next(null);
});
} else if (event.type === 'state') {
event.label = '[[topic:flag_manage_state_' + event.value + ']]';
setImmediate(next);
} else {
setImmediate(next);
}
},
], function (err) {
next(err, event);
});
}, function (err, history) {
if (err) {
return next(err);
}
post['flag:history'] = history;
next(null, post);
});
}, callback);
};
};

View File

@@ -8,12 +8,8 @@ var helpers = {};
helpers.some = function (tasks, callback) {
async.some(tasks, function (task, next) {
task(function (err, result) {
next(!err && result);
});
}, function (result) {
callback(null, result);
});
task(next);
}, callback);
};
helpers.isUserAllowedTo = function (privilege, uid, cid, callback) {

View File

@@ -3,6 +3,7 @@
var async = require('async');
var user = require('../user');
var groups = require('../groups');
var plugins = require('../plugins');
@@ -157,4 +158,49 @@ module.exports = function (privileges) {
callback(null, canEdit);
});
};
privileges.users.canBanUser = function (callerUid, uid, callback) {
async.waterfall([
function (next) {
async.parallel({
isAdmin: function (next) {
privileges.users.isAdministrator(callerUid, next);
},
isGlobalMod: function (next) {
privileges.users.isGlobalModerator(callerUid, next);
},
isTargetAdmin: function (next) {
privileges.users.isAdministrator(uid, next);
},
}, next);
},
function (results, next) {
results.canBan = !results.isTargetAdmin && (results.isAdmin || results.isGlobalMod);
results.callerUid = callerUid;
results.uid = uid;
plugins.fireHook('filter:user.canBanUser', results, next);
},
function (data, next) {
next(null, data.canBan);
},
], callback);
};
privileges.users.hasBanPrivilege = function (uid, callback) {
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(uid, next);
},
function (isAdminOrGlobalMod, next) {
plugins.fireHook('filter:user.hasBanPrivilege', {
uid: uid,
isAdminOrGlobalMod: isAdminOrGlobalMod,
canBan: isAdminOrGlobalMod,
}, next);
},
function (data, next) {
next(null, data.canBan);
},
], callback);
};
};

View File

@@ -33,12 +33,14 @@ rewards.checkConditionAndRewardUser = function (uid, condition, method, callback
async.filter(rewards, function (reward, next) {
if (!reward) {
return next(false);
return next(null, false);
}
checkCondition(reward, method, next);
}, function (eligible) {
if (!eligible) {
checkCondition(reward, method, function (result) {
next(null, result);
});
}, function (err, eligible) {
if (err || !eligible) {
return next(false);
}

View File

@@ -56,7 +56,6 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics);
router.get('/manage/tags', middlewares, controllers.admin.tags.get);
router.get('/manage/flags', middlewares, controllers.admin.flags.get);
router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get);
router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate);

View File

@@ -3,10 +3,15 @@
var helpers = {};
helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) {
middlewares = middlewares.concat([middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks]);
middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks].concat(middlewares);
router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, controller);
router.get('/api' + name, middlewares, controller);
};
helpers.setupAdminPageRoute = function (router, name, middleware, middlewares, controller) {
router.get(name, middleware.admin.buildHeader, middlewares, controller);
router.get('/api' + name, middlewares, controller);
};
module.exports = helpers;

View File

@@ -34,13 +34,11 @@ function mainRoutes(app, middleware, controllers) {
setupPageRoute(app, '/search', middleware, [], controllers.search.search);
setupPageRoute(app, '/reset/:code?', middleware, [], controllers.reset);
setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse);
app.get('/ping', controllers.ping);
app.get('/sping', controllers.ping);
}
function modRoutes(app, middleware, controllers) {
setupPageRoute(app, '/posts/flags', middleware, [], controllers.mods.flagged);
setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list);
setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail);
}
function globalModRoutes(app, middleware, controllers) {

View File

@@ -146,7 +146,7 @@ function getMatchedPosts(pids, data, callback) {
topicFields.push('postcount');
}
if (data.sortBy) {
if (data.sortBy && data.sortBy !== 'relevance') {
if (data.sortBy.startsWith('category')) {
topicFields.push('cid');
} else if (data.sortBy.startsWith('topic.')) {
@@ -326,7 +326,7 @@ function filterByTags(posts, hasTags) {
}
function sortPosts(posts, data) {
if (!posts.length || !data.sortBy) {
if (!posts.length || !data.sortBy || data.sortBy === 'relevance') {
return;
}

View File

@@ -19,31 +19,25 @@ var sitemap = {
};
sitemap.render = function (callback) {
var numTopics = parseInt(meta.config.sitemapTopics, 10) || 500;
var topicsPerPage = parseInt(meta.config.sitemapTopics, 10) || 500;
var returnData = {
url: nconf.get('url'),
topics: [],
};
var numPages;
async.waterfall([
async.apply(db.getSortedSetRange, 'topics:recent', 0, -1),
function (tids, next) {
privileges.topics.filterTids('read', tids, 0, next);
function (next) {
db.getObjectField('global', 'topicCount', next);
},
], function (err, tids) {
if (err) {
numPages = 1;
} else {
numPages = Math.ceil(tids.length / numTopics);
}
function (topicCount, next) {
var numPages = Math.max(0, topicCount / topicsPerPage);
for (var x = 1; x <= numPages; x += 1) {
returnData.topics.push(x);
}
for (var x = 1; x <= numPages; x += 1) {
returnData.topics.push(x);
}
callback(null, returnData);
});
next(null, returnData);
},
], callback);
};
sitemap.getPages = function (callback) {

View File

@@ -173,6 +173,7 @@ SocketAdmin.config.setMultiple = function (socket, data, callback) {
logger.monitorConfig({ io: index.server }, setting);
}
}
plugins.fireHook('action:config.set', { settings: data });
setImmediate(next);
},
], callback);

View File

@@ -7,7 +7,9 @@ var groups = require('../../groups');
var categories = require('../../categories');
var privileges = require('../../privileges');
var plugins = require('../../plugins');
var Categories = {};
var events = require('../../events');
var Categories = module.exports;
Categories.create = function (socket, data, callback) {
if (!data) {
@@ -42,7 +44,26 @@ Categories.getNames = function (socket, data, callback) {
};
Categories.purge = function (socket, cid, callback) {
categories.purge(cid, socket.uid, callback);
var name;
async.waterfall([
function (next) {
categories.getCategoryField(cid, 'name', next);
},
function (_name, next) {
name = _name;
categories.purge(cid, socket.uid, next);
},
function (next) {
events.log({
type: 'category-purge',
uid: socket.uid,
ip: socket.ip,
cid: cid,
name: name,
});
setImmediate(next);
},
], callback);
};
Categories.update = function (socket, data, callback) {
@@ -102,5 +123,3 @@ Categories.copySettingsFrom = function (socket, data, callback) {
Categories.copyPrivilegesFrom = function (socket, data, callback) {
categories.copyPrivilegesFrom(data.fromCid, data.toCid, callback);
};
module.exports = Categories;

View File

@@ -69,14 +69,6 @@ User.resetLockouts = function (socket, uids, callback) {
async.each(uids, user.auth.resetLockout, callback);
};
User.resetFlags = function (socket, uids, callback) {
if (!Array.isArray(uids)) {
return callback(new Error('[[error:invalid-data]]'));
}
user.resetFlags(uids, callback);
};
User.validateEmail = function (socket, uids, callback) {
if (!Array.isArray(uids)) {
return callback(new Error('[[error:invalid-data]]'));

102
src/socket.io/flags.js Normal file
View File

@@ -0,0 +1,102 @@
'use strict';
var async = require('async');
var user = require('../user');
var flags = require('../flags');
var SocketFlags = {};
SocketFlags.create = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:not-logged-in]]'));
}
if (!data || !data.type || !data.id || !data.reason) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
async.apply(flags.validate, {
uid: socket.uid,
type: data.type,
id: data.id,
}),
function (next) {
// If we got here, then no errors occurred
flags.create(data.type, data.id, socket.uid, data.reason, next);
},
], function (err, flagObj) {
if (err) {
return callback(err);
}
flags.notify(flagObj, socket.uid);
callback(null, flagObj);
});
};
SocketFlags.update = function (socket, data, callback) {
if (!data || !(data.flagId && data.data)) {
return callback(new Error('[[error:invalid-data]]'));
}
var payload = {};
async.waterfall([
function (next) {
async.parallel([
async.apply(user.isAdminOrGlobalMod, socket.uid),
async.apply(user.isModeratorOfAnyCategory, socket.uid),
], function (err, results) {
next(err, results[0] || results[1]);
});
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[no-privileges]]'));
}
// Translate form data into object
payload = data.data.reduce(function (memo, cur) {
memo[cur.name] = cur.value;
return memo;
}, payload);
flags.update(data.flagId, socket.uid, payload, next);
},
async.apply(flags.getHistory, data.flagId),
], callback);
};
SocketFlags.appendNote = function (socket, data, callback) {
if (!data || !(data.flagId && data.note)) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
async.parallel([
async.apply(user.isAdminOrGlobalMod, socket.uid),
async.apply(user.isModeratorOfAnyCategory, socket.uid),
], function (err, results) {
next(err, results[0] || results[1]);
});
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[no-privileges]]'));
}
flags.appendNote(data.flagId, socket.uid, data.note, next);
},
function (next) {
async.parallel({
notes: async.apply(flags.getNotes, data.flagId),
history: async.apply(flags.getHistory, data.flagId),
}, next);
},
], callback);
};
module.exports = SocketFlags;

View File

@@ -106,6 +106,7 @@ SocketHelpers.sendNotificationToPostOwner = function (pid, fromuid, command, not
var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
notifications.create({
type: command,
bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]',
bodyLong: results.postObj.content,
pid: pid,

View File

@@ -151,7 +151,7 @@ function onMessage(socket, payload) {
function requireModules() {
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist',
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist', 'flags',
];
modules.forEach(function (module) {
@@ -219,7 +219,11 @@ function addRedisAdapter(io) {
var redis = require('../database/redis');
var pub = redis.connect();
var sub = redis.connect();
io.adapter(redisAdapter({ pubClient: pub, subClient: sub }));
io.adapter(redisAdapter({
key: 'db:' + nconf.get('redis:database') + ':adapter_key',
pubClient: pub,
subClient: sub,
}));
} else if (nconf.get('isCluster') === 'true') {
winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.');
}

View File

@@ -3,6 +3,7 @@
var async = require('async');
var validator = require('validator');
var db = require('../database');
var meta = require('../meta');
var notifications = require('../notifications');
var plugins = require('../plugins');
@@ -36,6 +37,17 @@ SocketModules.chats.getRaw = function (socket, data, callback) {
], callback);
};
SocketModules.chats.isDnD = function (socket, uid, callback) {
async.waterfall([
function (next) {
db.getObjectField('user:' + uid, 'status', next);
},
function (status, next) {
next(null, status === 'dnd');
},
], callback);
};
SocketModules.chats.newRoom = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
@@ -133,6 +145,7 @@ SocketModules.chats.loadRoom = function (socket, data, callback) {
results.roomData.groupChat = results.roomData.hasOwnProperty('groupChat') ? results.roomData.groupChat : results.users.length > 2;
results.roomData.isOwner = parseInt(results.roomData.owner, 10) === socket.uid;
results.roomData.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0;
results.roomData.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000;
results.roomData.showUserInput = !results.roomData.maximumUsersInChatRoom || results.roomData.maximumUsersInChatRoom > 2;
next(null, results.roomData);
},

View File

@@ -1,11 +1,8 @@
'use strict';
var async = require('async');
var user = require('../user');
var notifications = require('../notifications');
var utils = require('../utils');
var SocketNotifs = {};
var SocketNotifs = module.exports;
SocketNotifs.get = function (socket, data, callback) {
if (data && Array.isArray(data.nids) && socket.uid) {
@@ -15,25 +12,6 @@ SocketNotifs.get = function (socket, data, callback) {
}
};
SocketNotifs.loadMore = function (socket, data, callback) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) {
return callback(new Error('[[error:invalid-data]]'));
}
if (!socket.uid) {
return callback(new Error('[[error:no-privileges]]'));
}
var start = parseInt(data.after, 10);
var stop = start + 20;
async.waterfall([
function (next) {
user.notifications.getAll(socket.uid, start, stop, next);
},
function (notifications, next) {
next(null, { notifications: notifications, nextStart: stop });
},
], callback);
};
SocketNotifs.getCount = function (socket, data, callback) {
user.notifications.getUnreadCount(socket.uid, callback);
};
@@ -57,5 +35,3 @@ SocketNotifs.markUnread = function (socket, nid, callback) {
SocketNotifs.markAllRead = function (socket, data, callback) {
notifications.markAllRead(socket.uid, callback);
};
module.exports = SocketNotifs;

View File

@@ -20,7 +20,6 @@ require('./posts/move')(SocketPosts);
require('./posts/votes')(SocketPosts);
require('./posts/bookmarks')(SocketPosts);
require('./posts/tools')(SocketPosts);
require('./posts/flag')(SocketPosts);
SocketPosts.reply = function (socket, data, callback) {
if (!data || !data.tid || !data.content) {

View File

@@ -1,171 +0,0 @@
'use strict';
var async = require('async');
var S = require('string');
var user = require('../../user');
var groups = require('../../groups');
var posts = require('../../posts');
var topics = require('../../topics');
var privileges = require('../../privileges');
var notifications = require('../../notifications');
var plugins = require('../../plugins');
var meta = require('../../meta');
var utils = require('../../utils');
module.exports = function (SocketPosts) {
SocketPosts.flag = function (socket, data, callback) {
if (!socket.uid) {
return callback(new Error('[[error:not-logged-in]]'));
}
if (!data || !data.pid || !data.reason) {
return callback(new Error('[[error:invalid-data]]'));
}
var flaggingUser = {};
var post;
async.waterfall([
function (next) {
posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next);
},
function (postData, next) {
if (parseInt(postData.deleted, 10) === 1) {
return next(new Error('[[error:post-deleted]]'));
}
post = postData;
topics.getTopicFields(post.tid, ['title', 'cid'], next);
},
function (topicData, next) {
post.topic = topicData;
async.parallel({
isAdminOrMod: function (next) {
privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next);
},
userData: function (next) {
user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next);
},
}, next);
},
function (user, next) {
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) {
return next(new Error('[[error:not-enough-reputation-to-flag]]'));
}
if (parseInt(user.banned, 10) === 1) {
return next(new Error('[[error:user-banned]]'));
}
flaggingUser = user.userData;
flaggingUser.uid = socket.uid;
posts.flag(post, socket.uid, data.reason, next);
},
function (next) {
async.parallel({
post: function (next) {
posts.parsePost(post, next);
},
admins: function (next) {
groups.getMembers('administrators', 0, -1, next);
},
globalMods: function (next) {
groups.getMembers('Global Moderators', 0, -1, next);
},
moderators: function (next) {
groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next);
},
}, next);
},
function (results, next) {
var title = S(post.topic.title).decodeHTMLEntities().s;
var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
notifications.create({
bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]',
bodyLong: post.content,
pid: data.pid,
path: '/post/' + data.pid,
nid: 'post_flag:' + data.pid + ':uid:' + socket.uid,
from: socket.uid,
mergeId: 'notifications:user_flagged_post_in|' + data.pid,
topicTitle: post.topic.title,
}, function (err, notification) {
if (err || !notification) {
return next(err);
}
plugins.fireHook('action:post.flag', { post: post, reason: data.reason, flaggingUser: flaggingUser });
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next);
});
},
], callback);
};
SocketPosts.dismissFlag = function (socket, pid, callback) {
if (!pid || !socket.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(socket.uid, next);
},
function (isAdminOrGlobalModerator, next) {
if (!isAdminOrGlobalModerator) {
return next(new Error('[[no-privileges]]'));
}
posts.dismissFlag(pid, next);
},
], callback);
};
SocketPosts.dismissAllFlags = function (socket, data, callback) {
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(socket.uid, next);
},
function (isAdminOrGlobalModerator, next) {
if (!isAdminOrGlobalModerator) {
return next(new Error('[[no-privileges]]'));
}
posts.dismissAllFlags(next);
},
], callback);
};
SocketPosts.updateFlag = function (socket, data, callback) {
if (!data || !(data.pid && data.data)) {
return callback(new Error('[[error:invalid-data]]'));
}
var payload = {};
async.waterfall([
function (next) {
async.parallel([
async.apply(user.isAdminOrGlobalMod, socket.uid),
async.apply(user.isModeratorOfAnyCategory, socket.uid),
], function (err, results) {
next(err, results[0] || results[1]);
});
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[no-privileges]]'));
}
// Translate form data into object
payload = data.data.reduce(function (memo, cur) {
memo[cur.name] = cur.value;
return memo;
}, payload);
posts.updateFlagData(socket.uid, data.pid, payload, next);
},
], callback);
};
};

View File

@@ -14,10 +14,14 @@ helpers.postCommand = function (socket, command, eventName, notification, data,
return callback(new Error('[[error:not-logged-in]]'));
}
if (!data || !data.pid || !data.room_id) {
if (!data || !data.pid) {
return callback(new Error('[[error:invalid-data]]'));
}
if (!data.room_id) {
return callback(new Error('[[error:invalid-room-id, ' + data.room_id + ' ]]'));
}
async.waterfall([
function (next) {
async.parallel({

View File

@@ -138,39 +138,46 @@ module.exports = function (SocketPosts) {
return callback(new Error('[[error:invalid-data]]'));
}
var postData;
var topicData;
var isMainAndLast = false;
async.waterfall([
function (next) {
isMainAndLastPost(data.pid, next);
},
function (results, next) {
if (results.isMain && !results.isLast) {
return callback(new Error('[[error:cant-purge-main-post]]'));
return next(new Error('[[error:cant-purge-main-post]]'));
}
if (results.isMain && results.isLast) {
return deleteTopicOf(data.pid, socket, next);
}
setImmediate(next);
isMainAndLast = results.isMain && results.isLast;
posts.getPostFields(data.pid, ['toPid', 'tid'], next);
},
function (next) {
posts.getPostField(data.pid, 'toPid', next);
},
function (toPid, next) {
postData = { pid: data.pid, toPid: toPid };
function (_postData, next) {
postData = _postData;
postData.pid = data.pid;
posts.tools.purge(socket.uid, data.pid, next);
},
function (next) {
websockets.in('topic_' + data.tid).emit('event:post_purged', postData);
topics.getTopicField(data.tid, 'title', next);
topics.getTopicFields(data.tid, ['title', 'cid'], next);
},
function (title, next) {
function (_topicData, next) {
topicData = _topicData;
events.log({
type: 'post-purge',
uid: socket.uid,
pid: data.pid,
ip: socket.ip,
title: String(title),
title: String(topicData.title),
}, next);
},
function (next) {
if (isMainAndLast) {
socketTopics.doTopicAction('purge', 'event:topic_purged', socket, { tids: [postData.tid], cid: topicData.cid }, next);
} else {
setImmediate(next);
}
},
], callback);
};

View File

@@ -40,19 +40,19 @@ module.exports = function (SocketTopics) {
var reverse = data.topicPostSort === 'newest_to_oldest' || data.topicPostSort === 'most_votes';
var start = Math.max(0, parseInt(data.after, 10));
var infScrollPostsPerPage = 10;
var infScrollPostsPerPage = Math.max(0, Math.min(meta.config.postsPerPage || 20, parseInt(data.postsPerPage, 10) || meta.config.postsPerPage || 20) - 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 -= infScrollPostsPerPage;
}
var stop = start + (infScrollPostsPerPage - 1);
var stop = start + (infScrollPostsPerPage);
start = Math.max(0, start);
stop = Math.max(0, stop);
@@ -93,9 +93,9 @@ 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 || 20, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage || 20) - 1);
topics.getUnreadTopics(data.cid, socket.uid, start, stop, data.filter, callback);
topics.getUnreadTopics({ cid: data.cid, uid: socket.uid, start: start, stop: stop, filter: data.filter }, callback);
};
SocketTopics.loadMoreRecentTopics = function (socket, data, 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 || 20, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage || 20) - 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 || 20, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage || 20) - 1);
topics.getTopicsFromSet(data.set, socket.uid, start, stop, callback);
};

View File

@@ -50,7 +50,7 @@ module.exports = function (SocketTopics) {
SocketTopics.markCategoryTopicsRead = function (socket, cid, callback) {
async.waterfall([
function (next) {
topics.getUnreadTids(cid, socket.uid, '', next);
topics.getUnreadTids({ cid: cid, uid: socket.uid, filter: '' }, next);
},
function (tids, next) {
SocketTopics.markAsRead(socket, tids, next);

View File

@@ -171,6 +171,7 @@ SocketUser.follow = function (socket, data, callback) {
function (_userData, next) {
userData = _userData;
notifications.create({
type: 'follow',
bodyShort: '[[notifications:user_started_following_you, ' + userData.username + ']]',
nid: 'follow:' + data.uid + ':uid:' + socket.uid,
from: socket.uid,
@@ -256,6 +257,7 @@ SocketUser.getUnreadCounts = function (socket, data, callback) {
async.parallel({
unreadTopicCount: async.apply(topics.getTotalUnread, socket.uid),
unreadNewTopicCount: async.apply(topics.getTotalUnread, socket.uid, 'new'),
unreadWatchedTopicCount: async.apply(topics.getTotalUnread, socket.uid, 'watched'),
unreadChatCount: async.apply(messaging.getUnreadCount, socket.uid),
unreadNotificationCount: async.apply(user.notifications.getUnreadCount, socket.uid),
}, callback);
@@ -315,7 +317,7 @@ SocketUser.getUserByEmail = function (socket, email, callback) {
};
SocketUser.setModerationNote = function (socket, data, callback) {
if (!socket.uid || !data || !data.uid) {
if (!socket.uid || !data || !data.uid || !data.note) {
return callback(new Error('[[error:invalid-data]]'));
}
@@ -334,11 +336,13 @@ SocketUser.setModerationNote = function (socket, data, callback) {
if (!allowed) {
return next(new Error('[[error:no-privileges]]'));
}
if (data.note) {
user.setUserField(data.uid, 'moderationNote', data.note, next);
} else {
db.deleteObjectField('user:' + data.uid, 'moderationNote', next);
}
var note = {
uid: socket.uid,
note: data.note,
timestamp: Date.now(),
};
db.sortedSetAdd('uid:' + data.uid + ':moderation:notes', note.timestamp, JSON.stringify(note), next);
},
], callback);
};

View File

@@ -1,11 +1,15 @@
'use strict';
var async = require('async');
var user = require('../../user');
var meta = require('../../meta');
var websockets = require('../index');
var events = require('../../events');
var privileges = require('../../privileges');
var plugins = require('../../plugins');
var emailer = require('../../emailer');
var translator = require('../../translator');
module.exports = function (SocketUser) {
SocketUser.banUsers = function (socket, data, callback) {
@@ -35,6 +39,9 @@ module.exports = function (SocketUser) {
});
next();
},
function (next) {
user.auth.revokeAllSessions(uid, next);
},
], next);
}, callback);
};
@@ -72,10 +79,10 @@ module.exports = function (SocketUser) {
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(uid, next);
privileges.users.hasBanPrivilege(uid, next);
},
function (isAdminOrGlobalMod, next) {
if (!isAdminOrGlobalMod) {
function (hasBanPrivilege, next) {
if (!hasBanPrivilege) {
return next(new Error('[[error:no-privileges]]'));
}
async.each(uids, method, next);
@@ -92,10 +99,38 @@ module.exports = function (SocketUser) {
if (isAdmin) {
return next(new Error('[[error:cant-ban-other-admins]]'));
}
user.getUserField(uid, 'username', next);
},
function (username, next) {
var siteTitle = meta.config.title || 'NodeBB';
var data = {
subject: '[[email:banned.subject, ' + siteTitle + ']]',
site_title: siteTitle,
username: username,
until: until ? new Date(until).toString() : false,
reason: reason,
};
emailer.send('banned', uid, data, next);
},
function (next) {
user.ban(uid, until, reason, next);
},
function (next) {
websockets.in('uid_' + uid).emit('event:banned');
if (!reason) {
return translator.translate('[[user:info.banned-no-reason]]', function (translated) {
next(false, translated);
});
}
next(false, reason);
},
function (_reason, next) {
websockets.in('uid_' + uid).emit('event:banned', {
until: until,
reason: _reason,
});
next();
},
], callback);

View File

@@ -99,6 +99,9 @@ module.exports = function (SocketUser) {
picture: userData.uploadedpicture === userData.picture ? '' : userData.picture, // if current picture is uploaded picture, reset to user icon
}, next);
},
function (next) {
plugins.fireHook('action:user.removeUploadedPicture', { callerUid: socket.uid, uid: data.uid }, next);
},
], callback);
};

View File

@@ -161,7 +161,7 @@ module.exports = function (SocketUser) {
data.email = oldUserData.email;
}
user.updateProfile(data.uid, data, next);
user.updateProfile(socket.uid, data, next);
},
function (userData, next) {
function log(type, eventData) {

View File

@@ -48,7 +48,7 @@ start.start = function () {
require('./socket.io').init(webserver.server);
if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) {
require('./notifications').init();
require('./notifications').startJobs();
require('./user').startJobs();
}
@@ -58,16 +58,16 @@ start.start = function () {
if (err) {
switch (err.message) {
case 'schema-out-of-date':
winston.warn('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:');
winston.warn(' ./nodebb upgrade');
winston.error('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:');
winston.error(' ./nodebb upgrade');
break;
case 'dependencies-out-of-date':
winston.warn('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:');
winston.warn(' ./nodebb upgrade');
winston.error('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:');
winston.error(' ./nodebb upgrade');
break;
case 'dependencies-missing':
winston.warn('One or more of NodeBB\'s dependent packages are missing. Please run the following command to update them:');
winston.warn(' ./nodebb upgrade');
winston.error('One or more of NodeBB\'s dependent packages are missing. Please run the following command to update them:');
winston.error(' ./nodebb upgrade');
break;
default:
winston.error(err);

View File

@@ -2,6 +2,7 @@
'use strict';
var async = require('async');
var _ = require('underscore');
var validator = require('validator');
var S = require('string');
var db = require('../database');
@@ -81,7 +82,7 @@ module.exports = function (Topics) {
], next);
},
function (results, next) {
plugins.fireHook('action:topic.save', topicData);
plugins.fireHook('action:topic.save', { topic: _.clone(topicData) });
next(null, topicData.tid);
},
], callback);
@@ -177,7 +178,7 @@ module.exports = function (Topics) {
data.postData.index = 0;
analytics.increment(['topics', 'topics:byCid:' + data.topicData.cid]);
plugins.fireHook('action:topic.post', data.topicData);
plugins.fireHook('action:topic.post', { topic: data.topicData, post: data.postData });
if (parseInt(uid, 10)) {
user.notifications.sendTopicNotificationToFollowers(uid, data.topicData, data.postData);
@@ -272,7 +273,7 @@ module.exports = function (Topics) {
Topics.notifyFollowers(postData, uid);
analytics.increment(['posts', 'posts:byCid:' + cid]);
plugins.fireHook('action:topic.reply', postData);
plugins.fireHook('action:topic.reply', { post: _.clone(postData) });
next(null, postData);
},

View File

@@ -154,7 +154,10 @@ module.exports = function (Topics) {
});
},
function (next) {
plugins.fireHook('action:topic.purge', tid);
Topics.getTopicData(tid, next);
},
function (topicData, next) {
plugins.fireHook('action:topic.purge', { topic: topicData, uid: uid });
db.delete('topic:' + tid, next);
},
], callback);

View File

@@ -162,27 +162,31 @@ module.exports = function (Topics) {
};
Topics.filterWatchedTids = function (tids, uid, callback) {
db.sortedSetScores('uid:' + uid + ':followed_tids', tids, function (err, scores) {
if (err) {
return callback(err);
}
tids = tids.filter(function (tid, index) {
return tid && !!scores[index];
});
callback(null, tids);
});
async.waterfall([
function (next) {
db.sortedSetScores('uid:' + uid + ':followed_tids', tids, next);
},
function (scores, next) {
tids = tids.filter(function (tid, index) {
return tid && !!scores[index];
});
next(null, tids);
},
], callback);
};
Topics.filterNotIgnoredTids = function (tids, uid, callback) {
db.sortedSetScores('uid:' + uid + ':ignored_tids', tids, function (err, scores) {
if (err) {
return callback(err);
}
tids = tids.filter(function (tid, index) {
return tid && !scores[index];
});
callback(null, tids);
});
async.waterfall([
function (next) {
db.sortedSetScores('uid:' + uid + ':ignored_tids', tids, next);
},
function (scores, next) {
tids = tids.filter(function (tid, index) {
return tid && !scores[index];
});
next(null, tids);
},
], callback);
};
Topics.notifyFollowers = function (postData, exceptUid, callback) {
@@ -224,6 +228,7 @@ module.exports = function (Topics) {
postData.content = posts.relativeToAbsolute(postData.content);
notifications.create({
type: 'new-reply',
bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]',
bodyLong: postData.content,
pid: postData.pid,

Some files were not shown because too many files have changed in this diff Show More