diff --git a/install/package.json b/install/package.json index 47938c1133..858a663b79 100644 --- a/install/package.json +++ b/install/package.json @@ -89,7 +89,7 @@ "nodebb-plugin-spam-be-gone": "0.6.7", "nodebb-rewards-essentials": "0.1.2", "nodebb-theme-lavender": "5.0.11", - "nodebb-theme-persona": "10.1.33", + "nodebb-theme-persona": "10.1.34", "nodebb-theme-slick": "1.2.28", "nodebb-theme-vanilla": "11.1.15", "nodebb-widget-essentials": "4.0.18", diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js index ae3412832d..2c7ddf4678 100644 --- a/src/meta/blacklist.js +++ b/src/meta/blacklist.js @@ -3,6 +3,7 @@ const ipaddr = require('ipaddr.js'); const winston = require('winston'); const _ = require('lodash'); +const validator = require('validator'); const db = require('../database'); const pubsub = require('../pubsub'); @@ -128,7 +129,7 @@ Blacklist.validate = function (rules) { } if (!addr || whitelist.includes(rule)) { - invalid.push(rule); + invalid.push(validator.escape(rule)); return false; } diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 397796b4a4..6f380b6563 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -110,6 +110,7 @@ module.exports = function (privileges) { return await utils.promiseParallel({ categories: categories.getCategoriesFields(cids, ['disabled']), allowedTo: helpers.isUserAllowedTo(privilege, uid, cids), + view_deleted: helpers.isUserAllowedTo('posts:view_deleted', uid, cids), isAdmin: user.isAdministrator(uid), }); }; diff --git a/src/privileges/posts.js b/src/privileges/posts.js index f8d824f943..ac595c0c31 100644 --- a/src/privileges/posts.js +++ b/src/privileges/posts.js @@ -88,16 +88,17 @@ module.exports = function (privileges) { cids = _.uniq(cids); const results = await privileges.categories.getBase(privilege, cids, uid); - cids = cids.filter(function (cid, index) { + const allowedCids = cids.filter(function (cid, index) { return !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin); }); - const cidsSet = new Set(cids); + const cidsSet = new Set(allowedCids); + const canViewDeleted = _.zipObject(cids, results.view_deleted); pids = postData.filter(function (post) { return post.topic && cidsSet.has(post.topic.cid) && - ((!post.topic.deleted && !post.deleted) || results.isAdmin); + ((!post.topic.deleted && !post.deleted) || canViewDeleted[post.topic.cid] || results.isAdmin); }).map(post => post.pid); const data = await plugins.fireHook('filter:privileges.posts.filter', { diff --git a/src/privileges/topics.js b/src/privileges/topics.js index af9e57698d..d4b34194ff 100644 --- a/src/privileges/topics.js +++ b/src/privileges/topics.js @@ -68,14 +68,15 @@ module.exports = function (privileges) { } const topicsData = await topics.getTopicsFields(tids, ['tid', 'cid', 'deleted']); - let cids = _.uniq(topicsData.map(topic => topic.cid)); + const cids = _.uniq(topicsData.map(topic => topic.cid)); const results = await privileges.categories.getBase(privilege, cids, uid); - cids = cids.filter((cid, index) => !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin)); + const allowedCids = cids.filter((cid, index) => !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin)); - const cidsSet = new Set(cids); + const cidsSet = new Set(allowedCids); + const canViewDeleted = _.zipObject(cids, results.view_deleted); - tids = topicsData.filter(t => cidsSet.has(t.cid) && (!t.deleted || results.isAdmin)).map(t => t.tid); + tids = topicsData.filter(t => cidsSet.has(t.cid) && (!t.deleted || canViewDeleted[t.cid] || results.isAdmin)).map(t => t.tid); const data = await plugins.fireHook('filter:privileges.topics.filter', { privilege: privilege, @@ -115,7 +116,7 @@ module.exports = function (privileges) { }; privileges.topics.canDelete = async function (tid, uid) { - const topicData = await topics.getTopicFields(tid, ['cid', 'postcount']); + const topicData = await topics.getTopicFields(tid, ['uid', 'cid', 'postcount', 'deleterUid']); const [isModerator, isAdministrator, isOwner, allowedTo] = await Promise.all([ user.isModerator(uid, topicData.cid), user.isAdministrator(uid), @@ -135,7 +136,8 @@ module.exports = function (privileges) { throw new Error(langKey); } - return allowedTo[0] && (isOwner || isModerator); + const deleterUid = topicData.deleterUid; + return allowedTo[0] && ((isOwner && (deleterUid === 0 || deleterUid === topicData.uid)) || isModerator); }; privileges.topics.canEdit = async function (tid, uid) { diff --git a/src/topics/data.js b/src/topics/data.js index 8ca113fbaf..44625c47f2 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -11,6 +11,7 @@ const intFields = [ 'tid', 'cid', 'uid', 'mainPid', 'postcount', 'viewcount', 'deleted', 'locked', 'pinned', 'timestamp', 'upvotes', 'downvotes', 'lastposttime', + 'deleterUid', ]; module.exports = function (Topics) { diff --git a/src/topics/index.js b/src/topics/index.js index 7473f5754a..57f738b282 100644 --- a/src/topics/index.js +++ b/src/topics/index.js @@ -50,8 +50,7 @@ Topics.getTopics = async function (tids, options) { } tids = await privileges.topics.filterTids('topics:read', tids, uid); - const topics = await Topics.getTopicsByTids(tids, options); - return topics; + return await Topics.getTopicsByTids(tids, options); }; Topics.getTopicsByTids = async function (tids, options) { diff --git a/src/user/online.js b/src/user/online.js index 3f07cd619d..05c40464f7 100644 --- a/src/user/online.js +++ b/src/user/online.js @@ -30,11 +30,8 @@ module.exports = function (User) { const now = Date.now(); const isArray = Array.isArray(uid); uid = isArray ? uid : [uid]; - const lastonline = db.sortedSetScores('users:online', uid); - const isOnline = uid.map(function (uid, index) { - return (now - lastonline[index]) < (meta.config.onlineCutoff * 60000); - }); - + const lastonline = await db.sortedSetScores('users:online', uid); + const isOnline = uid.map((uid, index) => (now - lastonline[index]) < (meta.config.onlineCutoff * 60000)); return isArray ? isOnline : isOnline[0]; }; }; diff --git a/test/posts.js b/test/posts.js index adcc4a7ec2..dc82563463 100644 --- a/test/posts.js +++ b/test/posts.js @@ -142,6 +142,14 @@ describe('Post\'s', function () { } }); + it('should fail to change owner if user is not authorized', async function () { + try { + await socketPosts.changeOwner({ uid: voterUid }, { pids: [1, 2], toUid: voterUid }); + } catch (err) { + assert.strictEqual(err.message, '[[error:no-privileges]]'); + } + }); + it('should return falsy if post does not exist', function (done) { posts.getPostData(9999, function (err, postData) { assert.ifError(err); diff --git a/test/topics.js b/test/topics.js index 00976ba3f3..c91395407b 100644 --- a/test/topics.js +++ b/test/topics.js @@ -23,9 +23,11 @@ describe('Topic\'s', function () { var categoryObj; var adminUid; var adminJar; + var fooUid; before(async function () { adminUid = await User.create({ username: 'admin', password: '123456' }); + fooUid = await User.create({ username: 'foo' }); await groups.join('administrators', adminUid); adminJar = await helpers.loginUser('admin', '123456'); @@ -572,6 +574,21 @@ describe('Topic\'s', function () { }); }); }); + + it('should not allow user to restore their topic if it was deleted by an admin', async function () { + const result = await topics.post({ + uid: fooUid, + title: 'topic for restore test', + content: 'topic content', + cid: categoryObj.cid, + }); + await socketTopics.delete({ uid: adminUid }, { tids: [result.topicData.tid], cid: categoryObj.cid }); + try { + await socketTopics.restore({ uid: fooUid }, { tids: [result.topicData.tid], cid: categoryObj.cid }); + } catch (err) { + assert.strictEqual(err.message, '[[error:no-privileges]]'); + } + }); }); describe('order pinned topics', function () { diff --git a/test/user.js b/test/user.js index 2faf71fb70..66654cd649 100644 --- a/test/user.js +++ b/test/user.js @@ -2164,10 +2164,20 @@ describe('User', function () { }); }); - it('should return offline if user is guest', function (done) { - var status = User.getStatus({ uid: 0 }); - assert.strictEqual(status, 'offline'); - done(); + describe('status/online', function () { + it('should return offline if user is guest', function (done) { + var status = User.getStatus({ uid: 0 }); + assert.strictEqual(status, 'offline'); + done(); + }); + + it('should return offline if user is guest', async function () { + assert.strictEqual(await User.isOnline(0), false); + }); + + it('should return true', async function () { + assert.strictEqual(await User.isOnline(testUid), true); + }); }); describe('isPrivilegedOrSelf', function () {