diff --git a/src/categories.js b/src/categories.js index 1a5eac3022..26198af0d3 100644 --- a/src/categories.js +++ b/src/categories.js @@ -171,28 +171,6 @@ var RDB = require('./redis.js'), }); }; - - Categories.privileges = function(cid, uid, callback) { - function isModerator(next) { - user.isModerator(uid, cid, function(isMod) { - next(null, isMod); - }); - } - - function isAdministrator(next) { - user.isAdministrator(uid, function(isAdmin) { - next(null, isAdmin); - }); - } - - async.parallel([isModerator, isAdministrator], function(err, results) { - callback({ - editable: results.indexOf(true) !== -1 ? true : false, - view_deleted: results.indexOf(true) !== -1 ? true : false - }); - }); - }; - Categories.isTopicsRead = function(cid, uid, callback) { RDB.zrange('categories:' + cid + ':tid', 0, -1, function(err, tids) { diff --git a/src/categoryTools.js b/src/categoryTools.js new file mode 100644 index 0000000000..b4cb9770c9 --- /dev/null +++ b/src/categoryTools.js @@ -0,0 +1,32 @@ +var Groups = require('./groups'), + User = require('./user'), + + async = require('async'), + + CategoryTools = {}; + +CategoryTools.privileges = function(cid, uid, callback) { + async.parallel({ + "+r": function(next) { + Groups.isMemberByGroupName(uid, 'cid:' + cid + ':privileges:+r', next); + }, + "+w": function(next) { + Groups.isMemberByGroupName(uid, 'cid:' + cid + ':privileges:+w', next); + }, + moderator: function(next) { + User.isModerator(uid, cid, next); + }, + admin: function(next) { + User.isAdministrator(uid, next); + } + }, function(err, privileges) { + callback(err, !privileges ? null : { + read: privileges['+r'] || privileges.moderator || privileges.admin, + write: privileges['+w'] || privileges.moderator || privileges.admin, + editable: privileges.moderator || privileges.admin, + view_deleted: privileges.moderator || privileges.admin + }); + }); +}; + +module.exports = CategoryTools; \ No newline at end of file diff --git a/src/feed.js b/src/feed.js index f6c5ab49ae..7608648850 100644 --- a/src/feed.js +++ b/src/feed.js @@ -108,8 +108,6 @@ }; Feed.updateRecent = function(callback) { - console.log('entered'); - if (process.env.NODE_ENV === 'development') winston.info('[rss] Updating Recent Posts RSS feed'); topics.getLatestTopics(0, 0, 19, undefined, function (err, recentData) { var feed = new rss({ title: 'Recently Active Topics', diff --git a/src/postTools.js b/src/postTools.js index aa4eeb09cf..ffb067a42a 100644 --- a/src/postTools.js +++ b/src/postTools.js @@ -36,9 +36,7 @@ var RDB = require('./redis'), function getThreadPrivileges(next) { posts.getPostField(pid, 'tid', function(err, tid) { - threadTools.privileges(tid, uid, function(privileges) { - next(null, privileges); - }); + threadTools.privileges(tid, uid, next); }); } diff --git a/src/posts.js b/src/posts.js index 65258abcae..4bf7b80119 100644 --- a/src/posts.js +++ b/src/posts.js @@ -1,19 +1,20 @@ -var RDB = require('./redis.js'), - utils = require('./../public/src/utils.js'), - user = require('./user.js'), - topics = require('./topics.js'), - categories = require('./categories.js'), - favourites = require('./favourites.js'), - threadTools = require('./threadTools.js'), +var RDB = require('./redis'), + utils = require('./../public/src/utils'), + user = require('./user'), + topics = require('./topics'), + categories = require('./categories'), + favourites = require('./favourites'), + threadTools = require('./threadTools'), postTools = require('./postTools'), categories = require('./categories'), - feed = require('./feed.js'), - async = require('async'), + feed = require('./feed'), plugins = require('./plugins'), + meta = require('./meta'), + + async = require('async'), reds = require('reds'), postSearch = reds.createSearch('nodebbpostsearch'), nconf = require('nconf'), - meta = require('./meta.js'), validator = require('validator'), winston = require('winston'); @@ -125,60 +126,63 @@ var RDB = require('./redis.js'), }; Posts.reply = function(tid, uid, content, callback) { - if(content) { - content = content.trim(); - } - - if (!content || content.length < meta.config.minimumPostLength) { - callback(new Error('content-too-short'), null); - return; - } - - Posts.create(uid, tid, content, function(err, postData) { - if(err) { - return callback(err, null); - } else if(!postData) { - callback(new Error('reply-error'), null); + threadTools.privileges(tid, uid, function(err, privileges) { + if (content) { + content = content.trim(); } - async.parallel([ - function(next) { - topics.markUnRead(tid, function(err) { - if(err) { - return next(err); - } - topics.markAsRead(tid, uid); - next(); - }); - }, - function(next) { - Posts.getCidByPid(postData.pid, function(err, cid) { - if(err) { - return next(err); - } + if (!content || content.length < meta.config.minimumPostLength) { + return callback(new Error('content-too-short')); + } else if (!privileges.write) { + return callback(new Error('no-privileges')); + } - RDB.del('cid:' + cid + ':read_by_uid'); - next(); - }); - }, - function(next) { - threadTools.notifyFollowers(tid, uid); - next(); - }, - function(next) { - Posts.addUserInfoToPost(postData, function(err) { - if(err) { - return next(err); - } - next(); - }); - } - ], function(err, results) { + Posts.create(uid, tid, content, function(err, postData) { if(err) { return callback(err, null); + } else if(!postData) { + callback(new Error('reply-error'), null); } - callback(null, postData); + async.parallel([ + function(next) { + topics.markUnRead(tid, function(err) { + if(err) { + return next(err); + } + topics.markAsRead(tid, uid); + next(); + }); + }, + function(next) { + Posts.getCidByPid(postData.pid, function(err, cid) { + if(err) { + return next(err); + } + + RDB.del('cid:' + cid + ':read_by_uid'); + next(); + }); + }, + function(next) { + threadTools.notifyFollowers(tid, uid); + next(); + }, + function(next) { + Posts.addUserInfoToPost(postData, function(err) { + if(err) { + return next(err); + } + next(); + }); + } + ], function(err, results) { + if(err) { + return callback(err, null); + } + + callback(null, postData); + }); }); }); } diff --git a/src/routes/api.js b/src/routes/api.js index 9b9555cbe5..ba97e3a391 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -130,7 +130,7 @@ var user = require('./../user.js'), var uid = (req.user) ? req.user.uid : 0; // Category Whitelisting (support for "-r" to come later) - var whitelistReadKey = 'cid:' + req.params.id + ':permissions:+r', + var whitelistReadKey = 'cid:' + req.params.id + ':privileges:+r', success = function() { categories.getCategoryById(req.params.id, uid, function (err, data) { if (!err && data && data.disabled === "0") diff --git a/src/routes/debug.js b/src/routes/debug.js index d2f216f729..f604e0c18a 100644 --- a/src/routes/debug.js +++ b/src/routes/debug.js @@ -55,15 +55,6 @@ var DebugRoute = function(app) { }); }); - app.get('/prune', function(req, res) { - var Notifications = require('../notifications'); - - Notifications.prune(new Date(), function() { - console.log('done'); - }); - res.send(); - }); - app.get('/uuidtest', function(req, res) { var Utils = require('../../public/src/utils.js'); diff --git a/src/threadTools.js b/src/threadTools.js index 82ba56757b..a7df6eeaf8 100644 --- a/src/threadTools.js +++ b/src/threadTools.js @@ -1,16 +1,18 @@ -var RDB = require('./redis.js'), - topics = require('./topics.js'), - categories = require('./categories.js'), - user = require('./user.js'), +var RDB = require('./redis'), + topics = require('./topics'), + categories = require('./categories'), + CategoryTools = require('./categoryTools'), + user = require('./user'), async = require('async'), - notifications = require('./notifications.js'), + notifications = require('./notifications'), posts = require('./posts'), + meta = require('./meta'), + websockets = require('./websockets'); + reds = require('reds'), topicSearch = reds.createSearch('nodebbtopicsearch'), winston = require('winston'), - meta = require('./meta'), nconf = require('nconf'), - websockets = require('./websockets'); (function(ThreadTools) { @@ -22,28 +24,24 @@ var RDB = require('./redis.js'), } ThreadTools.privileges = function(tid, uid, callback) { - //todo: break early if one condition is true - - function getCategoryPrivileges(next) { - topics.getTopicField(tid, 'cid', function(err, cid) { - categories.privileges(cid, uid, function(privileges) { - next(null, privileges); + async.parallel({ + categoryPrivs: function(next) { + topics.getTopicField(tid, 'cid', function(err, cid) { + CategoryTools.privileges(cid, uid, next); }); - }); - } - - function hasEnoughRep(next) { - user.getUserField(uid, 'reputation', function(err, reputation) { - if (err) return next(null, false); - next(null, parseInt(reputation, 10) >= parseInt(meta.config['privileges:manage_topic'], 10)); - }); - } - - - async.parallel([getCategoryPrivileges, hasEnoughRep], function(err, results) { - callback({ - editable: results[0].editable || results[1], - view_deleted: results[0].view_deleted || results[1] + }, + hasEnoughRep: function(next) { + user.getUserField(uid, 'reputation', function(err, reputation) { + if (err) return next(null, false); + next(null, parseInt(reputation, 10) >= parseInt(meta.config['privileges:manage_topic'], 10)); + }); + } + }, function(err, results) { + callback(err, !results ? undefined : { + read: results.categoryPrivs.read, + write: results.categoryPrivs.write, + editable: results.categoryPrivs.editable || results.hasEnoughRep, + view_deleted: results.categoryPrivs.view_deleted || results.hasEnoughRep }); }); } diff --git a/src/topics.js b/src/topics.js index 905b262135..fdb2db4e97 100644 --- a/src/topics.js +++ b/src/topics.js @@ -2,16 +2,17 @@ var RDB = require('./redis'), posts = require('./posts'), utils = require('./../public/src/utils'), user = require('./user'), - Groups = require('./groups') categories = require('./categories'), + CategoryTools = require('./categoryTools'), posts = require('./posts'), threadTools = require('./threadTools'), postTools = require('./postTools'), notifications = require('./notifications'), - async = require('async'), feed = require('./feed'), favourites = require('./favourites'), meta = require('./meta'), + + async = require('async'), reds = require('reds'), topicSearch = reds.createSearch('nodebbtopicsearch'), nconf = require('nconf'), @@ -20,98 +21,104 @@ var RDB = require('./redis'), (function(Topics) { Topics.post = function(uid, title, content, category_id, callback) { - if (!category_id) - throw new Error('Attempted to post without a category_id'); - - if (content) - content = content.trim(); - if (title) - title = title.trim(); - - if (!uid) { - callback(new Error('not-logged-in'), null); - return; - } else if (!title || title.length < meta.config.minimumTitleLength) { - callback(new Error('title-too-short'), null); - return; - } else if (!content || content.length < meta.config.miminumPostLength) { - callback(new Error('content-too-short'), null); - return; - } - - user.getUserField(uid, 'lastposttime', function(err, lastposttime) { - if (err) lastposttime = 0; - if (Date.now() - lastposttime < meta.config.postDelay * 1000) { - callback(new Error('too-many-posts'), null); - return; - } - - RDB.incr('next_topic_id', function(err, tid) { - RDB.handle(err); - - // Global Topics - if (uid == null) uid = 0; - if (uid !== null) { - RDB.sadd('topics:tid', tid); - } else { - // need to add some unique key sent by client so we can update this with the real uid later - RDB.lpush('topics:queued:tid', tid); + CategoryTools.privileges(category_id, uid, function(err, privileges) { + if (privileges.write) { + if (!category_id) + throw new Error('Attempted to post without a category_id'); + + if (content) + content = content.trim(); + if (title) + title = title.trim(); + + if (!uid) { + callback(new Error('not-logged-in'), null); + return; + } else if (!title || title.length < meta.config.minimumTitleLength) { + callback(new Error('title-too-short'), null); + return; + } else if (!content || content.length < meta.config.miminumPostLength) { + callback(new Error('content-too-short'), null); + return; } - var slug = tid + '/' + utils.slugify(title); - var timestamp = Date.now(); - RDB.hmset('topic:' + tid, { - 'tid': tid, - 'uid': uid, - 'cid': category_id, - 'title': title, - 'slug': slug, - 'timestamp': timestamp, - 'lastposttime': 0, - 'postcount': 0, - 'viewcount': 0, - 'locked': 0, - 'deleted': 0, - 'pinned': 0 - }); + user.getUserField(uid, 'lastposttime', function(err, lastposttime) { + if (err) lastposttime = 0; + if (Date.now() - lastposttime < meta.config.postDelay * 1000) { + callback(new Error('too-many-posts'), null); + return; + } + + RDB.incr('next_topic_id', function(err, tid) { + RDB.handle(err); + + // Global Topics + if (uid == null) uid = 0; + if (uid !== null) { + RDB.sadd('topics:tid', tid); + } else { + // need to add some unique key sent by client so we can update this with the real uid later + RDB.lpush('topics:queued:tid', tid); + } + + var slug = tid + '/' + utils.slugify(title); + var timestamp = Date.now(); + RDB.hmset('topic:' + tid, { + 'tid': tid, + 'uid': uid, + 'cid': category_id, + 'title': title, + 'slug': slug, + 'timestamp': timestamp, + 'lastposttime': 0, + 'postcount': 0, + 'viewcount': 0, + 'locked': 0, + 'deleted': 0, + 'pinned': 0 + }); - topicSearch.index(title, tid); + topicSearch.index(title, tid); - user.addTopicIdToUser(uid, tid); + user.addTopicIdToUser(uid, tid); - // let everyone know that there is an unread topic in this category - RDB.del('cid:' + category_id + ':read_by_uid', function(err, data) { - Topics.markAsRead(tid, uid); - }); + // let everyone know that there is an unread topic in this category + RDB.del('cid:' + category_id + ':read_by_uid', function(err, data) { + Topics.markAsRead(tid, uid); + }); - // in future it may be possible to add topics to several categories, so leaving the door open here. - RDB.zadd('categories:' + category_id + ':tid', timestamp, tid); - RDB.hincrby('category:' + category_id, 'topic_count', 1); - RDB.incr('totaltopiccount'); + // in future it may be possible to add topics to several categories, so leaving the door open here. + RDB.zadd('categories:' + category_id + ':tid', timestamp, tid); + RDB.hincrby('category:' + category_id, 'topic_count', 1); + RDB.incr('totaltopiccount'); - feed.updateCategory(category_id); + feed.updateCategory(category_id); - posts.create(uid, tid, content, function(err, postData) { - if(err) { - return callback(err, null); - } else if(!postData) { - return callback(new Error('invalid-post'), null); - } + posts.create(uid, tid, content, function(err, postData) { + if(err) { + return callback(err, null); + } else if(!postData) { + return callback(new Error('invalid-post'), null); + } - // Auto-subscribe the post creator to the newly created topic - threadTools.toggleFollow(tid, uid); + // Auto-subscribe the post creator to the newly created topic + threadTools.toggleFollow(tid, uid); - Topics.getTopicForCategoryView(tid, uid, function(topicData) { - topicData.unreplied = 1; + Topics.getTopicForCategoryView(tid, uid, function(topicData) { + topicData.unreplied = 1; - callback(null, { - topicData: topicData, - postData: postData + callback(null, { + topicData: topicData, + postData: postData + }); + }); }); }); }); - }); + } else { + callback(new Error('no-privileges')); + } }); }; @@ -403,7 +410,7 @@ var RDB = require('./redis'), // temporary. I don't think this call should belong here function getPrivileges(next) { - categories.privileges(category_id, current_user, function(user_privs) { + CategoryTools.privileges(category_id, current_user, function(user_privs) { next(null, user_privs); }); } @@ -486,27 +493,25 @@ var RDB = require('./redis'), function getTopicData(next) { Topics.getTopicData(tid, next); - } + }; function getTopicPosts(next) { - Topics.getTopicPosts(tid, start, end, current_user, function(topicPosts, privileges) { + Topics.getTopicPosts(tid, start, end, current_user, function(topicPosts) { next(null, topicPosts); }); - } + }; function getPrivileges(next) { - threadTools.privileges(tid, current_user, function(privData) { - next(null, privData); - }); - } + threadTools.privileges(tid, current_user, next); + }; function getCategoryData(next) { Topics.getCategoryData(tid, next); - } + }; async.parallel([getTopicData, getTopicPosts, getPrivileges, getCategoryData], function(err, results) { if (err) { - console.log(err.message); + winston.error('[Topics.getTopicWithPosts] Could not retrieve topic data: ', err.message); callback(err, null); return; } diff --git a/src/user.js b/src/user.js index 78a450c12b..ac492bd3cd 100644 --- a/src/user.js +++ b/src/user.js @@ -723,14 +723,14 @@ var bcrypt = require('bcrypt'), User.isModerator = function(uid, cid, callback) { RDB.sismember('cid:' + cid + ':moderators', uid, function(err, exists) { RDB.handle(err); - callback( !! exists); + callback(err, !! exists); }); }; User.isAdministrator = function(uid, callback) { Groups.getGidFromName('Administrators', function(err, gid) { Groups.isMember(uid, gid, function(err, isAdmin) { - callback( !! isAdmin); + callback(err, !! isAdmin); }); }); }; diff --git a/src/websockets.js b/src/websockets.js index 73dde73bd8..94d95facca 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -348,7 +348,7 @@ module.exports.init = function(io) { topics.post(uid, data.title, data.content, data.category_id, function(err, result) { if(err) { - if(err.message === 'not-logged-in') { + if (err.message === 'not-logged-in') { socket.emit('event:alert', { title: 'Thank you for posting', message: 'Since you are unregistered, your post is awaiting approval. Click here to register now.', @@ -364,6 +364,13 @@ module.exports.init = function(io) { posts.emitContentTooShortAlert(socket); } else if (err.message === 'too-many-posts') { posts.emitTooManyPostsAlert(socket); + } else if (err.message === 'no-privileges') { + socket.emit('event:alert', { + title: 'Unable to post', + message: 'You do not have posting privileges in this category.', + type: 'danger', + timeout: 7500 + }); } else { socket.emit('event:alert', { title: 'Error', @@ -423,8 +430,7 @@ module.exports.init = function(io) { posts.reply(data.topic_id, uid, data.content, function(err, postData) { if(err) { - - if(err.message === 'content-too-short') { + if (err.message === 'content-too-short') { posts.emitContentTooShortAlert(socket); } else if (err.message === 'too-many-posts') { posts.emitTooManyPostsAlert(socket); @@ -435,6 +441,13 @@ module.exports.init = function(io) { type: 'warning', timeout: 2000 }); + } else if (err.message === 'no-privileges') { + socket.emit('event:alert', { + title: 'Unable to post', + message: 'You do not have posting privileges in this category.', + type: 'danger', + timeout: 7500 + }); } return; } @@ -495,8 +508,8 @@ module.exports.init = function(io) { }); socket.on('api:topic.delete', function(data) { - threadTools.privileges(data.tid, uid, function(privileges) { - if (privileges.editable) { + threadTools.privileges(data.tid, uid, function(err, privileges) { + if (!err && privileges.editable) { threadTools.delete(data.tid, function(err) { if (!err) { emitTopicPostStats(); @@ -511,8 +524,8 @@ module.exports.init = function(io) { }); socket.on('api:topic.restore', function(data) { - threadTools.privileges(data.tid, uid, function(privileges) { - if (privileges.editable) { + threadTools.privileges(data.tid, uid, function(err, privileges) { + if (!err && privileges.editable) { threadTools.restore(data.tid, socket, function(err) { emitTopicPostStats(); @@ -526,32 +539,32 @@ module.exports.init = function(io) { }); socket.on('api:topic.lock', function(data) { - threadTools.privileges(data.tid, uid, function(privileges) { - if (privileges.editable) { + threadTools.privileges(data.tid, uid, function(err, privileges) { + if (!err && privileges.editable) { threadTools.lock(data.tid, socket); } }); }); socket.on('api:topic.unlock', function(data) { - threadTools.privileges(data.tid, uid, function(privileges) { - if (privileges.editable) { + threadTools.privileges(data.tid, uid, function(err, privileges) { + if (!err && privileges.editable) { threadTools.unlock(data.tid, socket); } }); }); socket.on('api:topic.pin', function(data) { - threadTools.privileges(data.tid, uid, function(privileges) { - if (privileges.editable) { + threadTools.privileges(data.tid, uid, function(err, privileges) { + if (!err && privileges.editable) { threadTools.pin(data.tid, socket); } }); }); socket.on('api:topic.unpin', function(data) { - threadTools.privileges(data.tid, uid, function(privileges) { - if (privileges.editable) { + threadTools.privileges(data.tid, uid, function(err, privileges) { + if (!err && privileges.editable) { threadTools.unpin(data.tid, socket); } });