Merge commit '813fdaf6f6b8cefa4fd7336d8888af9aefa01905' into v1.7.x

This commit is contained in:
Misty (Bot)
2018-01-24 21:14:43 +00:00
688 changed files with 4482 additions and 1970 deletions

View File

@@ -338,30 +338,27 @@ Categories.buildForSelect = function (uid, privilege, callback) {
};
Categories.buildForSelectCategories = function (categories, callback) {
function recursive(category, categoriesData, level) {
if (category.link) {
return;
}
function recursive(category, categoriesData, level, depth) {
var bullet = level ? '• ' : '';
category.value = category.cid;
category.level = level;
category.text = level + bullet + category.name;
category.depth = depth;
categoriesData.push(category);
category.children.forEach(function (child) {
recursive(child, categoriesData, '    ' + level);
recursive(child, categoriesData, '    ' + level, depth + 1);
});
}
var categoriesData = [];
categories = categories.filter(function (category) {
return category && !category.link && !parseInt(category.parentCid, 10);
return category && !parseInt(category.parentCid, 10);
});
categories.forEach(function (category) {
recursive(category, categoriesData, '');
recursive(category, categoriesData, '', 0);
});
callback(null, categoriesData);
};

View File

@@ -61,8 +61,9 @@ module.exports = function (Categories) {
'topics:tag',
'posts:edit',
'posts:delete',
'posts:upvote',
'posts:downvote',
'topics:delete',
'upload:post:image',
];
async.series([

View File

@@ -31,7 +31,11 @@ module.exports = function (Categories) {
category.name = validator.escape(String(category.name || ''));
category.disabled = category.hasOwnProperty('disabled') ? parseInt(category.disabled, 10) === 1 : undefined;
category.isSection = category.hasOwnProperty('isSection') ? parseInt(category.isSection, 10) === 1 : undefined;
category.icon = category.icon || 'hidden';
if (category.hasOwnProperty('icon')) {
category.icon = category.icon || 'hidden';
}
if (category.hasOwnProperty('post_count')) {
category.post_count = category.post_count || 0;
category.totalPostCount = category.post_count;

View File

@@ -8,7 +8,7 @@ var dirname = require('./paths').baseDir;
// check to make sure dependencies are installed
try {
fs.readFileSync(path.join(dirname, 'package.json'));
fs.accessSync(path.join(dirname, 'package.json'), fs.constants.R_OK);
} catch (e) {
if (e.code === 'ENOENT') {
console.warn('package.json not found.');
@@ -18,6 +18,8 @@ try {
packageInstall.preserveExtraneousPlugins();
try {
fs.accessSync(path.join(dirname, 'node_modules/colors/package.json'), fs.constants.R_OK);
require('colors');
console.log('OK'.green);
} catch (e) {
@@ -29,15 +31,30 @@ try {
}
try {
fs.readFileSync(path.join(dirname, 'node_modules/async/package.json'), 'utf8');
fs.readFileSync(path.join(dirname, 'node_modules/commander/package.json'), 'utf8');
fs.readFileSync(path.join(dirname, 'node_modules/colors/package.json'), 'utf8');
fs.readFileSync(path.join(dirname, 'node_modules/nconf/package.json'), 'utf8');
fs.accessSync(path.join(dirname, 'node_modules/semver/package.json'), fs.constants.R_OK);
var semver = require('semver');
var defaultPackage = require('../../install/package.json');
var checkVersion = function (packageName) {
var version = JSON.parse(fs.readFileSync(path.join(dirname, 'node_modules', packageName, 'package.json'), 'utf8')).version;
if (!semver.satisfies(version, defaultPackage.dependencies[packageName])) {
var e = new TypeError('Incorrect dependency version: ' + packageName);
e.code = 'DEP_WRONG_VERSION';
throw e;
}
};
checkVersion('nconf');
checkVersion('async');
checkVersion('commander');
checkVersion('colors');
} catch (e) {
if (e.code === 'ENOENT') {
console.warn('Dependencies not yet installed.');
if (['ENOENT', 'DEP_WRONG_VERSION', 'MODULE_NOT_FOUND'].indexOf(e.code) !== -1) {
console.warn('Dependencies outdated or not yet installed.');
console.log('Installing them now...\n');
packageInstall.updatePackageFile();
packageInstall.installAll();
require('colors');
@@ -241,7 +258,7 @@ program
'When running particular upgrade scripts, options are ignored.',
'By default all options are enabled. Passing any options disables that default.',
'Only package and dependency updates: ' + './nodebb upgrade -mi'.yellow,
'Only database update: ' + './nodebb upgrade -d'.yellow,
'Only database update: ' + './nodebb upgrade -s'.yellow,
].join('\n'));
})
.action(function (scripts, options) {
@@ -280,16 +297,12 @@ program
}
});
program
.command('*', {}, {
noHelp: true,
})
.action(function () {
program.help();
});
require('./colors');
if (process.argv.length === 2) {
program.help();
}
program.executables = false;
program.parse(process.argv);

View File

@@ -33,6 +33,7 @@ function installAll() {
var prod = global.env !== 'development';
var command = 'npm install';
try {
fs.accessSync(path.join(modulesPath, 'nconf/package.json'), fs.constants.R_OK);
var packageManager = require('nconf').get('package_manager');
if (packageManager === 'yarn') {
command = 'yarn';

View File

@@ -23,6 +23,7 @@ var steps = {
install: {
message: 'Bringing base dependencies up to date...',
handler: function (next) {
process.stdout.write(' started\n'.green);
packageInstall.installAll();
next();
},

View File

@@ -5,6 +5,7 @@ var async = require('async');
var messaging = require('../../messaging');
var meta = require('../../meta');
var user = require('../../user');
var privileges = require('../../privileges');
var helpers = require('../helpers');
var chatsController = module.exports;
@@ -26,6 +27,13 @@ chatsController.get = function (req, res, callback) {
if (!uid) {
return callback();
}
privileges.global.can('chat', req.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
messaging.getRecentChats(req.uid, uid, 0, 19, next);
},
function (_recentChats, next) {

View File

@@ -28,6 +28,9 @@ 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.allowWebsite = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:website'], 10) || 0);
userData.allowAboutMe = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:aboutme'], 10) || 0);
userData.allowSignature = !userData.isSelf || parseInt(userData.reputation, 10) >= (parseInt(meta.config['min:rep:signature'], 10) || 0);
userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 200;
userData.defaultAvatar = user.getDefaultAvatar();

View File

@@ -62,6 +62,9 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
sso: function (next) {
plugins.fireHook('filter:auth.list', { uid: uid, associations: [] }, next);
},
canEdit: function (next) {
privileges.users.canEdit(callerUID, uid, next);
},
canBanUser: function (next) {
privileges.users.canBanUser(callerUID, uid, next);
},
@@ -113,7 +116,7 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
userData.isAdminOrGlobalModerator = isAdmin || isGlobalModerator;
userData.isAdminOrGlobalModeratorOrModerator = isAdmin || isGlobalModerator || isModerator;
userData.isSelfOrAdminOrGlobalModerator = isSelf || isAdmin || isGlobalModerator;
userData.canEdit = isAdmin || (isGlobalModerator && !results.isTargetAdmin);
userData.canEdit = results.canEdit;
userData.canBan = results.canBanUser;
userData.canChangePassword = isAdmin || (isSelf && parseInt(meta.config['password:disableEdit'], 10) !== 1);
userData.isSelf = isSelf;

View File

@@ -3,6 +3,8 @@
var adminController = {
dashboard: require('./admin/dashboard'),
categories: require('./admin/categories'),
privileges: require('./admin/privileges'),
adminsMods: require('./admin/admins-mods'),
tags: require('./admin/tags'),
postQueue: require('./admin/postqueue'),
blacklist: require('./admin/blacklist'),

View File

@@ -0,0 +1,50 @@
'use strict';
var async = require('async');
var groups = require('../../groups');
var categories = require('../../categories');
var AdminsMods = module.exports;
AdminsMods.get = function (req, res, next) {
async.waterfall([
function (next) {
async.parallel({
admins: function (next) {
groups.get('administrators', { uid: req.uid }, next);
},
globalMods: function (next) {
groups.get('Global Moderators', { uid: req.uid }, next);
},
categories: function (next) {
getModeratorsOfCategories(req.uid, next);
},
}, next);
},
function (results) {
res.render('admin/manage/admins-mods', results);
},
], next);
};
function getModeratorsOfCategories(uid, callback) {
async.waterfall([
function (next) {
categories.buildForSelect(uid, 'find', next);
},
function (categoryData, next) {
async.map(categoryData, function (category, next) {
async.waterfall([
function (next) {
categories.getModerators(category.cid, next);
},
function (moderators, next) {
category.moderators = moderators;
next(null, category);
},
], next);
}, next);
},
], callback);
}

View File

@@ -3,7 +3,6 @@
var async = require('async');
var categories = require('../../categories');
var privileges = require('../../privileges');
var analytics = require('../../analytics');
var plugins = require('../../plugins');
var translator = require('../../translator');
@@ -15,7 +14,6 @@ categoriesController.get = function (req, res, callback) {
function (next) {
async.parallel({
category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid),
privileges: async.apply(privileges.categories.list, req.params.category_id),
allCategories: async.apply(categories.buildForSelect, req.uid, 'read'),
}, next);
},
@@ -36,7 +34,6 @@ categoriesController.get = function (req, res, callback) {
req: req,
res: res,
category: category,
privileges: data.privileges,
allCategories: data.allCategories,
}, next);
},
@@ -44,7 +41,6 @@ categoriesController.get = function (req, res, callback) {
data.category.name = translator.escape(String(data.category.name));
res.render('admin/manage/category', {
category: data.category,
privileges: data.privileges,
allCategories: data.allCategories,
});
},

View File

@@ -48,6 +48,7 @@ dashboardController.get = function (req, res, next) {
version: nconf.get('version'),
notices: results.notices,
stats: results.stats,
canRestart: !!process.send,
});
},
], next);

View File

@@ -0,0 +1,39 @@
'use strict';
var async = require('async');
var categories = require('../../categories');
var privileges = require('../../privileges');
var privilegesController = module.exports;
privilegesController.get = function (req, res, callback) {
var cid = req.params.cid ? req.params.cid : 0;
async.waterfall([
function (next) {
async.parallel({
privileges: function (next) {
if (!cid) {
privileges.global.list(next);
} else {
privileges.categories.list(cid, next);
}
},
allCategories: async.apply(categories.buildForSelect, req.uid, 'read'),
}, next);
},
function (data) {
data.allCategories.forEach(function (category) {
if (category) {
category.selected = parseInt(category.cid, 10) === parseInt(cid, 10);
}
});
res.render('admin/manage/privileges', {
privileges: data.privileges,
allCategories: data.allCategories,
cid: cid,
});
},
], callback);
};

View File

@@ -129,7 +129,7 @@ helpers.buildCategoryBreadcrumbs = function (cid, callback) {
return callback(err);
}
if (!meta.config.homePageRoute && meta.config.homePageCustom) {
if (meta.config.homePageRoute && meta.config.homePageRoute !== 'categories') {
breadcrumbs.unshift({
text: '[[global:header.categories]]',
url: nconf.get('relative_path') + '/categories',

View File

@@ -1,61 +1,56 @@
'use strict';
var async = require('async');
var plugins = require('../plugins');
var meta = require('../meta');
var user = require('../user');
var pubsub = require('../pubsub');
var adminHomePageRoute;
var getRoute;
function configUpdated() {
adminHomePageRoute = (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories';
getRoute = parseInt(meta.config.allowUserHomePage, 10) ? getRouteAllowUserHomePage : getRouteDisableUserHomePage;
function adminHomePageRoute() {
return (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories';
}
function getRouteDisableUserHomePage(uid, next) {
next(null, adminHomePageRoute);
function getUserHomeRoute(uid, callback) {
async.waterfall([
function (next) {
user.getSettings(uid, next);
},
function (settings, next) {
var route = adminHomePageRoute();
if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') {
route = settings.homePageRoute || route;
}
next(null, route);
},
], callback);
}
function getRouteAllowUserHomePage(uid, next) {
user.getSettings(uid, function (err, settings) {
if (err) {
return next(err);
}
var route = adminHomePageRoute;
if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') {
route = settings.homePageRoute || route;
}
next(null, route);
});
}
pubsub.on('config:update', configUpdated);
configUpdated();
function rewrite(req, res, next) {
if (req.path !== '/' && req.path !== '/api/' && req.path !== '/api') {
return next();
}
getRoute(req.uid, function (err, route) {
if (err) {
return next(err);
}
async.waterfall([
function (next) {
if (parseInt(meta.config.allowUserHomePage, 10)) {
getUserHomeRoute(req.uid, next);
} else {
next(null, adminHomePageRoute());
}
},
function (route, next) {
var hook = 'action:homepage.get:' + route;
var hook = 'action:homepage.get:' + route;
if (!plugins.hasListeners(hook)) {
req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + route;
} else {
res.locals.homePageRoute = route;
}
if (!plugins.hasListeners(hook)) {
req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + route;
} else {
res.locals.homePageRoute = route;
}
next();
});
next();
},
], next);
}
exports.rewrite = rewrite;

View File

@@ -126,7 +126,7 @@ modsController.flags.detail = function (req, res, next) {
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;
memo[cur] = results.flagData.type === cur && (!results.flagData.target || !!Object.keys(results.flagData.target).length);
} else {
memo[cur] = !Object.keys(results.flagData.target).length;
}

View File

@@ -54,6 +54,10 @@ searchController.search = function (req, res, next) {
return next(err);
}
results.categories = results.categories.filter(function (category) {
return category && !category.link;
});
var categoriesData = [
{ value: 'all', text: '[[unread:all_categories]]' },
{ value: 'watched', text: '[[category:watched-categories]]' },

View File

@@ -8,7 +8,7 @@ var topics = require('../topics');
var pagination = require('../pagination');
var helpers = require('./helpers');
var tagsController = {};
var tagsController = module.exports;
tagsController.getTag = function (req, res, next) {
var tag = validator.escape(String(req.params.tag));
@@ -33,7 +33,7 @@ tagsController.getTag = function (req, res, next) {
templateData.nextStart = stop + 1;
async.parallel({
topicCount: function (next) {
topics.getTagTopicCount(tag, next);
topics.getTagTopicCount(req.params.tag, next);
},
tids: function (next) {
topics.getTagTids(req.params.tag, start, stop, next);
@@ -47,44 +47,41 @@ tagsController.getTag = function (req, res, next) {
topicCount = results.topicCount;
topics.getTopics(results.tids, req.uid, next);
},
], function (err, topics) {
if (err) {
return next(err);
}
function (topics) {
res.locals.metaTags = [
{
name: 'title',
content: tag,
},
{
property: 'og:title',
content: tag,
},
];
templateData.topics = topics;
res.locals.metaTags = [
{
name: 'title',
content: tag,
},
{
property: 'og:title',
content: tag,
},
];
templateData.topics = topics;
var pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage));
templateData.pagination = pagination.create(page, pageCount);
var pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage));
templateData.pagination = pagination.create(page, pageCount);
res.render('tag', templateData);
});
res.render('tag', templateData);
},
], next);
};
tagsController.getTags = function (req, res, next) {
topics.getTags(0, 99, function (err, tags) {
if (err) {
return next(err);
}
tags = tags.filter(Boolean);
var data = {
tags: tags,
nextStart: 100,
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[tags:tags]]' }]),
title: '[[pages:tags]]',
};
res.render('tags', data);
});
async.waterfall([
function (next) {
topics.getTags(0, 99, next);
},
function (tags) {
tags = tags.filter(Boolean);
var data = {
tags: tags,
nextStart: 100,
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[tags:tags]]' }]),
title: '[[pages:tags]]',
};
res.render('tags', data);
},
], next);
};
module.exports = tagsController;

View File

@@ -387,7 +387,7 @@ topicsController.pagination = function (req, res, callback) {
}
var postCount = parseInt(results.topic.postcount, 10);
var pageCount = Math.max(1, Math.ceil((postCount - 1) / results.settings.postsPerPage));
var pageCount = Math.max(1, Math.ceil(postCount / results.settings.postsPerPage));
var paginationData = pagination.create(currentPage, pageCount);
paginationData.rel.forEach(function (rel) {

View File

@@ -37,9 +37,6 @@ uploadsController.upload = function (req, res, filesIterator) {
uploadsController.uploadPost = function (req, res, next) {
uploadsController.upload(req, res, function (uploadedFile, next) {
if (!parseInt(req.body.cid, 10)) {
return next(new Error('[[error:category-not-selected]]'));
}
var isImage = uploadedFile.type.match(/image./);
if (isImage) {
uploadAsImage(req, uploadedFile, next);
@@ -52,7 +49,7 @@ uploadsController.uploadPost = function (req, res, next) {
function uploadAsImage(req, uploadedFile, callback) {
async.waterfall([
function (next) {
privileges.categories.can('upload:post:image', req.body.cid, req.uid, next);
privileges.global.can('upload:post:image', req.uid, next);
},
function (canUpload, next) {
if (!canUpload) {
@@ -82,7 +79,7 @@ function uploadAsImage(req, uploadedFile, callback) {
function uploadAsFile(req, uploadedFile, callback) {
async.waterfall([
function (next) {
privileges.categories.can('upload:post:file', req.body.cid, req.uid, next);
privileges.global.can('upload:post:file', req.uid, next);
},
function (canUpload, next) {
if (!canUpload) {

View File

@@ -83,8 +83,8 @@ module.exports = function (db, module) {
if (!key) {
return callback();
}
db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { value: 1 } }, { new: true, upsert: true }, function (err, result) {
callback(err, result && result.value ? result.value.value : null);
db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { data: 1 } }, { new: true, upsert: true }, function (err, result) {
callback(err, result && result.value ? result.value.data : null);
});
};
@@ -108,6 +108,7 @@ module.exports = function (db, module) {
if (!data) {
return callback(null, null);
}
delete data.expireAt;
var keys = Object.keys(data);
if (keys.length === 4 && data.hasOwnProperty('_key') && data.hasOwnProperty('score') && data.hasOwnProperty('value')) {
return callback(null, 'zset');

View File

@@ -37,19 +37,22 @@ redisModule.questions = [
];
redisModule.init = function (callback) {
redisClient = redisModule.connect();
callback = callback || function () { };
redisClient = redisModule.connect({}, function (err) {
if (err) {
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error', err);
return callback(err);
}
redisModule.client = redisClient;
redisModule.client = redisClient;
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);
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);
if (typeof callback === 'function') {
callback();
}
});
};
redisModule.initSessionStore = function (callback) {
@@ -66,7 +69,8 @@ redisModule.initSessionStore = function (callback) {
}
};
redisModule.connect = function (options) {
redisModule.connect = function (options, callback) {
callback = callback || function () {};
var redis_socket_or_host = nconf.get('redis:host');
var cxn;
@@ -88,7 +92,11 @@ redisModule.connect = function (options) {
cxn.on('error', function (err) {
winston.error(err.stack);
process.exit(1);
callback(err);
});
cxn.on('ready', function () {
callback();
});
if (nconf.get('redis:password')) {
@@ -99,7 +107,7 @@ redisModule.connect = function (options) {
if (dbIdx >= 0) {
cxn.select(dbIdx, function (err) {
if (err) {
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error', err);
winston.error('NodeBB could not select Redis database. Redis returned the following error', err);
throw err;
}
});

View File

@@ -10,6 +10,7 @@ var htmlToText = require('html-to-text');
var url = require('url');
var path = require('path');
var fs = require('fs');
var _ = require('lodash');
var User = require('./user');
var Plugins = require('./plugins');
@@ -289,11 +290,10 @@ function buildCustomTemplates(config) {
file.walk(viewsDir, next);
},
function (paths, next) {
paths = paths.reduce(function (obj, p) {
var relative = path.relative(viewsDir, p);
obj['/' + relative] = p;
return obj;
}, {});
paths = _.fromPairs(paths.map(function (p) {
var relative = path.relative(viewsDir, p).replace(/\\/g, '/');
return [relative, p];
}));
meta.templates.processImports(paths, template.path, template.text, next);
},
function (source, next) {

View File

@@ -241,7 +241,7 @@ Flags.validate = function (payload, callback) {
return callback(err);
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 0;
var minimumReputation = utils.isNumber(meta.config['min:rep:flag']) ? parseInt(meta.config['min:rep:flag'], 10) : 0;
// 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]]'));
@@ -257,7 +257,7 @@ Flags.validate = function (payload, callback) {
return callback(err);
}
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 0;
var minimumReputation = utils.isNumber(meta.config['min:rep:flag']) ? parseInt(meta.config['min:rep:flag'], 10) : 0;
// 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]]'));
@@ -387,7 +387,7 @@ Flags.create = function (type, id, uid, reason, timestamp, callback) {
tasks.push(async.apply(Flags.update, flagId, uid, { state: 'open' }));
}
async.parallel(tasks, function (err) {
async.series(tasks, function (err) {
next(err, flagId);
});
},

View File

@@ -90,11 +90,11 @@ module.exports = function (Groups) {
return callback(new Error('[[error:group-name-too-long]]'));
}
if (!Groups.isPrivilegeGroup(name) && name.indexOf(':') !== -1) {
if (!Groups.isPrivilegeGroup(name) && name.includes(':')) {
return callback(new Error('[[error:invalid-group-name]]'));
}
if (name.indexOf('/') !== -1 || !utils.slugify(name)) {
if (name.includes('/') || !utils.slugify(name)) {
return callback(new Error('[[error:invalid-group-name]]'));
}

View File

@@ -157,16 +157,17 @@ function completeConfigSetup(config, next) {
}
}
nconf.overrides(config);
async.waterfall([
function (next) {
install.save(config, next);
},
function (next) {
require('./database').init(next);
},
function (next) {
require('./database').createIndices(next);
},
function (next) {
install.save(config, next);
},
], next);
}
@@ -353,6 +354,11 @@ function createGlobalModeratorsGroup(next) {
], next);
}
function giveGlobalPrivileges(next) {
var privileges = require('./privileges');
privileges.global.give(['chat', 'upload:post:image'], 'registered-users', next);
}
function createCategories(next) {
var Categories = require('./categories');
@@ -498,6 +504,7 @@ install.setup = function (callback) {
createCategories,
createAdministrator,
createGlobalModeratorsGroup,
giveGlobalPrivileges,
createMenuItems,
createWelcomePost,
enableDefaultPlugins,
@@ -517,7 +524,7 @@ install.setup = function (callback) {
], function (err, results) {
if (err) {
winston.warn('NodeBB Setup Aborted.\n ' + err.stack);
process.exit();
process.exit(1);
} else {
var data = {};
if (results[6]) {

View File

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

View File

@@ -143,6 +143,11 @@ function build(targets, callback) {
target = target.toLowerCase().replace(/-/g, '');
if (!aliases[target]) {
winston.warn('[build] Unknown target: ' + target);
if (target.indexOf(',') !== -1) {
winston.warn('[build] Are you specifying multiple targets? Separate them with spaces:');
winston.warn('[build] e.g. `./nodebb build adminjs tpl`');
}
return false;
}

View File

@@ -14,6 +14,7 @@ var JS = module.exports;
JS.scripts = {
base: [
'node_modules/promise-polyfill/dist/polyfill.js',
'node_modules/jquery/dist/jquery.js',
'node_modules/socket.io-client/dist/socket.io.js',
'public/vendor/jquery/timeago/jquery.timeago.js',
@@ -36,7 +37,6 @@ JS.scripts = {
'public/src/ajaxify.js',
'public/src/overrides.js',
'public/src/widgets.js',
'node_modules/promise-polyfill/promise.js',
],
// files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load
@@ -343,6 +343,11 @@ JS.buildBundle = function (target, fork, callback) {
function (next) {
getBundleScriptList(target, next);
},
function (files, next) {
mkdirp(path.join(__dirname, '../../build/public'), function (err) {
next(err, files);
});
},
function (files, next) {
var minify = global.env !== 'development';
var filePath = path.join(__dirname, '../../build/public', fileNames[target]);

View File

@@ -75,7 +75,7 @@ function forkAction(action, callback) {
freeChild(proc);
if (message.type === 'error') {
return callback(message.err);
return callback(message.message);
}
if (message.type === 'end') {
@@ -103,7 +103,7 @@ if (process.env.minifier_child) {
if (typeof actions[action.act] !== 'function') {
process.send({
type: 'error',
err: Error('Unknown action'),
message: 'Unknown action',
});
return;
}
@@ -112,7 +112,7 @@ if (process.env.minifier_child) {
if (err) {
process.send({
type: 'error',
err: err,
message: err.stack,
});
return;
}

View File

@@ -7,6 +7,7 @@ var async = require('async');
var path = require('path');
var fs = require('fs');
var nconf = require('nconf');
var _ = require('lodash');
var plugins = require('../plugins');
var file = require('../file');
@@ -24,7 +25,7 @@ function processImports(paths, templatePath, source, callback) {
return callback(null, source);
}
var partial = '/' + matches[1];
var partial = matches[1];
if (paths[partial] && templatePath !== partial) {
fs.readFile(paths[partial], 'utf8', function (err, partialSource) {
if (err) {
@@ -43,124 +44,108 @@ function processImports(paths, templatePath, source, callback) {
}
Templates.processImports = processImports;
Templates.compile = function (callback) {
callback = callback || function () {};
function getTemplateDirs(callback) {
var pluginTemplates = _.values(plugins.pluginsData)
.filter(function (pluginData) {
return !pluginData.id.startsWith('nodebb-theme-');
})
.map(function (pluginData) {
return path.join(__dirname, '../../node_modules/', pluginData.id, pluginData.templates || 'templates');
});
var themeConfig = require(nconf.get('theme_config'));
var baseTemplatesPaths = themeConfig.baseTheme ? getBaseTemplates(themeConfig.baseTheme) : [nconf.get('base_templates_path')];
var theme = themeConfig.baseTheme;
var themePath;
var themeTemplates = [nconf.get('theme_templates_path')];
while (theme) {
themePath = path.join(nconf.get('themes_path'), theme);
themeConfig = require(path.join(themePath, 'theme.json'));
themeTemplates.push(path.join(themePath, themeConfig.templates || 'templates'));
theme = themeConfig.baseTheme;
}
themeTemplates.push(nconf.get('base_templates_path'));
themeTemplates = _.uniq(themeTemplates.reverse());
var coreTemplatesPath = nconf.get('core_templates_path');
var templateDirs = _.uniq([coreTemplatesPath].concat(themeTemplates, pluginTemplates));
async.filter(templateDirs, file.exists, callback);
}
function getTemplateFiles(dirs, callback) {
async.waterfall([
function (cb) {
async.map(dirs, function (dir, next) {
file.walk(dir, function (err, files) {
if (err) { return next(err); }
files = files.filter(function (path) {
return path.endsWith('.tpl');
}).map(function (file) {
return {
name: path.relative(dir, file).replace(/\\/g, '/'),
path: file,
};
});
next(null, files);
});
}, cb);
},
function (buckets, cb) {
var dict = {};
buckets.forEach(function (files) {
files.forEach(function (file) {
dict[file.name] = file.path;
});
});
cb(null, dict);
},
], callback);
}
function compile(callback) {
callback = callback || function () {};
async.waterfall([
function (next) {
preparePaths(baseTemplatesPaths, next);
rimraf(viewsPath, function (err) { next(err); });
},
function (paths, next) {
async.each(Object.keys(paths), function (relativePath, next) {
function (next) {
mkdirp(viewsPath, function (err) { next(err); });
},
getTemplateDirs,
getTemplateFiles,
function (files, next) {
async.each(Object.keys(files), function (name, next) {
var filePath = files[name];
async.waterfall([
function (next) {
fs.readFile(paths[relativePath], 'utf8', next);
fs.readFile(filePath, 'utf8', next);
},
function (source, next) {
processImports(paths, relativePath, source, next);
processImports(files, name, source, next);
},
function (source, next) {
mkdirp(path.join(viewsPath, path.dirname(relativePath)), function (err) {
mkdirp(path.join(viewsPath, path.dirname(name)), function (err) {
next(err, source);
});
},
function (compiled, next) {
fs.writeFile(path.join(viewsPath, relativePath), compiled, next);
fs.writeFile(path.join(viewsPath, name), compiled, next);
},
], next);
}, next);
},
function (next) {
rimraf(path.join(viewsPath, '*.js'), next);
},
function (next) {
winston.verbose('[meta/templates] Successfully compiled templates.');
next();
},
], callback);
};
function getBaseTemplates(theme) {
var baseTemplatesPaths = [];
var baseThemePath;
var baseThemeConfig;
while (theme) {
baseThemePath = path.join(nconf.get('themes_path'), theme);
baseThemeConfig = require(path.join(baseThemePath, 'theme.json'));
baseTemplatesPaths.push(path.join(baseThemePath, baseThemeConfig.templates || 'templates'));
theme = baseThemeConfig.baseTheme;
}
return baseTemplatesPaths.reverse();
}
function preparePaths(baseTemplatesPaths, callback) {
var coreTemplatesPath = nconf.get('core_templates_path');
var pluginTemplates;
async.waterfall([
function (next) {
rimraf(viewsPath, next);
},
function (next) {
mkdirp(viewsPath, next);
},
function (viewsPath, next) {
plugins.fireHook('static:templates.precompile', {}, next);
},
function (next) {
plugins.getTemplates(next);
},
function (_pluginTemplates, next) {
pluginTemplates = _pluginTemplates;
winston.verbose('[meta/templates] Compiling templates');
async.parallel({
coreTpls: function (next) {
file.walk(coreTemplatesPath, next);
},
baseThemes: function (next) {
async.map(baseTemplatesPaths, function (baseTemplatePath, next) {
file.walk(baseTemplatePath, function (err, paths) {
paths = paths.map(function (tpl) {
return {
base: baseTemplatePath,
path: tpl.replace(baseTemplatePath, ''),
};
});
next(err, paths);
});
}, next);
},
}, next);
},
function (data, next) {
var baseThemes = data.baseThemes;
var coreTpls = data.coreTpls;
var paths = {};
coreTpls.forEach(function (el, i) {
paths[coreTpls[i].replace(coreTemplatesPath, '')] = coreTpls[i];
});
baseThemes.forEach(function (baseTpls) {
baseTpls.forEach(function (el, i) {
paths[baseTpls[i].path] = path.join(baseTpls[i].base, baseTpls[i].path);
});
});
for (var tpl in pluginTemplates) {
if (pluginTemplates.hasOwnProperty(tpl)) {
paths[tpl] = pluginTemplates[tpl];
}
}
next(null, paths);
},
], callback);
}
Templates.compile = compile;

View File

@@ -12,6 +12,7 @@ var meta = require('../meta');
var plugins = require('../plugins');
var navigation = require('../navigation');
var translator = require('../translator');
var privileges = require('../privileges');
var utils = require('../utils');
var controllers = {
@@ -77,6 +78,9 @@ module.exports = function (middleware) {
isModerator: function (next) {
user.isModeratorOfAnyCategory(req.uid, next);
},
privileges: function (next) {
privileges.global.get(req.uid, next);
},
user: function (next) {
var userData = {
uid: 0,
@@ -132,6 +136,8 @@ module.exports = function (middleware) {
results.user.isAdmin = results.isAdmin;
results.user.isGlobalMod = results.isGlobalMod;
results.user.isMod = !!results.isModerator;
results.user.privileges = results.privileges;
results.user.uid = parseInt(results.user.uid, 10);
results.user.email = String(results.user.email);
results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1;
@@ -183,6 +189,7 @@ module.exports = function (middleware) {
templateValues.isAdmin = results.user.isAdmin;
templateValues.isGlobalMod = results.user.isGlobalMod;
templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod;
templateValues.canChat = results.canChat && parseInt(meta.config.disableChat, 10) !== 1;
templateValues.user = results.user;
templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true });
templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS;

View File

@@ -220,7 +220,7 @@ function pushToUids(uids, notification, callback) {
async.eachLimit(uids, 3, function (uid, next) {
emailer.send('notification', uid, {
path: notification.path,
subject: '[[notifications:new_notification_from, ' + meta.config.title + ']]',
subject: notification.subject || '[[notifications:new_notification_from, ' + meta.config.title + ']]',
intro: utils.stripHTMLTags(notification.bodyShort),
body: utils.stripHTMLTags(notification.bodyLong || ''),
showUnsubscribe: true,

View File

@@ -138,10 +138,13 @@ Plugins.reloadRoutes = function (callback) {
});
};
// DEPRECATED: remove in v1.8.0
Plugins.getTemplates = function (callback) {
var templates = {};
var tplName;
winston.warn('[deprecated] Plugins.getTemplates is DEPRECATED to be removed in v1.8.0');
Plugins.data.getActive(function (err, plugins) {
if (err) {
return callback(err);
@@ -213,8 +216,8 @@ Plugins.list = function (matching, callback) {
require('request')(url, {
json: true,
}, function (err, res, body) {
if (err) {
winston.error('Error parsing plugins', err);
if (err || (res && res.statusCode !== 200)) {
winston.error('Error loading ' + url, err || body);
return Plugins.normalise([], callback);
}
@@ -225,7 +228,7 @@ Plugins.list = function (matching, callback) {
Plugins.normalise = function (apiReturn, callback) {
var pluginMap = {};
var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies;
apiReturn = apiReturn || [];
apiReturn = Array.isArray(apiReturn) ? apiReturn : [];
for (var i = 0; i < apiReturn.length; i += 1) {
apiReturn[i].id = apiReturn[i].name;
apiReturn[i].installed = false;

View File

@@ -12,6 +12,7 @@ var plugins = require('../plugins');
var cache = require('./cache');
var pubsub = require('../pubsub');
var utils = require('../utils');
var translator = require('../translator');
module.exports = function (Posts) {
pubsub.on('post:edit', function (pid) {
@@ -140,7 +141,7 @@ module.exports = function (Posts) {
db.setObject('topic:' + tid, results.topic, next);
},
function (next) {
topics.updateTags(tid, data.tags, next);
topics.updateTopicTags(tid, data.tags, next);
},
function (next) {
topics.getTopicTagsObjects(tid, next);
@@ -149,6 +150,7 @@ module.exports = function (Posts) {
topicData.tags = data.tags;
topicData.oldTitle = results.topic.title;
topicData.timestamp = results.topic.timestamp;
var renamed = translator.escape(validator.escape(String(title))) !== results.topic.title;
plugins.fireHook('action:topic.edit', { topic: topicData, uid: data.uid });
next(null, {
tid: tid,
@@ -158,7 +160,7 @@ module.exports = function (Posts) {
oldTitle: results.topic.title,
slug: topicData.slug,
isMainPost: true,
renamed: title !== results.topic.title,
renamed: renamed,
tags: tags,
});
},

View File

@@ -6,6 +6,7 @@ var meta = require('../meta');
var db = require('../database');
var user = require('../user');
var plugins = require('../plugins');
var privileges = require('../privileges');
module.exports = function (Posts) {
var votesInProgress = {};
@@ -15,16 +16,27 @@ module.exports = function (Posts) {
return callback(new Error('[[error:reputation-system-disabled]]'));
}
if (voteInProgress(pid, uid)) {
return callback(new Error('[[error:already-voting-for-this-post]]'));
}
async.waterfall([
function (next) {
privileges.posts.can('posts:upvote', pid, uid, next);
},
function (canUpvote, next) {
if (!canUpvote) {
return next(new Error('[[error:no-privileges]]'));
}
putVoteInProgress(pid, uid);
if (voteInProgress(pid, uid)) {
return next(new Error('[[error:already-voting-for-this-post]]'));
}
toggleVote('upvote', pid, uid, function (err, data) {
clearVoteProgress(pid, uid);
callback(err, data);
});
putVoteInProgress(pid, uid);
toggleVote('upvote', pid, uid, function (err, data) {
clearVoteProgress(pid, uid);
next(err, data);
});
},
], callback);
};
Posts.downvote = function (pid, uid, callback) {
@@ -36,16 +48,27 @@ module.exports = function (Posts) {
return callback(new Error('[[error:downvoting-disabled]]'));
}
if (voteInProgress(pid, uid)) {
return callback(new Error('[[error:already-voting-for-this-post]]'));
}
async.waterfall([
function (next) {
privileges.posts.can('posts:downvote', pid, uid, next);
},
function (canUpvote, next) {
if (!canUpvote) {
return next(new Error('[[error:no-privileges]]'));
}
putVoteInProgress(pid, uid);
if (voteInProgress(pid, uid)) {
return next(new Error('[[error:already-voting-for-this-post]]'));
}
toggleVote('downvote', pid, uid, function (err, data) {
clearVoteProgress(pid, uid);
callback(err, data);
});
putVoteInProgress(pid, uid);
toggleVote('downvote', pid, uid, function (err, data) {
clearVoteProgress(pid, uid);
next(err, data);
});
},
], callback);
};
Posts.unvote = function (pid, uid, callback) {
@@ -106,7 +129,7 @@ module.exports = function (Posts) {
};
function voteInProgress(pid, uid) {
return Array.isArray(votesInProgress[uid]) && votesInProgress[uid].indexOf(parseInt(pid, 10)) !== -1;
return Array.isArray(votesInProgress[uid]) && votesInProgress[uid].includes(parseInt(pid, 10));
}
function putVoteInProgress(pid, uid) {
@@ -156,7 +179,7 @@ module.exports = function (Posts) {
return callback(new Error('[[error:self-vote]]'));
}
if (command === 'downvote' && parseInt(results.reputation, 10) < parseInt(meta.config['privileges:downvote'], 10)) {
if (command === 'downvote' && parseInt(results.reputation, 10) < parseInt(meta.config['min:rep:downvote'], 10)) {
return callback(new Error('[[error:not-enough-reputation-to-downvote]]'));
}

View File

@@ -11,9 +11,9 @@ privileges.privilegeLabels = [
{ name: 'Tag Topics' },
{ name: 'Edit Posts' },
{ name: 'Delete Posts' },
{ name: 'Upvote Posts' },
{ name: 'Downvote Posts' },
{ name: 'Delete Topics' },
{ name: 'Upload Images' },
{ name: 'Upload Files' },
{ name: 'Purge' },
{ name: 'Moderate' },
];
@@ -27,9 +27,9 @@ privileges.userPrivilegeList = [
'topics:tag',
'posts:edit',
'posts:delete',
'posts:upvote',
'posts:downvote',
'topics:delete',
'upload:post:image',
'upload:post:file',
'purge',
'moderate',
];
@@ -40,6 +40,7 @@ privileges.groupPrivilegeList = privileges.userPrivilegeList.map(function (privi
privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList);
require('./privileges/global')(privileges);
require('./privileges/categories')(privileges);
require('./privileges/topics')(privileges);
require('./privileges/posts')(privileges);

View File

@@ -15,121 +15,20 @@ module.exports = function (privileges) {
privileges.categories.list = function (cid, callback) {
// Method used in admin/category controller to show all users/groups with privs in that given cid
var privilegeLabels = privileges.privilegeLabels.slice();
var userPrivilegeList = privileges.userPrivilegeList.slice();
var groupPrivilegeList = privileges.groupPrivilegeList.slice();
async.waterfall([
function (next) {
async.parallel({
labels: function (next) {
async.parallel({
users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privilegeLabels),
groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privilegeLabels),
users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privileges.privilegeLabels.slice()),
groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privileges.privilegeLabels.slice()),
}, next);
},
users: function (next) {
var userPrivileges;
var memberSets;
async.waterfall([
async.apply(plugins.fireHook, 'filter:privileges.list', userPrivilegeList),
function (_privs, next) {
userPrivileges = _privs;
groups.getMembersOfGroups(userPrivileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:' + privilege;
}), next);
},
function (_memberSets, next) {
memberSets = _memberSets.map(function (set) {
return set.map(function (uid) {
return parseInt(uid, 10);
});
});
var members = _.uniq(_.flatten(memberSets));
user.getUsersFields(members, ['picture', 'username'], next);
},
function (memberData, next) {
memberData.forEach(function (member) {
member.privileges = {};
for (var x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) {
member.privileges[userPrivileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1;
}
});
next(null, memberData);
},
], next);
helpers.getUserPrivileges(cid, 'filter:privileges.list', privileges.userPrivilegeList, next);
},
groups: function (next) {
var groupPrivileges;
async.waterfall([
async.apply(plugins.fireHook, 'filter:privileges.groups.list', groupPrivilegeList),
function (_privs, next) {
groupPrivileges = _privs;
async.parallel({
memberSets: function (next) {
groups.getMembersOfGroups(groupPrivileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:' + privilege;
}), next);
},
groupNames: function (next) {
groups.getGroups('groups:createtime', 0, -1, next);
},
}, next);
},
function (results, next) {
var memberSets = results.memberSets;
var uniqueGroups = _.uniq(_.flatten(memberSets));
var groupNames = results.groupNames.filter(function (groupName) {
return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1;
});
groupNames = groups.ephemeralGroups.concat(groupNames);
var registeredUsersIndex = groupNames.indexOf('registered-users');
if (registeredUsersIndex !== -1) {
groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]);
} else {
groupNames = ['registered-users'].concat(groupNames);
}
var adminIndex = groupNames.indexOf('administrators');
if (adminIndex !== -1) {
groupNames.splice(adminIndex, 1);
}
var memberPrivs;
var memberData = groupNames.map(function (member) {
memberPrivs = {};
for (var x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) {
memberPrivs[groupPrivileges[x]] = memberSets[x].indexOf(member) !== -1;
}
return {
name: member,
privileges: memberPrivs,
};
});
next(null, memberData);
},
function (memberData, next) {
// Grab privacy info for the groups as well
async.map(memberData, function (member, next) {
async.waterfall([
function (next) {
groups.isPrivate(member.name, next);
},
function (isPrivate, next) {
member.isPrivate = isPrivate;
next(null, member);
},
], next);
}, next);
},
], next);
helpers.getGroupPrivileges(cid, 'filter:privileges.groups.list', privileges.groupPrivilegeList, next);
},
}, next);
},
@@ -299,19 +198,13 @@ module.exports = function (privileges) {
};
privileges.categories.give = function (privileges, cid, groupName, callback) {
giveOrRescind(groups.join, privileges, cid, groupName, callback);
helpers.giveOrRescind(groups.join, privileges, cid, groupName, callback);
};
privileges.categories.rescind = function (privileges, cid, groupName, callback) {
giveOrRescind(groups.leave, privileges, cid, groupName, callback);
helpers.giveOrRescind(groups.leave, privileges, cid, groupName, callback);
};
function giveOrRescind(method, privileges, cid, groupName, callback) {
async.eachSeries(privileges, function (privilege, next) {
method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next);
}, callback);
}
privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) {
async.waterfall([
function (next) {

128
src/privileges/global.js Normal file
View File

@@ -0,0 +1,128 @@
'use strict';
var async = require('async');
var _ = require('lodash');
var user = require('../user');
var groups = require('../groups');
var helpers = require('./helpers');
var plugins = require('../plugins');
module.exports = function (privileges) {
privileges.global = {};
privileges.global.privilegeLabels = [
{ name: 'Chat' },
{ name: 'Upload Images' },
{ name: 'Upload Files' },
];
privileges.global.userPrivilegeList = [
'chat',
'upload:post:image',
'upload:post:file',
];
privileges.global.groupPrivilegeList = privileges.global.userPrivilegeList.map(function (privilege) {
return 'groups:' + privilege;
});
privileges.global.list = function (callback) {
async.waterfall([
function (next) {
async.parallel({
labels: function (next) {
async.parallel({
users: async.apply(plugins.fireHook, 'filter:privileges.global.list_human', privileges.global.privilegeLabels.slice()),
groups: async.apply(plugins.fireHook, 'filter:privileges.global.groups.list_human', privileges.global.privilegeLabels.slice()),
}, next);
},
users: function (next) {
helpers.getUserPrivileges(0, 'filter:privileges.global.list', privileges.global.userPrivilegeList, next);
},
groups: function (next) {
helpers.getGroupPrivileges(0, 'filter:privileges.global.groups.list', privileges.global.groupPrivilegeList, next);
},
}, next);
},
function (payload, next) {
// This is a hack because I can't do {labels.users.length} to echo the count in templates.js
payload.columnCount = payload.labels.users.length + 2;
next(null, payload);
},
], callback);
};
privileges.global.get = function (uid, callback) {
async.waterfall([
function (next) {
async.parallel({
privileges: function (next) {
helpers.isUserAllowedTo(privileges.global.userPrivilegeList, uid, 0, next);
},
isAdministrator: function (next) {
user.isAdministrator(uid, next);
},
isGlobalModerator: function (next) {
user.isGlobalModerator(uid, next);
},
}, next);
},
function (results, next) {
var privData = _.zipObject(privileges.global.userPrivilegeList, results.privileges);
var isAdminOrMod = results.isAdministrator || results.isGlobalModerator;
plugins.fireHook('filter:privileges.global.get', {
chat: privData.chat || isAdminOrMod,
'upload:post:image': privData['upload:post:image'] || isAdminOrMod,
'upload:post:file': privData['upload:post:file'] || isAdminOrMod,
}, next);
},
], callback);
};
privileges.global.can = function (privilege, uid, callback) {
helpers.some([
function (next) {
helpers.isUserAllowedTo(privilege, uid, [0], function (err, results) {
next(err, Array.isArray(results) && results.length ? results[0] : false);
});
},
function (next) {
user.isGlobalModerator(uid, next);
},
function (next) {
user.isAdministrator(uid, next);
},
], callback);
};
privileges.global.give = function (privileges, groupName, callback) {
helpers.giveOrRescind(groups.join, privileges, 0, groupName, callback);
};
privileges.global.rescind = function (privileges, groupName, callback) {
helpers.giveOrRescind(groups.leave, privileges, 0, groupName, callback);
};
privileges.global.userPrivileges = function (uid, callback) {
var tasks = {};
privileges.global.userPrivilegeList.forEach(function (privilege) {
tasks[privilege] = async.apply(groups.isMember, uid, 'cid:0:privileges:' + privilege);
});
async.parallel(tasks, callback);
};
privileges.global.groupPrivileges = function (groupName, callback) {
var tasks = {};
privileges.global.groupPrivilegeList.forEach(function (privilege) {
tasks[privilege] = async.apply(groups.isMember, groupName, 'cid:0:privileges:' + privilege);
});
async.parallel(tasks, callback);
};
};

View File

@@ -2,7 +2,11 @@
'use strict';
var async = require('async');
var _ = require('lodash');
var groups = require('../groups');
var user = require('../user');
var plugins = require('../plugins');
var helpers = module.exports;
@@ -111,3 +115,115 @@ function isGuestAllowedToPrivileges(privileges, cid, callback) {
groups.isMemberOfGroups('guests', groupKeys, callback);
}
helpers.getUserPrivileges = function (cid, hookName, userPrivilegeList, callback) {
var userPrivileges;
var memberSets;
async.waterfall([
async.apply(plugins.fireHook, hookName, userPrivilegeList.slice()),
function (_privs, next) {
userPrivileges = _privs;
groups.getMembersOfGroups(userPrivileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:' + privilege;
}), next);
},
function (_memberSets, next) {
memberSets = _memberSets.map(function (set) {
return set.map(function (uid) {
return parseInt(uid, 10);
});
});
var members = _.uniq(_.flatten(memberSets));
user.getUsersFields(members, ['picture', 'username'], next);
},
function (memberData, next) {
memberData.forEach(function (member) {
member.privileges = {};
for (var x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) {
member.privileges[userPrivileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1;
}
});
next(null, memberData);
},
], callback);
};
helpers.getGroupPrivileges = function (cid, hookName, groupPrivilegeList, callback) {
var groupPrivileges;
async.waterfall([
async.apply(plugins.fireHook, hookName, groupPrivilegeList.slice()),
function (_privs, next) {
groupPrivileges = _privs;
async.parallel({
memberSets: function (next) {
groups.getMembersOfGroups(groupPrivileges.map(function (privilege) {
return 'cid:' + cid + ':privileges:' + privilege;
}), next);
},
groupNames: function (next) {
groups.getGroups('groups:createtime', 0, -1, next);
},
}, next);
},
function (results, next) {
var memberSets = results.memberSets;
var uniqueGroups = _.uniq(_.flatten(memberSets));
var groupNames = results.groupNames.filter(function (groupName) {
return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1;
});
groupNames = groups.ephemeralGroups.concat(groupNames);
var registeredUsersIndex = groupNames.indexOf('registered-users');
if (registeredUsersIndex !== -1) {
groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]);
} else {
groupNames = ['registered-users'].concat(groupNames);
}
var adminIndex = groupNames.indexOf('administrators');
if (adminIndex !== -1) {
groupNames.splice(adminIndex, 1);
}
var memberPrivs;
var memberData = groupNames.map(function (member) {
memberPrivs = {};
for (var x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) {
memberPrivs[groupPrivileges[x]] = memberSets[x].indexOf(member) !== -1;
}
return {
name: member,
privileges: memberPrivs,
};
});
next(null, memberData);
},
function (memberData, next) {
// Grab privacy info for the groups as well
async.map(memberData, function (member, next) {
async.waterfall([
function (next) {
groups.isPrivate(member.name, next);
},
function (isPrivate, next) {
member.isPrivate = isPrivate;
next(null, member);
},
], next);
}, next);
},
], callback);
};
helpers.giveOrRescind = function (method, privileges, cid, groupName, callback) {
async.eachSeries(privileges, function (privilege, next) {
method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next);
}, callback);
};

View File

@@ -200,7 +200,7 @@ module.exports = function (privileges) {
}, next);
},
function (results, next) {
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 0;
var minimumReputation = utils.isNumber(meta.config['min:rep:flag']) ? parseInt(meta.config['min:rep:flag'], 10) : 0;
var canFlag = results.isAdminOrMod || parseInt(results.userReputation, 10) >= minimumReputation;
next(null, { flag: canFlag });
},

View File

@@ -141,9 +141,13 @@ module.exports = function (privileges) {
}, next);
},
function (results, next) {
var canEdit = results.isAdmin || (results.isGlobalMod && !results.isTargetAdmin);
next(null, canEdit);
results.canEdit = results.isAdmin || (results.isGlobalMod && !results.isTargetAdmin);
results.callerUid = callerUid;
results.uid = uid;
plugins.fireHook('filter:user.canEdit', results, next);
},
function (data, next) {
next(null, data.canEdit);
},
], callback);
};

View File

@@ -55,6 +55,7 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/categories/:category_id', middlewares, controllers.admin.categories.get);
router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics);
router.get('/manage/privileges/:cid?', middlewares, controllers.admin.privileges.get);
router.get('/manage/tags', middlewares, controllers.admin.tags.get);
router.get('/manage/post-queue', middlewares, controllers.admin.postQueue.get);
router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get);
@@ -71,6 +72,8 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/users/banned', middlewares, controllers.admin.users.banned);
router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue);
router.get('/manage/admins-mods', middlewares, controllers.admin.adminsMods.get);
router.get('/manage/groups', middlewares, controllers.admin.groups.list);
router.get('/manage/groups/:name', middlewares, controllers.admin.groups.get);

View File

@@ -83,7 +83,11 @@ Categories.setPrivilege = function (socket, data, callback) {
};
Categories.getPrivilegeSettings = function (socket, cid, callback) {
privileges.categories.list(cid, callback);
if (!parseInt(cid, 10)) {
privileges.global.list(callback);
} else {
privileges.categories.list(cid, callback);
}
};
Categories.copyPrivilegesToChildren = function (socket, cid, callback) {

View File

@@ -13,11 +13,19 @@ Tags.create = function (socket, data, callback) {
};
Tags.update = function (socket, data, callback) {
if (!data) {
if (!Array.isArray(data)) {
return callback(new Error('[[error:invalid-data]]'));
}
topics.updateTag(data.tag, data, callback);
topics.updateTags(data, callback);
};
Tags.rename = function (socket, data, callback) {
if (!Array.isArray(data)) {
return callback(new Error('[[error:invalid-data]]'));
}
topics.renameTags(data, callback);
};
Tags.deleteTags = function (socket, data, callback) {

View File

@@ -11,6 +11,7 @@ var Messaging = require('../messaging');
var utils = require('../utils');
var server = require('./');
var user = require('../user');
var privileges = require('../privileges');
var SocketModules = module.exports;
@@ -73,6 +74,12 @@ SocketModules.chats.newRoom = function (socket, data, callback) {
async.waterfall([
function (next) {
privileges.global.can('chat', socket.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
Messaging.canMessageUser(socket.uid, data.touid, next);
},
function (next) {
@@ -92,6 +99,13 @@ SocketModules.chats.send = function (socket, data, callback) {
async.waterfall([
function (next) {
privileges.global.can('chat', socket.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
plugins.fireHook('filter:messaging.send', {
data: data,
uid: socket.uid,
@@ -133,6 +147,13 @@ SocketModules.chats.loadRoom = function (socket, data, callback) {
async.waterfall([
function (next) {
privileges.global.can('chat', socket.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
Messaging.isUserInRoom(socket.uid, data.roomId, next);
},
function (inRoom, next) {
@@ -174,6 +195,13 @@ SocketModules.chats.addUserToRoom = function (socket, data, callback) {
var uid;
async.waterfall([
function (next) {
privileges.global.can('chat', socket.uid, next);
},
function (canChat, next) {
if (!canChat) {
return next(new Error('[[error:no-privileges]]'));
}
Messaging.getUserCountInRoom(data.roomId, next);
},
function (userCount, next) {

View File

@@ -15,7 +15,7 @@ module.exports = function (SocketUser) {
async.waterfall([
function (next) {
isAdminOrSelfAndPasswordMatch(socket.uid, data, next);
isPrivilegedOrSelfAndPasswordMatch(socket.uid, data, next);
},
function (next) {
SocketUser.updateProfile(socket, data, next);
@@ -29,7 +29,7 @@ module.exports = function (SocketUser) {
}
async.waterfall([
function (next) {
user.isAdminOrSelf(socket.uid, data.uid, next);
user.isAdminOrGlobalModOrSelf(socket.uid, data.uid, next);
},
function (next) {
user.updateCoverPicture(data, next);
@@ -43,7 +43,7 @@ module.exports = function (SocketUser) {
}
async.waterfall([
function (next) {
user.isAdminOrSelf(socket.uid, data.uid, next);
user.isAdminOrGlobalModOrSelf(socket.uid, data.uid, next);
},
function (next) {
user.uploadCroppedPicture(data, next);
@@ -58,7 +58,7 @@ module.exports = function (SocketUser) {
async.waterfall([
function (next) {
user.isAdminOrSelf(socket.uid, data.uid, next);
user.isAdminOrGlobalModOrSelf(socket.uid, data.uid, next);
},
function (next) {
user.removeCoverPicture(data, next);
@@ -66,11 +66,13 @@ module.exports = function (SocketUser) {
], callback);
};
function isAdminOrSelfAndPasswordMatch(uid, data, callback) {
function isPrivilegedOrSelfAndPasswordMatch(uid, data, callback) {
async.waterfall([
function (next) {
async.parallel({
isAdmin: async.apply(user.isAdministrator, uid),
isTargetAdmin: async.apply(user.isAdministrator, data.uid),
isGlobalMod: async.apply(user.isGlobalModerator, uid),
hasPassword: async.apply(user.hasPassword, data.uid),
passwordMatch: function (next) {
if (data.password) {
@@ -84,7 +86,11 @@ module.exports = function (SocketUser) {
function (results, next) {
var isSelf = parseInt(uid, 10) === parseInt(data.uid, 10);
if (!results.isAdmin && !isSelf) {
if (results.isTargetAdmin && !results.isAdmin) {
return next(new Error('[[error:no-privileges]]'));
}
if ((!results.isAdmin || !results.isGlobalMod) && !isSelf) {
return next(new Error('[[error:no-privileges]]'));
}

View File

@@ -52,7 +52,7 @@ Topics.getPageCount = function (tid, uid, callback) {
user.getSettings(uid, next);
},
function (settings, next) {
next(null, Math.ceil((parseInt(postCount, 10) - 1) / settings.postsPerPage));
next(null, Math.ceil(parseInt(postCount, 10) / settings.postsPerPage));
},
], callback);
};

View File

@@ -64,6 +64,9 @@ module.exports = function (Topics) {
'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids',
], timestamp, topicData.tid, next);
},
function (next) {
db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', 0, topicData.tid, next);
},
function (next) {
categories.updateRecentTid(topicData.cid, topicData.tid, next);
},

View File

@@ -219,6 +219,7 @@ module.exports = function (Topics) {
notifications.create({
type: 'new-reply',
subject: title,
bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]',
bodyLong: postData.content,
pid: postData.pid,

View File

@@ -2,13 +2,14 @@
'use strict';
var async = require('async');
var validator = require('validator');
var db = require('../database');
var meta = require('../meta');
var _ = require('lodash');
var plugins = require('../plugins');
var utils = require('../utils');
var batch = require('../batch');
module.exports = function (Topics) {
Topics.createTags = function (tags, tid, timestamp, callback) {
@@ -95,13 +96,61 @@ module.exports = function (Topics) {
], callback);
};
Topics.updateTag = function (tag, data, callback) {
if (!tag) {
return setImmediate(callback, new Error('[[error:invalid-tag]]'));
}
db.setObject('tag:' + tag, data, callback);
Topics.updateTags = function (data, callback) {
async.eachSeries(data, function (tagData, next) {
db.setObject('tag:' + tagData.value, {
color: tagData.color,
bgColor: tagData.bgColor,
}, next);
}, callback);
};
Topics.renameTags = function (data, callback) {
async.eachSeries(data, function (tagData, next) {
renameTag(tagData.value, tagData.newName, next);
}, callback);
};
function renameTag(tag, newTagName, callback) {
if (!newTagName || tag === newTagName) {
return setImmediate(callback);
}
async.waterfall([
function (next) {
Topics.createEmptyTag(newTagName, next);
},
function (next) {
batch.processSortedSet('tag:' + tag + ':topics', function (tids, next) {
async.waterfall([
function (next) {
db.sortedSetScores('tag:' + tag + ':topics', tids, next);
},
function (scores, next) {
db.sortedSetAdd('tag:' + newTagName + ':topics', scores, tids, next);
},
function (next) {
var keys = tids.map(function (tid) {
return 'topic:' + tid + ':tags';
});
async.series([
async.apply(db.sortedSetRemove, 'tag:' + tag + ':topics', tids),
async.apply(db.setsRemove, keys, tag),
async.apply(db.setsAdd, keys, newTagName),
], next);
},
], next);
}, next);
},
function (next) {
Topics.deleteTag(tag, next);
},
function (next) {
updateTagCount(newTagName, next);
},
], callback);
}
function updateTagCount(tag, callback) {
callback = callback || function () {};
async.waterfall([
@@ -147,7 +196,9 @@ module.exports = function (Topics) {
return 'tag:' + tag;
}), next);
},
], callback);
], function (err) {
callback(err);
});
};
function removeTagsFromTopics(tags, callback) {
@@ -191,6 +242,7 @@ module.exports = function (Topics) {
},
function (tagData, next) {
tags.forEach(function (tag, index) {
tag.valueEscaped = validator.escape(String(tag.value));
tag.color = tagData[index] ? tagData[index].color : '';
tag.bgColor = tagData[index] ? tagData[index].bgColor : '';
});
@@ -264,7 +316,7 @@ module.exports = function (Topics) {
], callback);
};
Topics.updateTags = function (tid, tags, callback) {
Topics.updateTopicTags = function (tid, tags, callback) {
callback = callback || function () {};
async.waterfall([
function (next) {

View File

@@ -250,29 +250,35 @@ module.exports = function (Topics) {
var topic;
var oldCid;
var cid = data.cid;
async.waterfall([
function (next) {
Topics.exists(tid, next);
},
function (exists, next) {
if (!exists) {
return next(new Error('[[error:no-topic]]'));
}
Topics.getTopicFields(tid, ['cid', 'lastposttime', 'pinned', 'deleted', 'postcount'], next);
Topics.getTopicData(tid, next);
},
function (topicData, next) {
topic = topicData;
if (!topic) {
return next(new Error('[[error:no-topic]]'));
}
if (parseInt(cid, 10) === parseInt(topic.cid, 10)) {
return next(new Error('[[error:cant-move-topic-to-same-category]]'));
}
db.sortedSetsRemove([
'cid:' + topicData.cid + ':tids',
'cid:' + topicData.cid + ':tids:pinned',
'cid:' + topicData.cid + ':tids:posts',
'cid:' + topicData.cid + ':tids:votes',
'cid:' + topicData.cid + ':tids:lastposttime',
'cid:' + topicData.cid + ':recent_tids',
'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids',
], tid, next);
},
function (next) {
db.sortedSetAdd('cid:' + cid + ':tids:lastposttime', topic.lastposttime, tid, next);
},
function (next) {
db.sortedSetAdd('cid:' + cid + ':uid:' + topic.uid + ':tids', topic.timestamp, tid, next);
},
function (next) {
if (parseInt(topic.pinned, 10)) {
db.sortedSetAdd('cid:' + cid + ':tids:pinned', Date.now(), tid, next);
@@ -285,6 +291,10 @@ module.exports = function (Topics) {
topic.postcount = topic.postcount || 0;
db.sortedSetAdd('cid:' + cid + ':tids:posts', topic.postcount, tid, next);
},
function (next) {
var votes = (parseInt(topic.upvotes, 10) || 0) - (parseInt(topic.downvotes, 10) || 0);
db.sortedSetAdd('cid:' + cid + ':tids:votes', votes, tid, next);
},
], function (err) {
next(err);
});

View File

@@ -1,37 +1,43 @@
'use strict';
var db = require('../../database');
var meta = require('../../meta');
module.exports = {
name: 'Generate customHTML block from old customJS setting',
timestamp: Date.UTC(2017, 9, 12),
method: function (callback) {
var newHTML = meta.config.customJS;
var newJS = [];
// Forgive me for parsing HTML with regex...
var scriptMatch = /^<script\s?(?!async|deferred)?>([\s\S]+?)<\/script>/m;
var match = scriptMatch.exec(newHTML);
while (match) {
if (match[1]) {
// Append to newJS array
newJS.push(match[1].trim());
// Remove the match from the existing value
newHTML = ((match.index > 0 ? newHTML.slice(0, match.index) : '') + newHTML.slice(match.index + match[0].length)).trim();
db.getObjectField('config', 'customJS', function (err, newHTML) {
if (err) {
return callback(err);
}
match = scriptMatch.exec(newHTML);
}
var newJS = [];
// Combine newJS array
newJS = newJS.join('\n\n');
// Forgive me for parsing HTML with regex...
var scriptMatch = /^<script\s?(?!async|deferred)?>([\s\S]+?)<\/script>/m;
var match = scriptMatch.exec(newHTML);
// Write both values to config
meta.configs.setMultiple({
customHTML: newHTML,
customJS: newJS,
}, callback);
while (match) {
if (match[1]) {
// Append to newJS array
newJS.push(match[1].trim());
// Remove the match from the existing value
newHTML = ((match.index > 0 ? newHTML.slice(0, match.index) : '') + newHTML.slice(match.index + match[0].length)).trim();
}
match = scriptMatch.exec(newHTML);
}
// Combine newJS array
newJS = newJS.join('\n\n');
// Write both values to config
meta.configs.setMultiple({
customHTML: newHTML,
customJS: newJS,
}, callback);
});
},
};

View File

@@ -48,7 +48,7 @@ module.exports = {
done = true;
return next();
}
delete item.expireAt;
if (Object.keys(item).length === 3 && item.hasOwnProperty('_key') && item.hasOwnProperty('value')) {
client.collection('objects').update({ _key: item._key }, { $rename: { value: 'data' } }, next);
} else {

View File

@@ -16,7 +16,7 @@ module.exports = {
var topicData;
async.waterfall([
function (next) {
db.getObjectFields('topic:' + tid, ['mainPid', 'cid'], next);
db.getObjectFields('topic:' + tid, ['mainPid', 'cid', 'pinned'], next);
},
function (_topicData, next) {
topicData = _topicData;
@@ -44,7 +44,11 @@ module.exports = {
db.sortedSetAdd('topics:votes', votes, tid, next);
},
function (next) {
db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', votes, tid, next);
if (parseInt(topicData.pinned, 10) !== 1) {
db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', votes, tid, next);
} else {
next();
}
},
], function (err) {
next(err);

View File

@@ -0,0 +1,52 @@
'use strict';
var async = require('async');
var batch = require('../../batch');
var db = require('../../database');
module.exports = {
name: 'Fix topics in categories per user if they were moved',
timestamp: Date.UTC(2018, 0, 22),
method: function (callback) {
var progress = this.progress;
batch.processSortedSet('topics:tid', function (tids, next) {
async.eachLimit(tids, 500, function (tid, _next) {
progress.incr();
var topicData;
async.waterfall([
function (next) {
db.getObjectFields('topic:' + tid, ['cid', 'tid', 'uid', 'oldCid', 'timestamp'], next);
},
function (_topicData, next) {
topicData = _topicData;
if (!topicData.cid || !topicData.oldCid) {
return _next();
}
db.isSortedSetMember('cid:' + topicData.oldCid + ':uid:' + topicData.uid, topicData.tid, next);
},
function (isMember, next) {
if (isMember) {
async.series([
function (next) {
db.sortedSetRemove('cid:' + topicData.oldCid + ':uid:' + topicData.uid + ':tids', tid, next);
},
function (next) {
db.sortedSetAdd('cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids', topicData.timestamp, tid, next);
},
], function (err) {
next(err);
});
} else {
next();
}
},
], _next);
}, next);
}, {
progress: progress,
batch: 500,
}, callback);
},
};

View File

@@ -0,0 +1,12 @@
'use strict';
var groups = require('../../groups');
module.exports = {
name: 'Give chat privilege to registered-users',
timestamp: Date.UTC(2017, 11, 18),
method: function (callback) {
groups.join('cid:0:privileges:groups:chat', 'registered-users', callback);
},
};

View File

@@ -0,0 +1,53 @@
'use strict';
var async = require('async');
var batch = require('../../batch');
var db = require('../../database');
module.exports = {
name: 'Fix sort by votes for moved topics',
timestamp: Date.UTC(2018, 0, 8),
method: function (callback) {
var progress = this.progress;
batch.processSortedSet('topics:tid', function (tids, next) {
async.eachLimit(tids, 500, function (tid, _next) {
progress.incr();
var topicData;
async.waterfall([
function (next) {
db.getObjectFields('topic:' + tid, ['cid', 'oldCid', 'upvotes', 'downvotes', 'pinned'], next);
},
function (_topicData, next) {
topicData = _topicData;
if (!topicData.cid || !topicData.oldCid) {
return _next();
}
var upvotes = parseInt(topicData.upvotes, 10) || 0;
var downvotes = parseInt(topicData.downvotes, 10) || 0;
var votes = upvotes - downvotes;
async.series([
function (next) {
db.sortedSetRemove('cid:' + topicData.oldCid + ':tids:votes', tid, next);
},
function (next) {
if (parseInt(topicData.pinned, 10) !== 1) {
db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', votes, tid, next);
} else {
next();
}
},
], function (err) {
next(err);
});
},
], _next);
}, next);
}, {
progress: progress,
batch: 500,
}, callback);
},
};

View File

@@ -0,0 +1,45 @@
'use strict';
var async = require('async');
var groups = require('../../groups');
var privileges = require('../../privileges');
var db = require('../../database');
module.exports = {
name: 'Give upload privilege to registered-users globally if it is given on a category',
timestamp: Date.UTC(2018, 0, 3),
method: function (callback) {
db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
if (err) {
return callback(err);
}
async.eachSeries(cids, function (cid, next) {
getGroupPrivileges(cid, function (err, groupPrivileges) {
if (err) {
return next(err);
}
var privs = [];
if (groupPrivileges['groups:upload:post:image']) {
privs.push('upload:post:image');
}
if (groupPrivileges['groups:upload:post:file']) {
privs.push('upload:post:file');
}
privileges.global.give(privs, 'registered-users', next);
});
}, callback);
});
},
};
function getGroupPrivileges(cid, callback) {
var tasks = {};
['groups:upload:post:image', 'groups:upload:post:file'].forEach(function (privilege) {
tasks[privilege] = async.apply(groups.isMember, 'registered-users', 'cid:' + cid + ':privileges:' + privilege);
});
async.parallel(tasks, callback);
}

View File

@@ -0,0 +1,25 @@
'use strict';
var db = require('../../database');
module.exports = {
name: 'Rename privileges:downvote and privileges:flag to min:rep:downvote, min:rep:flag respectively',
timestamp: Date.UTC(2018, 0, 12),
method: function (callback) {
db.getObjectFields('config', ['privileges:downvote', 'privileges:flag'], function (err, config) {
if (err) {
return callback(err);
}
db.setObject('config', {
'min:rep:downvote': parseInt(config['privileges:downvote'], 10) || 0,
'min:rep:flag': parseInt(config['privileges:downvote'], 10) || 0,
}, function (err) {
if (err) {
return callback(err);
}
db.deleteObjectFields('config', ['privileges:downvote', 'privileges:flag'], callback);
});
});
},
};

View File

@@ -0,0 +1,22 @@
'use strict';
var async = require('async');
var privileges = require('../../privileges');
var db = require('../../database');
module.exports = {
name: 'Give vote privilege to registered-users on all categories',
timestamp: Date.UTC(2018, 0, 9),
method: function (callback) {
db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) {
if (err) {
return callback(err);
}
async.eachSeries(cids, function (cid, next) {
privileges.categories.give(['posts:upvote', 'posts:downvote'], cid, 'registered-users', next);
}, callback);
});
},
};

View File

@@ -90,8 +90,9 @@ Digest.getSubscribers = function (interval, callback) {
};
Digest.send = function (data, callback) {
var emailsSent = 0;
if (!data || !data.subscribers || !data.subscribers.length) {
return callback();
return callback(null, emailsSent);
}
var now = new Date();
@@ -131,7 +132,7 @@ Digest.send = function (data, callback) {
return topicObj;
});
emailsSent += 1;
emailer.send('digest', userObj.uid, {
subject: '[' + meta.config.title + '] [[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]',
username: userObj.username,
@@ -151,6 +152,6 @@ Digest.send = function (data, callback) {
}, next);
},
], function (err) {
callback(err, data.subscribers.length);
callback(err, emailsSent);
});
};

View File

@@ -17,14 +17,6 @@ module.exports = function (User) {
var updateUid = data.uid;
var oldData;
if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) {
return callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]'));
}
if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) {
return callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]'));
}
async.waterfall([
function (next) {
plugins.fireHook('filter:user.updateProfile', { uid: uid, data: data, fields: fields }, next);
@@ -33,13 +25,7 @@ module.exports = function (User) {
fields = data.fields;
data = data.data;
async.series([
async.apply(isEmailAvailable, data, updateUid),
async.apply(isUsernameAvailable, data, updateUid),
async.apply(isGroupTitleValid, data),
], function (err) {
next(err);
});
validateData(uid, data, next);
},
function (next) {
User.getUserFields(updateUid, fields, next);
@@ -73,6 +59,19 @@ module.exports = function (User) {
], callback);
};
function validateData(callerUid, data, callback) {
async.series([
async.apply(isEmailAvailable, data, data.uid),
async.apply(isUsernameAvailable, data, data.uid),
async.apply(isGroupTitleValid, data),
async.apply(isWebsiteValid, callerUid, data),
async.apply(isAboutMeValid, callerUid, data),
async.apply(isSignatureValid, callerUid, data),
], function (err) {
callback(err);
});
}
function isEmailAvailable(data, uid, callback) {
if (!data.email) {
return callback();
@@ -141,6 +140,52 @@ module.exports = function (User) {
}
}
function isWebsiteValid(callerUid, data, callback) {
if (!data.website) {
return setImmediate(callback);
}
checkMinReputation(callerUid, data.uid, 'min:rep:website', callback);
}
function isAboutMeValid(callerUid, data, callback) {
if (!data.aboutme) {
return setImmediate(callback);
}
if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) {
return callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]'));
}
checkMinReputation(callerUid, data.uid, 'min:rep:aboutme', callback);
}
function isSignatureValid(callerUid, data, callback) {
if (!data.signature) {
return setImmediate(callback);
}
if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) {
return callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]'));
}
checkMinReputation(callerUid, data.uid, 'min:rep:signature', callback);
}
function checkMinReputation(callerUid, uid, setting, callback) {
var isSelf = parseInt(callerUid, 10) === parseInt(uid, 10);
if (!isSelf) {
return setImmediate(callback);
}
async.waterfall([
function (next) {
User.getUserField(uid, 'reputation', next);
},
function (reputation, next) {
if (parseInt(reputation, 10) < (parseInt(meta.config[setting], 10) || 0)) {
return next(new Error('[[error:not-enough-reputation-' + setting.replace(/:/g, '-') + ']]'));
}
next();
},
], callback);
}
function updateEmail(uid, newEmail, callback) {
async.waterfall([
function (next) {

View File

@@ -119,7 +119,7 @@ UserReset.commit = function (code, password, callback) {
user.hashPassword(password, next);
},
function (hash, next) {
async.parallel([
async.series([
async.apply(user.setUserFields, uid, { password: hash, 'email:confirmed': 1 }),
async.apply(db.deleteObjectField, 'reset:uid', code),
async.apply(db.sortedSetRemove, 'reset:issueDate', code),
@@ -128,7 +128,10 @@ UserReset.commit = function (code, password, callback) {
async.apply(user.auth.resetLockout, uid),
async.apply(db.delete, 'uid:' + uid + ':confirm:email:sent'),
async.apply(db.sortedSetRemove, 'users:notvalidated', uid),
], next);
async.apply(UserReset.cleanByUid, uid),
], function (err) {
next(err);
});
},
], callback);
};

View File

@@ -81,6 +81,10 @@ module.exports = function (User) {
}
function filterAndSortUids(uids, data, callback) {
uids = uids.filter(function (uid) {
return parseInt(uid, 10);
});
var fields = [];
if (data.sortBy) {

View File

@@ -101,13 +101,15 @@
<div class="panel-heading">[[admin/general/dashboard:control-panel]]</div>
<div class="panel-body text-center">
<p>
<div class="btn-group">
<button class="btn btn-warning reload">[[admin/general/dashboard:reload]]</button>
<button class="btn btn-danger restart">[[admin/general/dashboard:restart]]</button>
</div>
<button class="btn btn-block btn-warning reload"<!-- IF !canRestart --> disabled<!-- END -->>[[admin/general/dashboard:reload]]</button>
<button class="btn btn-block btn-danger restart"<!-- IF !canRestart --> disabled<!-- END -->>[[admin/general/dashboard:restart]]</button>
</p>
<p class="help-block">
<p class="<!-- IF canRestart -->help-block<!-- ELSE -->alert alert-warning<!-- END -->">
<!-- IF canRestart -->
[[admin/general/dashboard:restart-warning]]
<!-- ELSE -->
[[admin/general/dashboard:restart-disabled]]
<!-- END -->
</p>
<p>
<a href="{config.relative_path}/admin/settings/advanced" class="btn btn-info btn-block" data-placement="bottom" data-toggle="tooltip" title="[[admin/general/dashboard:maintenance-mode-title]]">[[admin/general/dashboard:maintenance-mode]]</a>

View File

@@ -0,0 +1,64 @@
<div class="admins-mods">
<h4><!-- IF admins.icon --><i class="fa {admins.icon}"></i> <!-- ENDIF admins.icon -->[[admin/manage/admins-mods:administrators]]</h4>
<div class="administrator-area">
<!-- BEGIN admins.members -->
<div class="user-card pull-left" data-uid="{admins.members.uid}">
<!-- IF admins.members.picture -->
<img class="avatar avatar-sm" src="{admins.members.picture}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {admins.members.icon:bgColor};">{admins.members.icon:text}</div>
<!-- ENDIF admins.members.picture -->
<a href="{config.relative_path}/user/{admins.members.userslug}">{admins.members.username}</a>
<i class="remove-user-icon fa fa-times" role="button"></i>
</div>
<!-- END admins.members -->
</div>
<input id="admin-search" class="form-control" placeholder="[[admin/manage/admins-mods:add-administrator]]" />
<br/>
<h4><!-- IF globalMods.icon --><i class="fa {globalMods.icon}"></i> <!-- ENDIF globalMods.icon -->[[admin/manage/admins-mods:global-moderators]]</h4>
<div class="global-moderator-area">
<!-- BEGIN globalMods.members -->
<div class="user-card pull-left" data-uid="{globalMods.members.uid}">
<!-- IF globalMods.members.picture -->
<img class="avatar avatar-sm" src="{globalMods.members.picture}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {globalMods.members.icon:bgColor};">{globalMods.members.icon:text}</div>
<!-- ENDIF globalMods.members.picture -->
<a href="{config.relative_path}/user/{globalMods.members.userslug}">{globalMods.members.username}</a>
<i class="remove-user-icon fa fa-times" role="button"></i>
</div>
<!-- END globalMods.members -->
</div>
<div id="no-global-mods-warning" class="<!-- IF globalMods.members.length -->hidden<!-- ENDIF globalMods.members.length -->">[[admin/manage/admins-mods:no-global-moderators]]</div>
<input id="global-mod-search" class="form-control" placeholder="[[admin/manage/admins-mods:add-global-moderator]]" />
<br/>
<!-- BEGIN categories -->
<div class="categories category-wrapper category-depth-{categories.depth}">
<h4><!-- IF categories.icon --><i class="fa {categories.icon}"></i> <!-- ENDIF categories.icon -->[[admin/manage/admins-mods:moderators-of-category, {categories.name}]]</h4>
<div class="moderator-area" data-cid="{categories.cid}">
<!-- BEGIN categories.moderators -->
<div class="user-card pull-left" data-uid="{categories.moderators.uid}">
<!-- IF categories.moderators.picture -->
<img class="avatar avatar-sm" src="{categories.moderators.picture}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {categories.moderators.icon:bgColor};">{categories.moderators.icon:text}</div>
<!-- ENDIF categories.moderators.picture -->
<a href="{config.relative_path}/user/{categories.moderators.userslug}">{categories.moderators.username}</a>
<i class="remove-user-icon fa fa-times" role="button"></i>
</div>
<!-- END categories.moderators -->
</div>
<div data-cid="{categories.cid}" class="no-moderator-warning <!-- IF categories.moderators.length -->hidden<!-- ENDIF categories.moderators.length -->">[[admin/manage/admins-mods:no-moderators]]</div>
<input data-cid="{categories.cid}" class="form-control moderator-search" placeholder="[[admin/manage/admins-mods:add-moderator]]" />
</div>
<br/>
<!-- END categories -->
</div>

View File

@@ -2,15 +2,7 @@
<form role="form" class="category" data-cid="{category.cid}">
<div class="row">
<div class="col-md-9">
<ul class="nav nav-pills">
<li class="active"><a href="#category-settings" data-toggle="tab">
[[admin/manage/categories:settings]]
</a></li>
<li><a href="#privileges" data-toggle="tab">[[admin/manage/categories:privileges]]</a></li>
</ul>
</div>
<div class="col-md-3">
<div class="col-md-3 pull-right">
<select id="category-selector" class="form-control">
<!-- BEGIN allCategories -->
<option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option>
@@ -18,7 +10,7 @@
</select>
</div>
</div>
<br/>
<div class="tab-content">
@@ -174,19 +166,6 @@
</div>
</div>
</div>
<div class="tab-pane fade col-xs-12" id="privileges">
<p>
[[admin/manage/categories:privileges.description]]
</p>
<p class="text-warning">
[[admin/manage/categories:privileges.warning]]
</p>
<hr />
<div class="privilege-table-container">
<!-- IMPORT admin/partials/categories/privileges.tpl -->
</div>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,31 @@
<div class="row">
<form role="form" class="category">
<div class="row">
<div class="col-md-3 pull-right">
<select id="category-selector" class="form-control">
<option value="global" <!-- IF !cid --> selected <!-- ENDIF !cid -->>[[admin/manage/privileges:global]]</option>
<option disabled>_____________</option>
<!-- BEGIN allCategories -->
<option value="{allCategories.value}" <!-- IF allCategories.selected -->selected<!-- ENDIF allCategories.selected -->>{allCategories.text}</option>
<!-- END allCategories -->
</select>
</div>
</div>
<br/>
<div class="">
<p>
[[admin/manage/categories:privileges.description]]
</p>
<hr />
<div class="privilege-table-container">
<!-- IF cid -->
<!-- IMPORT admin/partials/categories/privileges.tpl -->
<!-- ELSE -->
<!-- IMPORT admin/partials/global/privileges.tpl -->
<!-- ENDIF cid -->
</div>
</div>
</form>
</div>

View File

@@ -41,6 +41,7 @@
<p>[[admin/manage/tags:description]]</p>
<button class="btn btn-primary btn-block" id="create">[[admin/manage/tags:create]]</button>
<button class="btn btn-primary btn-block" id="modify">[[admin/manage/tags:modify]]</button>
<button class="btn btn-primary btn-block" id="rename">[[admin/manage/tags:rename]]</button>
<button class="btn btn-warning btn-block" id="deleteSelected">[[admin/manage/tags:delete]]</button>
</div>
</div>
@@ -74,4 +75,11 @@
</div>
</div>
</div>
<div class="rename-modal hidden">
<div class="form-group">
<label for="value">[[admin/manage/tags:name]]</label>
<input id="value" data-name="value" value="{tags.value}" class="form-control" />
</div>
</div>
</div>

View File

@@ -8,9 +8,6 @@
<div class="btn-group pull-right">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href="#" class="admin-user"><i class="fa fa-fw fa-shield"></i> [[admin/manage/users:make-admin]]</a></li>
<li><a href="#" class="remove-admin-user"><i class="fa fa-fw fa-ban"></i> [[admin/manage/users:remove-admin]]</a></li>
<li class="divider"></li>
<li><a href="#" class="validate-email"><i class="fa fa-fw fa-check"></i> [[admin/manage/users:validate-email]]</a></li>
<li><a href="#" class="send-validation-email"><i class="fa fa-fw fa-mail-forward"></i> [[admin/manage/users:send-validation-email]]</a></li>
<li><a href="#" class="password-reset-email"><i class="fa fa-fw fa-key"></i> [[admin/manage/users:password-reset-email]]</a></li>
@@ -50,7 +47,7 @@
<div class="search {search_display}">
<label>[[admin/manage/users:search.uid]]</label>
<input class="form-control" id="search-user-uid" data-search-type="uid" type="text" placeholder="[[admin/manage/users:search.uid-placeholder]]"/><br />
<input class="form-control" id="search-user-uid" data-search-type="uid" type="number" placeholder="[[admin/manage/users:search.uid-placeholder]]"/><br />
<label>[[admin/manage/users:search.username]]</label>
<input class="form-control" id="search-user-name" data-search-type="username" type="text" placeholder="[[admin/manage/users:search.username-placeholder]]"/><br />

View File

@@ -0,0 +1,86 @@
<table class="table table-striped privilege-table">
<thead>
<tr class="privilege-table-header">
<th colspan="3"></th>
</tr><tr><!-- zebrastripe reset --></tr>
<tr>
<th colspan="2">[[admin/manage/categories:privileges.section-user]]</th>
<!-- BEGIN privileges.labels.users -->
<th class="text-center">{privileges.labels.users.name}</th>
<!-- END privileges.labels.users -->
</tr>
</thead>
<tbody>
<!-- IF privileges.users.length -->
<!-- BEGIN privileges.users -->
<tr data-uid="{privileges.users.uid}">
<td>
<!-- IF ../picture -->
<img class="avatar avatar-sm" src="{privileges.users.picture}" title="{privileges.users.username}" />
<!-- ELSE -->
<div class="avatar avatar-sm" style="background-color: {../icon:bgColor};">{../icon:text}</div>
<!-- ENDIF ../picture -->
</td>
<td>{privileges.users.username}</td>
{function.spawnPrivilegeStates, privileges.users.username, ../privileges}
</tr>
<!-- END privileges.users -->
<tr>
<td colspan="{privileges.columnCount}">
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
[[admin/manage/categories:privileges.search-user]]
</button>
</td>
</tr>
<!-- ELSE -->
<tr>
<td colspan="{privileges.columnCount}">
[[admin/manage/privileges:global.no-users]]
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.user">
[[admin/manage/categories:privileges.search-user]]
</button>
</td>
</tr>
<!-- ENDIF privileges.users.length -->
</tbody>
</table>
<table class="table table-striped privilege-table">
<thead>
<tr class="privilege-table-header">
<th colspan="3"></th>
</tr><tr><!-- zebrastripe reset --></tr>
<tr>
<th colspan="2">[[admin/manage/categories:privileges.section-group]]</th>
<!-- BEGIN privileges.labels.groups -->
<th class="text-center">{privileges.labels.groups.name}</th>
<!-- END privileges.labels.groups -->
</tr>
</thead>
<tbody>
<!-- BEGIN privileges.groups -->
<tr data-group-name="{privileges.groups.name}" data-private="<!-- IF privileges.groups.isPrivate -->1<!-- ELSE -->0<!-- ENDIF privileges.groups.isPrivate -->">
<td>
<!-- IF privileges.groups.isPrivate -->
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
<!-- ENDIF privileges.groups.isPrivate -->
{privileges.groups.name}
</td>
<td></td>
{function.spawnPrivilegeStates, privileges.groups.name, ../privileges}
</tr>
<!-- END privileges.groups -->
<tr>
<td colspan="{privileges.columnCount}">
<div class="btn-toolbar">
<button type="button" class="btn btn-primary pull-right" data-ajaxify="false" data-action="search.group">
[[admin/manage/categories:privileges.search-group]]
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div class="help-block">
[[admin/manage/categories:privileges.inherit]]
</div>

View File

@@ -15,7 +15,9 @@
<h3 class="menu-section-title">[[admin/menu:section-manage]]</h3>
<ul class="menu-section-list">
<li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>
<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
<li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li>
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
<li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li>
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>
@@ -188,7 +190,9 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">[[admin/menu:section-manage]]</a>
<ul class="dropdown-menu" role="menu">
<li><a href="{relative_path}/admin/manage/categories">[[admin/menu:manage/categories]]</a></li>
<li><a href="{relative_path}/admin/manage/privileges">[[admin/menu:manage/privileges]]</a></li>
<li><a href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a></li>
<li><a href="{relative_path}/admin/manage/admins-mods">[[admin/menu:manage/admins-mods]]</a></li>
<li><a href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a></li>
<li><a href="{relative_path}/admin/manage/tags">[[admin/menu:manage/tags]]</a></li>
<li><a href="{relative_path}/admin/manage/registration">[[admin/menu:manage/registration]]</a></li>

View File

@@ -32,8 +32,11 @@
<div class="col-sm-2 col-xs-12 settings-header">[[admin/settings/reputation:thresholds]]</div>
<div class="col-sm-10 col-xs-12">
<form>
<strong>[[admin/settings/reputation:min-rep-downvote]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="privileges:downvote"><br />
<strong>[[admin/settings/reputation:min-rep-flag]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="privileges:flag"><br />
<strong>[[admin/settings/reputation:min-rep-downvote]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:downvote"><br />
<strong>[[admin/settings/reputation:min-rep-flag]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:flag"><br />
<strong>[[admin/settings/reputation:min-rep-website]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:website"><br />
<strong>[[admin/settings/reputation:min-rep-aboutme]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:aboutme"><br />
<strong>[[admin/settings/reputation:min-rep-signature]]</strong><br /> <input type="text" class="form-control" placeholder="0" data-field="min:rep:signature"><br />
</form>
</div>
</div>