diff --git a/src/posts/parse.js b/src/posts/parse.js index f1f319ec24..a90b511136 100644 --- a/src/posts/parse.js +++ b/src/posts/parse.js @@ -1,6 +1,5 @@ 'use strict'; -var async = require('async'); var nconf = require('nconf'); var url = require('url'); var winston = require('winston'); @@ -21,37 +20,31 @@ module.exports = function (Posts) { length: 5, }; - Posts.parsePost = function (postData, callback) { + Posts.parsePost = async function (postData) { if (!postData) { - return setImmediate(callback, null, postData); + return postData; } postData.content = String(postData.content || ''); - var cache = require('./cache'); - if (postData.pid && cache.has(String(postData.pid))) { - postData.content = cache.get(String(postData.pid)); + const cache = require('./cache'); + const pid = String(postData.pid); + const cachedContent = cache.get(pid); + if (postData.pid && cachedContent !== undefined) { + postData.content = cachedContent; cache.hits += 1; - return callback(null, postData); + return postData; } cache.misses += 1; - - async.waterfall([ - function (next) { - plugins.fireHook('filter:parse.post', { postData: postData }, next); - }, - function (data, next) { - data.postData.content = translator.escape(data.postData.content); - - if (global.env === 'production' && data.postData.pid) { - cache.set(String(data.postData.pid), data.postData.content); - } - next(null, data.postData); - }, - ], callback); + const data = await plugins.fireHook('filter:parse.post', { postData: postData }); + data.postData.content = translator.escape(data.postData.content); + if (global.env === 'production' && data.postData.pid) { + cache.set(pid, data.postData.content); + } + return data.postData; }; - Posts.parseSignature = function (userData, uid, callback) { + Posts.parseSignature = async function (userData, uid) { userData.signature = sanitizeSignature(userData.signature || ''); - plugins.fireHook('filter:parse.signature', { userData: userData, uid: uid }, callback); + return await plugins.fireHook('filter:parse.signature', { userData: userData, uid: uid }); }; Posts.relativeToAbsolute = function (content, regex) { diff --git a/src/posts/queue.js b/src/posts/queue.js index 6a6f6d989b..f84e707c7a 100644 --- a/src/posts/queue.js +++ b/src/posts/queue.js @@ -1,6 +1,5 @@ 'use strict'; -var async = require('async'); const _ = require('lodash'); var db = require('../database'); @@ -15,296 +14,169 @@ var plugins = require('../plugins'); var socketHelpers = require('../socket.io/helpers'); module.exports = function (Posts) { - Posts.shouldQueue = function (uid, data, callback) { - async.waterfall([ - function (next) { - user.getUserFields(uid, ['uid', 'reputation', 'postcount'], next); - }, - function (userData, next) { - const shouldQueue = meta.config.postQueue && (!userData.uid || userData.reputation < meta.config.newbiePostDelayThreshold || userData.postcount <= 0); - plugins.fireHook('filter:post.shouldQueue', { - shouldQueue: shouldQueue, - uid: uid, - data: data, - }, next); - }, - function (result, next) { - next(null, result.shouldQueue); - }, - ], callback); + Posts.shouldQueue = async function (uid, data) { + const userData = await user.getUserFields(uid, ['uid', 'reputation', 'postcount']); + const shouldQueue = meta.config.postQueue && (!userData.uid || userData.reputation < meta.config.newbiePostDelayThreshold || userData.postcount <= 0); + const result = await plugins.fireHook('filter:post.shouldQueue', { + shouldQueue: !!shouldQueue, + uid: uid, + data: data, + }); + return result.shouldQueue; }; - function removeQueueNotification(id, callback) { - async.waterfall([ - function (next) { - notifications.rescind('post-queue-' + id, next); - }, - function (next) { - getParsedObject(id, next); - }, - function (data, next) { - if (!data) { - return callback(); - } - getCid(data.type, data, next); - }, - function (cid, next) { - getNotificationUids(cid, next); - }, - function (uids, next) { - uids.forEach(uid => user.notifications.pushCount(uid)); - next(); - }, - ], callback); + async function removeQueueNotification(id) { + await notifications.rescind('post-queue-' + id); + const data = await getParsedObject(id); + if (!data) { + return; + } + const cid = await getCid(data.type, data); + const uids = await getNotificationUids(cid); + uids.forEach(uid => user.notifications.pushCount(uid)); } - function getNotificationUids(cid, callback) { - async.waterfall([ - function (next) { - async.parallel([ - async.apply(groups.getMembersOfGroups, ['administrators', 'Global Moderators']), - async.apply(categories.getModeratorUids, [cid]), - ], next); - }, - function (results, next) { - next(null, _.uniq(_.flattenDeep(results))); - }, - ], callback); + async function getNotificationUids(cid) { + const results = await Promise.all([ + groups.getMembersOfGroups(['administrators', 'Global Moderators']), + categories.getModeratorUids([cid]), + ]); + return _.uniq(_.flattenDeep(results)); } - 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, 'lastqueuetime', Date.now(), next); - }, - function (next) { - async.parallel({ - notification: function (next) { - notifications.create({ - type: 'post-queue', - nid: 'post-queue-' + id, - mergeId: 'post-queue', - bodyShort: '[[notifications:post_awaiting_review]]', - bodyLong: data.content, - path: '/post-queue', - }, next); - }, - uids: function (next) { - async.waterfall([ - function (next) { - getCid(type, data, next); - }, - function (cid, next) { - getNotificationUids(cid, next); - }, - ], next); - }, - }, next); - }, - function (results, next) { - if (results.notification) { - notifications.push(results.notification, results.uids, next); - } else { - next(); - } - }, - function (next) { - next(null, { - id: id, - type: type, - queued: true, - message: '[[success:post-queued]]', - }); - }, - ], callback); + Posts.addToQueue = async function (data) { + const type = data.title ? 'topic' : 'reply'; + const now = Date.now(); + const id = type + '-' + now; + await canPost(type, data); + await db.sortedSetAdd('post:queue', now, id); + await db.setObject('post:queue:' + id, { + id: id, + uid: data.uid, + type: type, + data: JSON.stringify(data), + }); + await user.setUserField(data.uid, 'lastqueuetime', now); + + const cid = await getCid(type, data); + const uids = await getNotificationUids(cid); + const notifObj = await notifications.create({ + type: 'post-queue', + nid: 'post-queue-' + id, + mergeId: 'post-queue', + bodyShort: '[[notifications:post_awaiting_review]]', + bodyLong: data.content, + path: '/post-queue', + }); + await notifications.push(notifObj, uids); + return { + id: id, + type: type, + queued: true, + message: '[[success:post-queued]]', + }; }; - function getCid(type, data, callback) { + async function getCid(type, data) { if (type === 'topic') { - return setImmediate(callback, null, data.cid); + return data.cid; } else if (type === 'reply') { - topics.getTopicField(data.tid, 'cid', callback); - } else { - return setImmediate(callback, null, null); + return await topics.getTopicField(data.tid, 'cid'); + } + return null; + } + + async function canPost(type, data) { + const cid = await getCid(type, data); + const typeToPrivilege = { + topic: 'topics:create', + reply: 'topics:reply', + }; + + const [canPost] = await Promise.all([ + privileges.categories.can(typeToPrivilege[type], cid, data.uid), + user.isReadyToQueue(data.uid, cid), + ]); + if (!canPost) { + throw new Error('[[error:no-privileges]]'); } } - function canPost(type, data, callback) { - async.waterfall([ - function (next) { - getCid(type, data, next); - }, - function (cid, next) { - async.parallel({ - canPost: function (next) { - if (type === 'topic') { - privileges.categories.can('topics:create', cid, data.uid, next); - } else if (type === 'reply') { - privileges.categories.can('topics:reply', cid, data.uid, next); - } - }, - isReadyToQueue: function (next) { - user.isReadyToQueue(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) { - removeQueueNotification(id, next); - }, - function (next) { - db.sortedSetRemove('post:queue', id, next); - }, - function (next) { - db.delete('post:queue:' + id, next); - }, - ], callback); + Posts.removeFromQueue = async function (id) { + await removeQueueNotification(id); + await db.sortedSetRemove('post:queue', id); + await db.delete('post:queue:' + id); }; - Posts.submitFromQueue = function (id, callback) { - async.waterfall([ - function (next) { - getParsedObject(id, next); - }, - function (data, next) { - if (!data) { - return callback(); - } - if (data.type === 'topic') { - createTopic(data.data, next); - } else if (data.type === 'reply') { - createReply(data.data, next); - } - }, - function (next) { - Posts.removeFromQueue(id, next); - }, - ], callback); + Posts.submitFromQueue = async function (id) { + const data = await getParsedObject(id); + if (!data) { + return; + } + if (data.type === 'topic') { + await createTopic(data.data); + } else if (data.type === 'reply') { + await createReply(data.data); + } + await Posts.removeFromQueue(id); }; - function getParsedObject(id, callback) { - async.waterfall([ - function (next) { - db.getObject('post:queue:' + id, next); - }, - function (data, next) { - if (!data) { - return callback(null, null); - } - try { - data.data = JSON.parse(data.data); - } catch (err) { - return next(err); - } - next(null, data); - }, - ], callback); + async function getParsedObject(id) { + const data = await db.getObject('post:queue:' + id); + if (!data) { + return null; + } + data.data = JSON.parse(data.data); + return data; } - 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); + async function createTopic(data) { + const result = await topics.post(data); + socketHelpers.notifyNew(data.uid, 'newTopic', { posts: [result.postData], topic: result.topicData }); } - function createReply(data, callback) { - async.waterfall([ - function (next) { - topics.reply(data, next); - }, - function (postData, next) { - var result = { - posts: [postData], - 'reputation:disabled': !!meta.config['reputation:disabled'], - 'downvote:disabled': !!meta.config['downvote:disabled'], - }; - socketHelpers.notifyNew(data.uid, 'newPost', result); - next(); - }, - ], callback); + async function createReply(data) { + const postData = await topics.reply(data); + const result = { + posts: [postData], + 'reputation:disabled': !!meta.config['reputation:disabled'], + 'downvote:disabled': !!meta.config['downvote:disabled'], + }; + socketHelpers.notifyNew(data.uid, 'newPost', result); } - Posts.editQueuedContent = function (uid, id, content, callback) { - async.waterfall([ - function (next) { - Posts.canEditQueue(uid, id, next); - }, - function (canEditQueue, next) { - if (!canEditQueue) { - return callback(new Error('[[error:no-privileges]]')); - } - getParsedObject(id, next); - }, - function (data, next) { - if (!data) { - return callback(); - } - data.data.content = content; - db.setObjectField('post:queue:' + id, 'data', JSON.stringify(data.data), next); - }, - ], callback); + Posts.editQueuedContent = async function (uid, id, content) { + const canEditQueue = await Posts.canEditQueue(uid, id); + if (!canEditQueue) { + throw new Error('[[error:no-privileges]]'); + } + const data = await getParsedObject(id); + if (!data) { + return; + } + data.data.content = content; + await db.setObjectField('post:queue:' + id, 'data', JSON.stringify(data.data)); }; - Posts.canEditQueue = function (uid, id, callback) { - async.waterfall([ - function (next) { - async.parallel({ - isAdminOrGlobalMod: function (next) { - user.isAdminOrGlobalMod(uid, next); - }, - data: function (next) { - getParsedObject(id, next); - }, - }, next); - }, - function (results, next) { - if (results.isAdminOrGlobalMod) { - return callback(null, true); - } - if (!results.data) { - return callback(null, false); - } - if (results.data.type === 'topic') { - next(null, results.data.data.cid); - } else if (results.data.type === 'reply') { - topics.getTopicField(results.data.data.tid, 'cid', next); - } - }, - function (cid, next) { - user.isModerator(uid, cid, next); - }, - ], callback); + Posts.canEditQueue = async function (uid, id) { + const [isAdminOrGlobalMod, data] = await Promise.all([ + user.isAdminOrGlobalMod(uid), + getParsedObject(id), + ]); + if (!data) { + return false; + } + + if (isAdminOrGlobalMod) { + return true; + } + + let cid; + if (data.type === 'topic') { + cid = data.data.cid; + } else if (data.type === 'reply') { + cid = await topics.getTopicField(data.data.tid, 'cid'); + } + return await user.isModerator(uid, cid); }; }; diff --git a/src/posts/recent.js b/src/posts/recent.js index 879f537034..2ad84b0c7c 100644 --- a/src/posts/recent.js +++ b/src/posts/recent.js @@ -1,53 +1,33 @@ 'use strict'; -var async = require('async'); -var _ = require('lodash'); +const _ = require('lodash'); -var db = require('../database'); -var privileges = require('../privileges'); +const db = require('../database'); +const privileges = require('../privileges'); module.exports = function (Posts) { - var terms = { + const terms = { day: 86400000, week: 604800000, month: 2592000000, }; - Posts.getRecentPosts = function (uid, start, stop, term, callback) { - var min = 0; + Posts.getRecentPosts = async function (uid, start, stop, term) { + let min = 0; if (terms[term]) { min = Date.now() - terms[term]; } - var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; - - async.waterfall([ - function (next) { - db.getSortedSetRevRangeByScore('posts:pid', start, count, '+inf', min, next); - }, - function (pids, next) { - privileges.posts.filter('topics:read', pids, uid, next); - }, - function (pids, next) { - Posts.getPostSummaryByPids(pids, uid, { stripTags: true }, next); - }, - ], callback); + const count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; + let pids = await db.getSortedSetRevRangeByScore('posts:pid', start, count, '+inf', min); + pids = await privileges.posts.filter('topics:read', pids, uid); + return await Posts.getPostSummaryByPids(pids, uid, { stripTags: true }); }; - Posts.getRecentPosterUids = function (start, stop, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRevRange('posts:pid', start, stop, next); - }, - function (pids, next) { - Posts.getPostsFields(pids, ['uid'], next); - }, - function (postData, next) { - var uids = _.uniq(postData.map(post => post && post.uid).filter(uid => parseInt(uid, 10))); - - next(null, uids); - }, - ], callback); + Posts.getRecentPosterUids = async function (start, stop) { + const pids = await db.getSortedSetRevRange('posts:pid', start, stop); + const postData = await Posts.getPostsFields(pids, ['uid']); + return _.uniq(postData.map(p => p && p.uid).filter(uid => parseInt(uid, 10))); }; }; diff --git a/src/posts/summary.js b/src/posts/summary.js index ec746224b8..fd29f9e667 100644 --- a/src/posts/summary.js +++ b/src/posts/summary.js @@ -1,123 +1,83 @@ 'use strict'; -var async = require('async'); -var validator = require('validator'); -var _ = require('lodash'); +const validator = require('validator'); +const _ = require('lodash'); -var topics = require('../topics'); -var user = require('../user'); -var plugins = require('../plugins'); -var categories = require('../categories'); -var utils = require('../utils'); +const topics = require('../topics'); +const user = require('../user'); +const plugins = require('../plugins'); +const categories = require('../categories'); +const utils = require('../utils'); module.exports = function (Posts) { - Posts.getPostSummaryByPids = function (pids, uid, options, callback) { + Posts.getPostSummaryByPids = async function (pids, uid, options) { if (!Array.isArray(pids) || !pids.length) { - return callback(null, []); + return []; } options.stripTags = options.hasOwnProperty('stripTags') ? options.stripTags : false; options.parse = options.hasOwnProperty('parse') ? options.parse : true; options.extraFields = options.hasOwnProperty('extraFields') ? options.extraFields : []; - var fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted', 'upvotes', 'downvotes'].concat(options.extraFields); + const fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted', 'upvotes', 'downvotes'].concat(options.extraFields); - var posts; - async.waterfall([ - function (next) { - Posts.getPostsFields(pids, fields, next); - }, - function (_posts, next) { - posts = _posts.filter(Boolean); - user.blocks.filter(uid, posts, next); - }, - function (_posts, next) { - posts = _posts; - var uids = {}; - var topicKeys = {}; + let posts = await Posts.getPostsFields(pids, fields); + posts = posts.filter(Boolean); + posts = await user.blocks.filter(uid, posts); - posts.forEach(function (post) { - uids[post.uid] = 1; - topicKeys[post.tid] = 1; - }); - async.parallel({ - users: function (next) { - user.getUsersFields(Object.keys(uids), ['uid', 'username', 'userslug', 'picture', 'status'], next); - }, - topicsAndCategories: function (next) { - getTopicAndCategories(Object.keys(topicKeys), next); - }, - }, next); - }, - function (results, next) { - results.users = toObject('uid', results.users); - results.topics = toObject('tid', results.topicsAndCategories.topics); - results.categories = toObject('cid', results.topicsAndCategories.categories); + const uids = _.uniq(posts.map(p => p && p.uid)); + const tids = _.uniq(posts.map(p => p && p.tid)); - posts.forEach(function (post) { - // If the post author isn't represented in the retrieved users' data, then it means they were deleted, assume guest. - if (!results.users.hasOwnProperty(post.uid)) { - post.uid = 0; - } - post.user = results.users[post.uid]; - post.topic = results.topics[post.tid]; - post.category = post.topic && results.categories[post.topic.cid]; - post.isMainPost = post.topic && post.pid === post.topic.mainPid; - post.deleted = post.deleted === 1; - post.timestampISO = utils.toISOString(post.timestamp); - }); + const [users, topicsAndCategories] = await Promise.all([ + user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status']), + getTopicAndCategories(tids), + ]); - posts = posts.filter(function (post) { - return results.topics[post.tid]; - }); + const uidToUser = toObject('uid', users); + const tidToTopic = toObject('tid', topicsAndCategories.topics); + const cidToCategory = toObject('cid', topicsAndCategories.categories); - parsePosts(posts, options, next); - }, - function (posts, next) { - plugins.fireHook('filter:post.getPostSummaryByPids', { posts: posts, uid: uid }, next); - }, - function (data, next) { - next(null, data.posts); - }, - ], callback); + posts.forEach(function (post) { + // If the post author isn't represented in the retrieved users' data, then it means they were deleted, assume guest. + if (!uidToUser.hasOwnProperty(post.uid)) { + post.uid = 0; + } + post.user = uidToUser[post.uid]; + post.topic = tidToTopic[post.tid]; + post.category = post.topic && cidToCategory[post.topic.cid]; + post.isMainPost = post.topic && post.pid === post.topic.mainPid; + post.deleted = post.deleted === 1; + post.timestampISO = utils.toISOString(post.timestamp); + }); + + posts = posts.filter(post => tidToTopic[post.tid]); + + posts = await parsePosts(posts, options); + const result = await plugins.fireHook('filter:post.getPostSummaryByPids', { posts: posts, uid: uid }); + return result.posts; }; - function parsePosts(posts, options, callback) { - async.map(posts, function (post, next) { - async.waterfall([ - function (next) { - if (!post.content || !options.parse) { - post.content = post.content ? validator.escape(String(post.content)) : post.content; - return next(null, post); - } - Posts.parsePost(post, next); - }, - function (post, next) { - if (options.stripTags) { - post.content = stripTags(post.content); - } - next(null, post); - }, - ], next); - }, callback); + async function parsePosts(posts, options) { + async function parse(post) { + if (!post.content || !options.parse) { + post.content = post.content ? validator.escape(String(post.content)) : post.content; + return post; + } + post = await Posts.parsePost(post); + if (options.stripTags) { + post.content = stripTags(post.content); + } + return post; + } + return await Promise.all(posts.map(p => parse(p))); } - function getTopicAndCategories(tids, callback) { - var topicsData; - async.waterfall([ - function (next) { - topics.getTopicsFields(tids, ['uid', 'tid', 'title', 'cid', 'slug', 'deleted', 'postcount', 'mainPid'], next); - }, - function (_topicsData, next) { - topicsData = _topicsData; - var cids = _.uniq(topicsData.map(topic => topic && topic.cid)); - categories.getCategoriesFields(cids, ['cid', 'name', 'icon', 'slug', 'parentCid', 'bgColor', 'color'], next); - }, - function (categoriesData, next) { - next(null, { topics: topicsData, categories: categoriesData }); - }, - ], callback); + async function getTopicAndCategories(tids) { + const topicsData = await topics.getTopicsFields(tids, ['uid', 'tid', 'title', 'cid', 'slug', 'deleted', 'postcount', 'mainPid']); + const cids = _.uniq(topicsData.map(topic => topic && topic.cid)); + const categoriesData = await categories.getCategoriesFields(cids, ['cid', 'name', 'icon', 'slug', 'parentCid', 'bgColor', 'color']); + return { topics: topicsData, categories: categoriesData }; } function toObject(key, data) { diff --git a/src/posts/tools.js b/src/posts/tools.js index cd59129d4c..d184bb1018 100644 --- a/src/posts/tools.js +++ b/src/posts/tools.js @@ -1,72 +1,53 @@ 'use strict'; -var async = require('async'); - -var privileges = require('../privileges'); +const privileges = require('../privileges'); module.exports = function (Posts) { Posts.tools = {}; - Posts.tools.delete = function (uid, pid, callback) { - togglePostDelete(uid, pid, true, callback); + Posts.tools.delete = async function (uid, pid) { + return await togglePostDelete(uid, pid, true); }; - Posts.tools.restore = function (uid, pid, callback) { - togglePostDelete(uid, pid, false, callback); + Posts.tools.restore = async function (uid, pid) { + return await togglePostDelete(uid, pid, false); }; - function togglePostDelete(uid, pid, isDelete, callback) { - async.waterfall([ - function (next) { - Posts.exists(pid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-post]]')); - } - Posts.getPostField(pid, 'deleted', next); - }, - function (deleted, next) { - if (deleted && isDelete) { - return next(new Error('[[error:post-already-deleted]]')); - } else if (!deleted && !isDelete) { - return next(new Error('[[error:post-already-restored]]')); - } + async function togglePostDelete(uid, pid, isDelete) { + const [postData, canDelete] = await Promise.all([ + Posts.getPostData(pid), + privileges.posts.canDelete(pid, uid), + ]); + if (!postData) { + throw new Error('[[error:no-post]]'); + } - privileges.posts.canDelete(pid, uid, next); - }, - function (canDelete, next) { - if (!canDelete.flag) { - return next(new Error(canDelete.message)); - } + if (postData.deleted && isDelete) { + throw new Error('[[error:post-already-deleted]]'); + } else if (!postData.deleted && !isDelete) { + throw new Error('[[error:post-already-restored]]'); + } - if (isDelete) { - require('./cache').del(pid); - Posts.delete(pid, uid, next); - } else { - Posts.restore(pid, uid, function (err, postData) { - if (err) { - return next(err); - } - Posts.parsePost(postData, next); - }); - } - }, - ], callback); + if (!canDelete.flag) { + throw new Error(canDelete.message); + } + let post; + if (isDelete) { + require('./cache').del(pid); + post = await Posts.delete(pid, uid); + } else { + post = await Posts.restore(pid, uid); + post = await Posts.parsePost(post); + } + return post; } - Posts.tools.purge = function (uid, pid, callback) { - async.waterfall([ - function (next) { - privileges.posts.canPurge(pid, uid, next); - }, - function (canPurge, next) { - if (!canPurge) { - return next(new Error('[[error:no-privileges]]')); - } - require('./cache').del(pid); - Posts.purge(pid, uid, next); - }, - ], callback); + Posts.tools.purge = async function (uid, pid) { + const canPurge = await privileges.posts.canPurge(pid, uid); + if (!canPurge) { + throw new Error('[[error:no-privileges]]'); + } + require('./cache').del(pid); + await Posts.purge(pid, uid); }; }; diff --git a/src/posts/uploads.js b/src/posts/uploads.js index ae50b73e55..8142f1b992 100644 --- a/src/posts/uploads.js +++ b/src/posts/uploads.js @@ -5,7 +5,6 @@ var nconf = require('nconf'); var crypto = require('crypto'); var fs = require('fs'); var path = require('path'); -var util = require('util'); var winston = require('winston'); var db = require('../database'); @@ -18,42 +17,33 @@ module.exports = function (Posts) { const pathPrefix = path.join(nconf.get('upload_path'), 'files'); const searchRegex = /\/assets\/uploads\/files\/([^\s")]+\.?[\w]*)/g; - Posts.uploads.sync = function (pid, callback) { - // Scans a post and updates sorted set of uploads + Posts.uploads.sync = async function (pid) { + // Scans a post's content and updates sorted set of uploads - async.parallel({ - content: async.apply(Posts.getPostField, pid, 'content'), - uploads: async.apply(Posts.uploads.list, pid), - }, function (err, data) { - if (err) { - return callback(err); - } + const [content, currentUploads] = await Promise.all([ + Posts.getPostField(pid, 'content'), + Posts.uploads.list(pid), + ]); - // Extract upload file paths from post content - let match = searchRegex.exec(data.content); - const uploads = []; - while (match) { - uploads.push(match[1].replace('-resized', '')); - match = searchRegex.exec(data.content); - } + // Extract upload file paths from post content + let match = searchRegex.exec(content); + const uploads = []; + while (match) { + uploads.push(match[1].replace('-resized', '')); + match = searchRegex.exec(content); + } - // Create add/remove sets - const add = uploads.filter(path => !data.uploads.includes(path)); - const remove = data.uploads.filter(path => !uploads.includes(path)); - - async.parallel([ - async.apply(Posts.uploads.associate, pid, add), - async.apply(Posts.uploads.dissociate, pid, remove), - ], function (err) { - // Strictly return only err - callback(err); - }); - }); + // Create add/remove sets + const add = uploads.filter(path => !currentUploads.includes(path)); + const remove = currentUploads.filter(path => !uploads.includes(path)); + await Promise.all([ + Posts.uploads.associate(pid, add), + Posts.uploads.dissociate(pid, remove), + ]); }; - Posts.uploads.list = function (pid, callback) { - // Returns array of this post's uploads - db.getSortedSetRange('post:' + pid + ':uploads', 0, -1, callback); + Posts.uploads.list = async function (pid) { + return await db.getSortedSetRange('post:' + pid + ':uploads', 0, -1); }; Posts.uploads.listWithSizes = async function (pid) { @@ -66,98 +56,70 @@ module.exports = function (Posts) { })); }; - Posts.uploads.isOrphan = function (filePath, callback) { - // Returns bool indicating whether a file is still CURRENTLY included in any posts - db.sortedSetCard('upload:' + md5(filePath) + ':pids', function (err, length) { - callback(err, length === 0); - }); + Posts.uploads.isOrphan = async function (filePath) { + const length = await db.sortedSetCard('upload:' + md5(filePath) + ':pids'); + return length === 0; }; - Posts.uploads.getUsage = function (filePaths, callback) { + Posts.uploads.getUsage = async function (filePaths) { // Given an array of file names, determines which pids they are used in if (!Array.isArray(filePaths)) { filePaths = [filePaths]; } const keys = filePaths.map(fileObj => 'upload:' + md5(fileObj.name.replace('-resized', '')) + ':pids'); - async.map(keys, function (key, next) { - db.getSortedSetRange(key, 0, -1, next); - }, callback); + return await Promise.all(keys.map(k => db.getSortedSetRange(k, 0, -1))); }; - Posts.uploads.associate = function (pid, filePaths, callback) { + Posts.uploads.associate = async function (pid, filePaths) { // Adds an upload to a post's sorted set of uploads filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths; if (!filePaths.length) { - return setImmediate(callback); + return; } - async.filter(filePaths, function (filePath, next) { + filePaths = await async.filter(filePaths, function (filePath, next) { // Only process files that exist fs.access(path.join(pathPrefix, filePath), fs.constants.F_OK | fs.constants.R_OK, function (err) { next(null, !err); }); - }, function (err, filePaths) { - if (err) { - return callback(err); - } - const now = Date.now(); - const scores = filePaths.map(() => now); - let methods = [async.apply(db.sortedSetAdd.bind(db), 'post:' + pid + ':uploads', scores, filePaths)]; - - methods = methods.concat(filePaths.map(path => async.apply(db.sortedSetAdd.bind(db), 'upload:' + md5(path) + ':pids', now, pid))); - methods = methods.concat(async function () { - await Posts.uploads.saveSize(filePaths); - }); - async.parallel(methods, function (err) { - // Strictly return only err - callback(err); - }); }); + + const now = Date.now(); + const scores = filePaths.map(() => now); + const bulkAdd = filePaths.map(path => ['upload:' + md5(path) + ':pids', now, pid]); + await Promise.all([ + db.sortedSetAdd('post:' + pid + ':uploads', scores, filePaths), + db.sortedSetAddBulk(bulkAdd), + Posts.uploads.saveSize(filePaths), + ]); }; - Posts.uploads.dissociate = function (pid, filePaths, callback) { + Posts.uploads.dissociate = async function (pid, filePaths) { // Removes an upload from a post's sorted set of uploads filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths; if (!filePaths.length) { - return setImmediate(callback); + return; } - let methods = [async.apply(db.sortedSetRemove.bind(db), 'post:' + pid + ':uploads', filePaths)]; - methods = methods.concat(filePaths.map(path => async.apply(db.sortedSetRemove.bind(db), 'upload:' + md5(path) + ':pids', pid))); - async.parallel(methods, function (err) { - // Strictly return only err - callback(err); - }); + const bulkRemove = filePaths.map(path => ['upload:' + md5(path) + ':pids', pid]); + await Promise.all([ + db.sortedSetRemove('post:' + pid + ':uploads', filePaths), + db.sortedSetRemoveBulk(bulkRemove), + ]); }; Posts.uploads.saveSize = async (filePaths) => { - const getSize = util.promisify(image.size); - const sizes = await Promise.all(filePaths.map(async function (fileName) { + await Promise.all(filePaths.map(async function (fileName) { try { - return await getSize(path.join(pathPrefix, fileName)); - } catch (e) { - // Error returned by getSize, do not save size in database - return null; + const size = await image.size(path.join(pathPrefix, fileName)); + winston.verbose('[posts/uploads/' + fileName + '] Saving size'); + await db.setObject('upload:' + md5(fileName), { + width: size.width, + height: size.height, + }); + } catch (err) { + winston.error('[posts/uploads] Error while saving post upload sizes (' + fileName + '): ' + err.message); } })); - - const methods = filePaths.map((filePath, idx) => { - if (!sizes[idx]) { - return null; - } - - winston.verbose('[posts/uploads/' + filePath + '] Saving size'); - return async.apply(db.setObject, 'upload:' + md5(filePath), { - width: sizes[idx].width, - height: sizes[idx].height, - }); - }).filter(Boolean); - async.parallel(methods, function (err) { - if (err) { - winston.error('[posts/uploads] Error while saving post upload sizes: ', err.message); - } else { - winston.verbose('[posts/uploads] Finished saving post upload sizes.'); - } - }); }; }; diff --git a/src/posts/user.js b/src/posts/user.js index eb513e2157..9205bed2f4 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -20,7 +20,56 @@ module.exports = function (Posts) { privileges.global.can('signature', uid), ]); - var groupTitles = _.uniq(_.flatten(userData.map(u => u && u.groupTitleArray))); + const groupsMap = await getGroupsMap(userData); + + userData.forEach(function (userData, index) { + userData.signature = validator.escape(String(userData.signature || '')); + userData.fullname = userSettings[index].showfullname ? validator.escape(String(userData.fullname || '')) : undefined; + userData.selectedGroups = []; + + if (meta.config.hideFullname) { + userData.fullname = undefined; + } + }); + + return await Promise.all(userData.map(async function (userData) { + const [isMemberOfGroups, signature, customProfileInfo] = await Promise.all([ + checkGroupMembership(userData.uid, userData.groupTitleArray), + parseSignature(userData, uid, canUseSignature), + plugins.fireHook('filter:posts.custom_profile_info', { profile: [], uid: userData.uid }), + ]); + + if (isMemberOfGroups && userData.groupTitleArray) { + userData.groupTitleArray.forEach(function (userGroup, index) { + if (isMemberOfGroups[index] && groupsMap[userGroup]) { + userData.selectedGroups.push(groupsMap[userGroup]); + } + }); + } + userData.signature = signature; + userData.custom_profile_info = customProfileInfo.profile; + + return await plugins.fireHook('filter:posts.modifyUserInfo', userData); + })); + }; + + async function checkGroupMembership(uid, groupTitleArray) { + if (!Array.isArray(groupTitleArray) || !groupTitleArray.length) { + return null; + } + return await groups.isMemberOfGroups(uid, groupTitleArray); + } + + async function parseSignature(userData, uid, canUseSignature) { + if (!userData.signature || !canUseSignature || meta.config.disableSignatures) { + return ''; + } + const result = await Posts.parseSignature(userData, uid); + return result.userData.signature; + } + + async function getGroupsMap(userData) { + const groupTitles = _.uniq(_.flatten(userData.map(u => u && u.groupTitleArray))); const groupsMap = {}; const groupsData = await groups.getGroupsData(groupTitles); groupsData.forEach(function (group) { @@ -35,59 +84,8 @@ module.exports = function (Posts) { }; } }); - - userData.forEach(function (userData, index) { - userData.uid = userData.uid || 0; - userData.username = userData.username || '[[global:guest]]'; - userData.userslug = userData.userslug || ''; - userData.reputation = userData.reputation || 0; - userData.postcount = userData.postcount || 0; - userData.banned = userData.banned === 1; - userData.picture = userData.picture || ''; - userData.status = user.getStatus(userData); - userData.signature = validator.escape(String(userData.signature || '')); - userData.fullname = userSettings[index].showfullname ? validator.escape(String(userData.fullname || '')) : undefined; - userData.selectedGroups = []; - - if (meta.config.hideFullname) { - userData.fullname = undefined; - } - }); - - return await async.map(userData, async function (userData) { - const results = await async.parallel({ - isMemberOfGroups: function (next) { - if (!Array.isArray(userData.groupTitleArray) || !userData.groupTitleArray.length) { - return next(); - } - groups.isMemberOfGroups(userData.uid, userData.groupTitleArray, next); - }, - signature: function (next) { - if (!userData.signature || !canUseSignature || meta.config.disableSignatures) { - userData.signature = ''; - return next(); - } - Posts.parseSignature(userData, uid, next); - }, - customProfileInfo: function (next) { - plugins.fireHook('filter:posts.custom_profile_info', { profile: [], uid: userData.uid }, next); - }, - }); - - if (results.isMemberOfGroups && userData.groupTitleArray) { - userData.groupTitleArray.forEach(function (userGroup, index) { - if (results.isMemberOfGroups[index] && groupsMap[userGroup]) { - userData.selectedGroups.push(groupsMap[userGroup]); - } - }); - } - - userData.custom_profile_info = results.customProfileInfo.profile; - - const result = await plugins.fireHook('filter:posts.modifyUserInfo', userData); - return result; - }); - }; + return groupsMap; + } async function getUserData(uids, uid) { const fields = [ diff --git a/test/posts.js b/test/posts.js index 72c49af7fb..673a46c173 100644 --- a/test/posts.js +++ b/test/posts.js @@ -1022,7 +1022,7 @@ describe('Post\'s', function () { it('should not crash if id does not exist', function (done) { socketPosts.reject({ uid: globalModUid }, { id: '123123123' }, function (err) { - assert.ifError(err); + assert.equal(err.message, '[[error:no-privileges]]'); done(); }); });