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/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 5139dda53f..14d8f8cee4 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -42,6 +42,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} + | ++ + | +
+
+
+
+
+ |
+