diff --git a/src/controllers/category.js b/src/controllers/category.js index 867f17c5e9..96e3d8c2bf 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -154,12 +154,10 @@ categoryController.get = function (req, res, callback) { categoryData.description = translator.escape(categoryData.description); categoryData.privileges = userPrivileges; categoryData.showSelect = categoryData.privileges.editable; - + categoryData.rssFeedUrl = nconf.get('url') + '/category/' + categoryData.cid + '.rss'; if (parseInt(req.uid, 10)) { categories.markAsRead([cid], req.uid); - categoryData.rssFeedUrl = nconf.get('url') + '/category/' + categoryData.cid + '.rss?uid=' + req.uid + '&token=' + rssToken; - } else { - categoryData.rssFeedUrl = nconf.get('url') + '/category/' + categoryData.cid + '.rss'; + categoryData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; } addTags(categoryData, res); diff --git a/src/controllers/topics.js b/src/controllers/topics.js index c702ba90fc..a33703b8ce 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -23,6 +23,7 @@ topicsController.get = function (req, res, callback) { var pageCount = 1; var userPrivileges; var settings; + var rssToken; if ((req.params.post_index && !utils.isNumber(req.params.post_index)) || !utils.isNumber(tid)) { return callback(); @@ -40,6 +41,9 @@ topicsController.get = function (req, res, callback) { topic: function (next) { topics.getTopicData(tid, next); }, + rssToken: function (next) { + user.auth.getFeedToken(req.uid, next); + }, }, next); }, function (results, next) { @@ -48,6 +52,7 @@ topicsController.get = function (req, res, callback) { } userPrivileges = results.privileges; + rssToken = results.rssToken; if (!userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) { return helpers.notAllowed(req, res); @@ -262,6 +267,9 @@ topicsController.get = function (req, res, callback) { data.postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10) || 0; data.scrollToMyPost = settings.scrollToMyPost; data.rssFeedUrl = nconf.get('relative_path') + '/topic/' + data.tid + '.rss'; + if (req.uid) { + data.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; + } data.postIndex = req.params.post_index; data.pagination = pagination.create(currentPage, pageCount, req.query); data.pagination.rel.forEach(function (rel) { diff --git a/src/routes/feeds.js b/src/routes/feeds.js index f2a772a7bd..62b8e6f650 100644 --- a/src/routes/feeds.js +++ b/src/routes/feeds.js @@ -26,7 +26,7 @@ module.exports = function (app, middleware) { app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag); }; -function validateTokenIfRequiresLogin(requiresLogin, req, res, callback) { +function validateTokenIfRequiresLogin(requiresLogin, cid, req, res, callback) { var uid = req.query.uid; var token = req.query.token; @@ -38,23 +38,31 @@ function validateTokenIfRequiresLogin(requiresLogin, req, res, callback) { return helpers.notAllowed(req, res); } - user.getUserField(uid, 'rss_token', function (err, _token) { - if (err) { - return callback(err); - } - - if (token === _token) { - return callback(); - } - - user.auth.logAttempt(uid, req.ip, function (err) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + user.getUserField(uid, 'rss_token', next); + }, + function (_token, next) { + if (token === _token) { + async.waterfall([ + function (next) { + privileges.categories.get(cid, uid, next); + }, + function (privileges, next) { + if (!privileges.read) { + return helpers.notAllowed(req, res); + } + next(); + }, + ], callback); + return; } - - return helpers.notAllowed(req, res); - }); - }); + user.auth.logAttempt(uid, req.ip, next); + }, + function () { + helpers.notAllowed(req, res); + }, + ], callback); } function generateForTopic(req, res, callback) { @@ -64,6 +72,7 @@ function generateForTopic(req, res, callback) { var tid = req.params.topic_id; var userPrivileges; + var topic; async.waterfall([ function (next) { async.parallel({ @@ -79,15 +88,12 @@ function generateForTopic(req, res, callback) { if (!results.topic || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { return controllers404.send404(req, res); } - - validateTokenIfRequiresLogin(!results.privileges['topics:read'], req, res, function (err) { - if (err) { - return next(err); - } - - userPrivileges = results.privileges; - topics.getTopicWithPosts(results.topic, 'tid:' + tid + ':posts', req.uid, 0, 25, false, next); - }); + userPrivileges = results.privileges; + topic = results.topic; + validateTokenIfRequiresLogin(!results.privileges['topics:read'], results.topic.cid, req, res, next); + }, + function (next) { + topics.getTopicWithPosts(topic, 'tid:' + tid + ':posts', req.uid || req.query.uid || 0, 0, 25, false, next); }, function (topicData) { topics.modifyPostsByPrivilege(topicData, userPrivileges); @@ -130,40 +136,12 @@ function generateForTopic(req, res, callback) { ], callback); } -function generateForUserTopics(req, res, callback) { - if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return controllers404.send404(req, res); - } - - var userslug = req.params.userslug; - - async.waterfall([ - function (next) { - user.getUidByUserslug(userslug, next); - }, - function (uid, next) { - if (!uid) { - return callback(); - } - user.getUserFields(uid, ['uid', 'username'], next); - }, - function (userData, next) { - generateForTopics({ - uid: req.uid, - title: 'Topics by ' + userData.username, - description: 'A list of topics that are posted by ' + userData.username, - feed_url: '/user/' + userslug + '/topics.rss', - site_url: '/user/' + userslug + '/topics', - }, 'uid:' + userData.uid + ':topics', req, res, next); - }, - ], callback); -} - function generateForCategory(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { return controllers404.send404(req, res); } var cid = req.params.category_id; + var category; async.waterfall([ function (next) { @@ -178,25 +156,23 @@ function generateForCategory(req, res, next) { reverse: true, start: 0, stop: 25, - uid: req.uid, + uid: req.uid || req.query.uid || 0, }, next); }, }, next); }, function (results, next) { - validateTokenIfRequiresLogin(!results.privileges.read, req, res, function (err) { - if (err) { - return next(err); - } - - generateTopicsFeed({ - uid: req.uid, - title: results.category.name, - description: results.category.description, - feed_url: '/category/' + cid + '.rss', - site_url: '/category/' + results.category.cid, - }, results.category.topics, next); - }); + category = results.category; + validateTokenIfRequiresLogin(!results.privileges.read, cid, req, res, next); + }, + function (next) { + generateTopicsFeed({ + uid: req.uid || req.query.uid || 0, + title: category.name, + description: category.description, + feed_url: '/category/' + cid + '.rss', + site_url: '/category/' + category.cid, + }, category.topics, next); }, function (feed) { sendFeed(feed, res); @@ -330,12 +306,13 @@ function generateForRecentPosts(req, res, next) { ], next); } -function generateForCategoryRecentPosts(req, res, next) { +function generateForCategoryRecentPosts(req, res, callback) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { return controllers404.send404(req, res); } var cid = req.params.category_id; - + var category; + var posts; async.waterfall([ function (next) { async.parallel({ @@ -346,31 +323,29 @@ function generateForCategoryRecentPosts(req, res, next) { categories.getCategoryData(cid, next); }, posts: function (next) { - categories.getRecentReplies(cid, req.uid, 20, next); + categories.getRecentReplies(cid, req.uid || req.query.uid || 0, 20, next); }, }, next); }, function (results, next) { if (!results.category) { - return next(); + return controllers404.send404(req, res); } + category = results.category; + posts = results.posts; + validateTokenIfRequiresLogin(!results.privileges.read, cid, req, res, next); + }, + function () { + var feed = generateForPostsFeed({ + title: category.name + ' Recent Posts', + description: 'A list of recent posts from ' + category.name, + feed_url: '/category/' + cid + '/recentposts.rss', + site_url: '/category/' + cid + '/recentposts', + }, posts); - validateTokenIfRequiresLogin(!results.privileges.read, req, res, function (err) { - if (err) { - return next(err); - } - - var feed = generateForPostsFeed({ - title: results.category.name + ' Recent Posts', - description: 'A list of recent posts from ' + results.category.name, - feed_url: '/category/' + cid + '/recentposts.rss', - site_url: '/category/' + cid + '/recentposts', - }, results.posts); - - sendFeed(feed, res); - }); + sendFeed(feed, res); }, - ], next); + ], callback); } function generateForPostsFeed(feedOptions, posts) { @@ -397,6 +372,35 @@ function generateForPostsFeed(feedOptions, posts) { return feed; } +function generateForUserTopics(req, res, callback) { + if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { + return controllers404.send404(req, res); + } + + var userslug = req.params.userslug; + + async.waterfall([ + function (next) { + user.getUidByUserslug(userslug, next); + }, + function (uid, next) { + if (!uid) { + return callback(); + } + user.getUserFields(uid, ['uid', 'username'], next); + }, + function (userData, next) { + generateForTopics({ + uid: req.uid, + title: 'Topics by ' + userData.username, + description: 'A list of topics that are posted by ' + userData.username, + feed_url: '/user/' + userslug + '/topics.rss', + site_url: '/user/' + userslug + '/topics', + }, 'uid:' + userData.uid + ':topics', req, res, next); + }, + ], callback); +} + function generateForTag(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { return controllers404.send404(req, res); diff --git a/src/user/auth.js b/src/user/auth.js index fc1d995bf6..4347c860d1 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -52,19 +52,23 @@ module.exports = function (User) { if (!uid) { return callback(); } - - User.getUserField(uid, 'rss_token', function (err, token) { - if (err) { - return callback(err); - } - - if (!token) { - token = utils.generateUUID(); - User.setUserField(uid, 'rss_token', token); - } - - callback(false, token); - }); + var token; + async.waterfall([ + function (next) { + User.getUserField(uid, 'rss_token', next); + }, + function (_token, next) { + token = _token || utils.generateUUID(); + if (!_token) { + User.setUserField(uid, 'rss_token', token, next); + } else { + next(); + } + }, + function (next) { + next(null, token); + }, + ], callback); }; User.auth.clearLoginAttempts = function (uid) { diff --git a/test/feeds.js b/test/feeds.js index e8e3f8dfa4..325b44a6b6 100644 --- a/test/feeds.js +++ b/test/feeds.js @@ -12,6 +12,7 @@ var groups = require('../src/groups'); var user = require('../src/user'); var meta = require('../src/meta'); var privileges = require('../src/privileges'); +var helpers = require('./helpers'); describe('feeds', function () { var tid; @@ -113,4 +114,80 @@ describe('feeds', function () { }); }); }); + + describe('private feeds and tokens', function () { + var jar; + var rssToken; + before(function (done) { + helpers.loginUser('foo', 'barbar', function (err, _jar) { + assert.ifError(err); + jar = _jar; + done(); + }); + }); + + it('should load feed if its not private', function (done) { + request(nconf.get('url') + '/category/' + cid + '.rss', { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + + it('should not allow access if uid or token is missing', function (done) { + privileges.categories.rescind(['read'], cid, 'guests', function (err) { + assert.ifError(err); + async.parallel({ + test1: function (next) { + request(nconf.get('url') + '/category/' + cid + '.rss?uid=' + fooUid, { }, next); + }, + test2: function (next) { + request(nconf.get('url') + '/category/' + cid + '.rss?token=sometoken', { }, next); + }, + }, function (err, results) { + assert.ifError(err); + assert.equal(results.test1[0].statusCode, 200); + assert.equal(results.test2[0].statusCode, 200); + assert(results.test1[0].body.indexOf('Login to your account') !== -1); + assert(results.test2[0].body.indexOf('Login to your account') !== -1); + done(); + }); + }); + }); + + it('should not allow access if token is wrong', function (done) { + request(nconf.get('url') + '/category/' + cid + '.rss?uid=' + fooUid + '&token=sometoken', { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body.indexOf('Login to your account') !== -1); + done(); + }); + }); + + it('should allow access if token is correct', function (done) { + request(nconf.get('url') + '/api/category/' + cid, { jar: jar, json: true }, function (err, res, body) { + assert.ifError(err); + rssToken = body.rssFeedUrl.split('token')[1].slice(1); + request(nconf.get('url') + '/category/' + cid + '.rss?uid=' + fooUid + '&token=' + rssToken, { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + }); + + it('should not allow access if token is correct but has no privilege', function (done) { + privileges.categories.rescind(['read'], cid, 'registered-users', function (err) { + request(nconf.get('url') + '/category/' + cid + '.rss?uid=' + fooUid + '&token=' + rssToken, { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body.indexOf('Login to your account') !== -1); + done(); + }); + }); + }); + }); });