diff --git a/public/language/da/error.json b/public/language/da/error.json
index c2419381c8..509c6539bb 100644
--- a/public/language/da/error.json
+++ b/public/language/da/error.json
@@ -20,7 +20,7 @@
"invalid-pagination-value": "Ugyldig side værdi, skal mindst være %1 og maks. %2",
"username-taken": "Brugernavn optaget",
"email-taken": "Emailadresse allerede i brug",
- "email-not-confirmed": "Din email adresse er ikke blevet bekræftet endnu, venligst klik her for at bekrætige den.",
+ "email-not-confirmed": "Din email adresse er ikke blevet bekræftet endnu, klik venligst her for at bekræfte den.",
"email-not-confirmed-chat": "Du kan ikke chatte før din email er bekræftet, klik her for at bekræfte din email.",
"email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.",
"no-email-to-confirm": "Dette forum kræver bekræftelse af din email, klik her for at indtaste en email",
diff --git a/public/language/en-GB/admin/manage/post-queue.json b/public/language/en-GB/admin/manage/post-queue.json
new file mode 100644
index 0000000000..5c52e25538
--- /dev/null
+++ b/public/language/en-GB/admin/manage/post-queue.json
@@ -0,0 +1,8 @@
+{
+ "post-queue": "Post Queue",
+ "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Posting Restrictions and enable Post Queue.",
+ "user": "User",
+ "title": "Title",
+ "content": "Content",
+ "posted": "Posted"
+}
\ No newline at end of file
diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json
index 985c540e8a..3ec4ad41c4 100644
--- a/public/language/en-GB/admin/menu.json
+++ b/public/language/en-GB/admin/menu.json
@@ -12,6 +12,7 @@
"manage/tags": "Tags",
"manage/users": "Users",
"manage/registration": "Registration Queue",
+ "manage/post-queue": "Post Queue",
"manage/groups": "Groups",
"manage/ip-blacklist": "IP Blacklist",
diff --git a/public/language/en-GB/admin/settings/post.json b/public/language/en-GB/admin/settings/post.json
index aca8b39d64..a789025597 100644
--- a/public/language/en-GB/admin/settings/post.json
+++ b/public/language/en-GB/admin/settings/post.json
@@ -6,6 +6,8 @@
"sorting.most-votes": "Most Votes",
"sorting.topic-default": "Default Topic Sorting",
"restrictions": "Posting Restrictions",
+ "restrictions.post-queue": "Enable post queue",
+ "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval.",
"restrictions.seconds-between": "Seconds between Posts",
"restrictions.seconds-between-new": "Seconds between Posts for New Users",
"restrictions.rep-threshold": "Reputation threshold before this restriction is lifted",
diff --git a/public/language/en-GB/success.json b/public/language/en-GB/success.json
index 497b6fcd20..17093d3efe 100644
--- a/public/language/en-GB/success.json
+++ b/public/language/en-GB/success.json
@@ -1,6 +1,7 @@
{
"success": "Success",
"topic-post": "You have successfully posted.",
+ "post-queued": "Your post is queued for approval.",
"authentication-successful": "Authentication Successful",
"settings-saved": "Settings saved!"
}
\ No newline at end of file
diff --git a/public/language/zh-CN/admin/general/dashboard.json b/public/language/zh-CN/admin/general/dashboard.json
index a16f2dd433..f89eecfc59 100644
--- a/public/language/zh-CN/admin/general/dashboard.json
+++ b/public/language/zh-CN/admin/general/dashboard.json
@@ -24,7 +24,7 @@
"keep-updated": "请确保您已及时更新 NodeBB 以获得最新的安全补丁与 Bug 修复。",
"up-to-date": "
正在使用 最新版本
", "upgrade-available": "新版本 (v%1) 已经发布! 建议 更新你的 NodeBB。
", - "prerelease-upgrade-available": "你正在使用NodeBB过期的实验版本。新的版本 (v%1) 已经发布。 请考虑更新你的 NodeBB。
", + "prerelease-upgrade-available": "正在使用NodeBB过期的实验版本。新的版本 (v%1) 已经发布。 请考虑更新你的 NodeBB。", "prerelease-warning": "
正在使用测试版 NodeBB。可能会出现意外的 Bug。
", "running-in-development": "论坛正处于开发模式,这可能使其暴露于潜在的危险之中;请联系您的系统管理员。", diff --git a/public/src/admin/manage/post-queue.js b/public/src/admin/manage/post-queue.js new file mode 100644 index 0000000000..2bb9931dec --- /dev/null +++ b/public/src/admin/manage/post-queue.js @@ -0,0 +1,25 @@ +'use strict'; + + +define('admin/manage/post-queue', function () { + var PostQueue = {}; + + PostQueue.init = function () { + $('.posts-list').on('click', '[data-action]', function () { + var parent = $(this).parents('[data-id]'); + var action = $(this).attr('data-action'); + var id = parent.attr('data-id'); + var method = action === 'accept' ? 'posts.accept' : 'posts.reject'; + + socket.emit(method, { id: id }, function (err) { + if (err) { + return app.alertError(err.message); + } + parent.remove(); + }); + return false; + }); + }; + + return PostQueue; +}); diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 91922bec0b..6ef000fa14 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -4,6 +4,7 @@ var adminController = { dashboard: require('./admin/dashboard'), categories: require('./admin/categories'), tags: require('./admin/tags'), + postQueue: require('./admin/postqueue'), blacklist: require('./admin/blacklist'), groups: require('./admin/groups'), appearance: require('./admin/appearance'), diff --git a/src/controllers/admin/postqueue.js b/src/controllers/admin/postqueue.js new file mode 100644 index 0000000000..e401f7dedd --- /dev/null +++ b/src/controllers/admin/postqueue.js @@ -0,0 +1,66 @@ +'use strict'; + +var async = require('async'); + +var db = require('../../database'); +var user = require('../../user'); +var pagination = require('../../pagination'); +var utils = require('../../utils'); + +var postQueueController = module.exports; + +postQueueController.get = function (req, res, next) { + var page = parseInt(req.query.page, 10) || 1; + var postsPerPage = 20; + var pageCount = 0; + + var start = (page - 1) * postsPerPage; + var stop = start + postsPerPage - 1; + + var postData; + + async.waterfall([ + function (next) { + async.parallel({ + count: function (next) { + db.sortedSetCard('post:queue', next); + }, + ids: function (next) { + db.getSortedSetRange('post:queue', start, stop, next); + }, + }, next); + }, + function (results, next) { + pageCount = Math.ceil(results.count / postsPerPage); + + var keys = results.ids.map(function (id) { + return 'post:queue:' + id; + }); + + db.getObjects(keys, next); + }, + function (data, next) { + postData = data; + data.forEach(function (data) { + data.data = JSON.parse(data.data); + data.data.timestampISO = utils.toISOString(data.data.timestamp); + return data; + }); + var uids = data.map(function (data) { + return data && data.uid; + }); + user.getUsersFields(uids, ['username', 'userslug', 'picture'], next); + }, + function (userData) { + postData.forEach(function (postData, index) { + postData.user = userData[index]; + }); + + res.render('admin/manage/post-queue', { + title: '[[pages:post-queue]]', + posts: postData, + pagination: pagination.create(page, pageCount), + }); + }, + ], next); +}; diff --git a/src/controllers/globalmods.js b/src/controllers/globalmods.js index 1cae97a4e2..19a5bd02ee 100644 --- a/src/controllers/globalmods.js +++ b/src/controllers/globalmods.js @@ -4,6 +4,7 @@ var async = require('async'); var user = require('../user'); var adminBlacklistController = require('./admin/blacklist'); +var adminPostQueueController = require('./admin/postqueue'); var globalModsController = module.exports; @@ -20,3 +21,17 @@ globalModsController.ipBlacklist = function (req, res, next) { }, ], next); }; + +globalModsController.postQueue = function (req, res, next) { + async.waterfall([ + function (next) { + user.isAdminOrGlobalMod(req.uid, next); + }, + function (isAdminOrGlobalMod, next) { + if (!isAdminOrGlobalMod) { + return next(); + } + adminPostQueueController.get(req, res, next); + }, + ], next); +}; diff --git a/src/posts.js b/src/posts.js index 7c4678830f..f6b22b89ed 100644 --- a/src/posts.js +++ b/src/posts.js @@ -24,6 +24,7 @@ require('./posts/recent')(Posts); require('./posts/tools')(Posts); require('./posts/votes')(Posts); require('./posts/bookmarks')(Posts); +require('./posts/queue')(Posts); Posts.exists = function (pid, callback) { db.isSortedSetMember('posts:pid', pid, callback); diff --git a/src/posts/queue.js b/src/posts/queue.js new file mode 100644 index 0000000000..3b9f555f9d --- /dev/null +++ b/src/posts/queue.js @@ -0,0 +1,153 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var user = require('../user'); +var meta = require('../meta'); +var topics = require('../topics'); +var privileges = require('../privileges'); +var socketHelpers = require('../socket.io/helpers'); + +module.exports = function (Posts) { + Posts.shouldQueue = function (uid, data, callback) { + async.waterfall([ + function (next) { + user.getUserFields(uid, ['reputation', 'postcount'], next); + }, + function (userData, next) { + var shouldQueue = parseInt(meta.config.postQueue, 10) === 1 && (!parseInt(uid, 10) || (parseInt(userData.reputation, 10) <= 0 && parseInt(userData.postcount, 10) <= 0)); + next(null, shouldQueue); + }, + ], callback); + }; + + Posts.addToQueue = function (data, callback) { + var type = data.title ? 'topic' : 'reply'; + var id = type + '-' + Date.now(); + async.waterfall([ + function (next) { + canPost(type, data, next); + }, + function (next) { + db.sortedSetAdd('post:queue', Date.now(), id, next); + }, + function (next) { + db.setObject('post:queue:' + id, { + id: id, + uid: data.uid, + type: type, + data: JSON.stringify(data), + }, next); + }, + function (next) { + user.setUserField(data.uid, 'lastposttime', Date.now(), next); + }, + function (next) { + next(null, { + queued: true, + message: '[[success:post-queued]]', + }); + }, + ], callback); + }; + + function canPost(type, data, callback) { + async.waterfall([ + function (next) { + if (type === 'topic') { + next(null, data.cid); + } else if (type === 'reply') { + topics.getTopicField(data.tid, 'cid', next); + } + }, + function (cid, next) { + async.parallel({ + canPost: function (next) { + if (type === 'topic') { + privileges.categories.can('topics:create', data.cid, data.uid, next); + } else if (type === 'reply') { + privileges.categories.can('topics:reply', cid, data.uid, next); + } + }, + isReadyToPost: function (next) { + user.isReadyToPost(data.uid, cid, next); + }, + }, next); + }, + function (results, next) { + if (!results.canPost) { + return next(new Error('[[error:no-privileges]]')); + } + next(); + }, + ], callback); + } + + Posts.removeFromQueue = function (id, callback) { + async.waterfall([ + function (next) { + db.sortedSetRemove('post:queue', id, next); + }, + function (next) { + db.delete('post:queue:' + id, next); + }, + ], callback); + }; + + Posts.submitFromQueue = function (id, callback) { + async.waterfall([ + function (next) { + db.getObject('post:queue:' + id, next); + }, + function (data, next) { + if (!data) { + return callback(); + } + try { + data.data = JSON.parse(data.data); + } catch (err) { + return next(err); + } + + if (data.type === 'topic') { + createTopic(data.data, next); + } else if (data.type === 'reply') { + createReply(data.data, next); + } + }, + function (next) { + Posts.removeFromQueue(id, next); + }, + ], callback); + }; + + function createTopic(data, callback) { + async.waterfall([ + function (next) { + topics.post(data, next); + }, + function (result, next) { + socketHelpers.notifyNew(data.uid, 'newTopic', { posts: [result.postData], topic: result.topicData }); + next(); + }, + ], callback); + } + + function createReply(data, callback) { + async.waterfall([ + function (next) { + topics.reply(data, next); + }, + function (postData, next) { + var result = { + posts: [postData], + 'reputation:disabled': parseInt(meta.config['reputation:disabled'], 10) === 1, + 'downvote:disabled': parseInt(meta.config['downvote:disabled'], 10) === 1, + }; + socketHelpers.notifyNew(data.uid, 'newPost', result); + next(); + }, + ], callback); + } +}; diff --git a/src/routes/admin.js b/src/routes/admin.js index 42fd935aa7..ba4048516e 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -56,6 +56,7 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics); router.get('/manage/tags', middlewares, controllers.admin.tags.get); + router.get('/manage/post-queue', middlewares, controllers.admin.postQueue.get); router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get); router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate); diff --git a/src/routes/index.js b/src/routes/index.js index 7084e6554b..52375f9f67 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -44,6 +44,7 @@ function modRoutes(app, middleware, controllers) { } function globalModRoutes(app, middleware, controllers) { + setupPageRoute(app, '/post-queue', middleware, [], controllers.globalMods.postQueue); setupPageRoute(app, '/ip-blacklist', middleware, [], controllers.globalMods.ipBlacklist); } diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index d5b586d627..a5462ef5bb 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -30,6 +30,21 @@ SocketPosts.reply = function (socket, data, callback) { data.req = websockets.reqFromSocket(socket); data.timestamp = Date.now(); + async.waterfall([ + function (next) { + posts.shouldQueue(socket.uid, data, next); + }, + function (shouldQueue, next) { + if (shouldQueue) { + posts.addToQueue(data, next); + } else { + postReply(socket, data, next); + } + }, + ], callback); +}; + +function postReply(socket, data, callback) { async.waterfall([ function (next) { topics.reply(data, next); @@ -50,7 +65,7 @@ SocketPosts.reply = function (socket, data, callback) { socketHelpers.notifyNew(socket.uid, 'newPost', result); }, ], callback); -}; +} SocketPosts.getRawPost = function (socket, pid, callback) { async.waterfall([ @@ -152,3 +167,26 @@ SocketPosts.getReplies = function (socket, pid, callback) { }, ], callback); }; + +SocketPosts.accept = function (socket, data, callback) { + acceptOrReject(posts.submitFromQueue, socket, data, callback); +}; + +SocketPosts.reject = function (socket, data, callback) { + acceptOrReject(posts.removeFromQueue, socket, data, callback); +}; + +function acceptOrReject(method, socket, data, callback) { + async.waterfall([ + function (next) { + user.isAdminOrGlobalMod(socket.uid, next); + }, + function (isAdminOrGlobalMod, next) { + if (!isAdminOrGlobalMod) { + return callback(new Error('[[error:no-privileges]]')); + } + + method(data.id, next); + }, + ], callback); +} diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index 3b8a69d0f4..f8d333e67a 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -3,6 +3,7 @@ var async = require('async'); var topics = require('../topics'); +var posts = require('../posts'); var websockets = require('./index'); var user = require('../user'); var apiController = require('../controllers/api'); @@ -25,6 +26,21 @@ SocketTopics.post = function (socket, data, callback) { data.req = websockets.reqFromSocket(socket); data.timestamp = Date.now(); + async.waterfall([ + function (next) { + posts.shouldQueue(socket.uid, data, next); + }, + function (shouldQueue, next) { + if (shouldQueue) { + posts.addToQueue(data, next); + } else { + postTopic(socket, data, next); + } + }, + ], callback); +}; + +function postTopic(socket, data, callback) { async.waterfall([ function (next) { topics.post(data, next); @@ -38,7 +54,7 @@ SocketTopics.post = function (socket, data, callback) { socketHelpers.notifyNew(socket.uid, 'newTopic', { posts: [result.postData], topic: result.topicData }); }, ], callback); -}; +} SocketTopics.postcount = function (socket, tid, callback) { topics.getTopicField(tid, 'postcount', callback); diff --git a/src/views/admin/manage/post-queue.tpl b/src/views/admin/manage/post-queue.tpl new file mode 100644 index 0000000000..86c70f2f21 --- /dev/null +++ b/src/views/admin/manage/post-queue.tpl @@ -0,0 +1,59 @@ ++ [[admin/manage/post-queue:description, {config.relative_path}/admin/settings/post#posting-restrictions]] +
+ + +[[admin/manage/post-queue:user]] | +[[admin/manage/post-queue:title]] | +[[admin/manage/post-queue:content]] | +[[admin/manage/post-queue:posted]] | ++ |
---|---|---|---|---|
+ + {posts.user.username} + + {posts.user.username} + + | ++ {posts.data.title} + | ++ {posts.data.content} + | ++ + | +
+
+
+
+
+ |
+