diff --git a/src/user/posts.js b/src/user/posts.js index 19c3a6b5f5..b444c8a41d 100644 --- a/src/user/posts.js +++ b/src/user/posts.js @@ -1,113 +1,81 @@ 'use strict'; -var async = require('async'); -var db = require('../database'); -var meta = require('../meta'); -var privileges = require('../privileges'); +const db = require('../database'); +const meta = require('../meta'); +const privileges = require('../privileges'); module.exports = function (User) { - User.isReadyToPost = function (uid, cid, callback) { - isReady(uid, cid, 'lastposttime', callback); + User.isReadyToPost = async function (uid, cid) { + await isReady(uid, cid, 'lastposttime'); }; - User.isReadyToQueue = function (uid, cid, callback) { - isReady(uid, cid, 'lastqueuetime', callback); + User.isReadyToQueue = async function (uid, cid) { + await isReady(uid, cid, 'lastqueuetime'); }; - function isReady(uid, cid, field, callback) { + async function isReady(uid, cid, field) { if (parseInt(uid, 10) === 0) { - return callback(); + return; } - async.waterfall([ - function (next) { - async.parallel({ - userData: function (next) { - User.getUserFields(uid, ['uid', 'banned', 'joindate', 'email', 'email:confirmed', 'reputation'].concat([field]), next); - }, - isAdminOrMod: function (next) { - privileges.categories.isAdminOrMod(cid, uid, next); - }, - }, next); - }, - function (results, next) { - if (!results.userData.uid) { - return next(new Error('[[error:no-user]]')); - } + const [userData, isAdminOrMod] = await Promise.all([ + User.getUserFields(uid, ['uid', 'banned', 'joindate', 'email', 'email:confirmed', 'reputation'].concat([field])), + privileges.categories.isAdminOrMod(cid, uid), + ]); - if (results.isAdminOrMod) { - return next(); - } + if (!userData.uid) { + throw new Error('[[error:no-user]]'); + } - var userData = results.userData; + if (isAdminOrMod) { + return; + } - if (userData.banned) { - return next(new Error('[[error:user-banned]]')); - } + if (userData.banned) { + throw new Error('[[error:user-banned]]'); + } - if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { - return next(new Error('[[error:email-not-confirmed]]')); - } + if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { + throw new Error('[[error:email-not-confirmed]]'); + } - var now = Date.now(); - if (now - userData.joindate < meta.config.initialPostDelay * 1000) { - return next(new Error('[[error:user-too-new, ' + meta.config.initialPostDelay + ']]')); - } + var now = Date.now(); + if (now - userData.joindate < meta.config.initialPostDelay * 1000) { + throw new Error('[[error:user-too-new, ' + meta.config.initialPostDelay + ']]'); + } - var lasttime = userData[field] || 0; + var lasttime = userData[field] || 0; - if (meta.config.newbiePostDelay > 0 && meta.config.newbiePostDelayThreshold > userData.reputation && now - lasttime < meta.config.newbiePostDelay * 1000) { - return next(new Error('[[error:too-many-posts-newbie, ' + meta.config.newbiePostDelay + ', ' + meta.config.newbiePostDelayThreshold + ']]')); - } else if (now - lasttime < meta.config.postDelay * 1000) { - return next(new Error('[[error:too-many-posts, ' + meta.config.postDelay + ']]')); - } - - next(); - }, - ], callback); + if (meta.config.newbiePostDelay > 0 && meta.config.newbiePostDelayThreshold > userData.reputation && now - lasttime < meta.config.newbiePostDelay * 1000) { + throw new Error('[[error:too-many-posts-newbie, ' + meta.config.newbiePostDelay + ', ' + meta.config.newbiePostDelayThreshold + ']]'); + } else if (now - lasttime < meta.config.postDelay * 1000) { + throw new Error('[[error:too-many-posts, ' + meta.config.postDelay + ']]'); + } } - User.onNewPostMade = function (postData, callback) { - async.series([ - function (next) { - User.addPostIdToUser(postData, next); - }, - function (next) { - User.incrementUserPostCountBy(postData.uid, 1, next); - }, - function (next) { - User.setUserField(postData.uid, 'lastposttime', postData.timestamp, next); - }, - function (next) { - User.updateLastOnlineTime(postData.uid, next); - }, - ], callback); + User.onNewPostMade = async function (postData) { + await User.addPostIdToUser(postData); + await User.incrementUserPostCountBy(postData.uid, 1); + await User.setUserField(postData.uid, 'lastposttime', postData.timestamp); + await User.updateLastOnlineTime(postData.uid); }; - User.addPostIdToUser = function (postData, callback) { - db.sortedSetsAdd([ + User.addPostIdToUser = async function (postData) { + await db.sortedSetsAdd([ 'uid:' + postData.uid + ':posts', 'cid:' + postData.cid + ':uid:' + postData.uid + ':pids', - ], postData.timestamp, postData.pid, callback); + ], postData.timestamp, postData.pid); }; - User.incrementUserPostCountBy = function (uid, value, callback) { - callback = callback || function () {}; - async.waterfall([ - function (next) { - User.incrementUserFieldBy(uid, 'postcount', value, next); - }, - function (newpostcount, next) { - if (parseInt(uid, 10) <= 0) { - return next(); - } - db.sortedSetAdd('users:postcount', newpostcount, uid, next); - }, - ], callback); + User.incrementUserPostCountBy = async function (uid, value) { + const newpostcount = await User.incrementUserFieldBy(uid, 'postcount', value); + if (parseInt(uid, 10) <= 0) { + return; + } + await db.sortedSetAdd('users:postcount', newpostcount, uid); }; - User.getPostIds = function (uid, start, stop, callback) { - db.getSortedSetRevRange('uid:' + uid + ':posts', start, stop, function (err, pids) { - callback(err, Array.isArray(pids) ? pids : []); - }); + User.getPostIds = async function (uid, start, stop) { + const pids = await db.getSortedSetRevRange('uid:' + uid + ':posts', start, stop); + return Array.isArray(pids) ? pids : []; }; }; diff --git a/src/user/profile.js b/src/user/profile.js index cf621a91a6..3c9c13c812 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -10,350 +10,246 @@ var groups = require('../groups'); var plugins = require('../plugins'); module.exports = function (User) { - User.updateProfile = function (uid, data, callback) { + User.updateProfile = async function (uid, data) { var fields = ['username', 'email', 'fullname', 'website', 'location', 'groupTitle', 'birthday', 'signature', 'aboutme']; var updateUid = data.uid; - var oldData; - async.waterfall([ - function (next) { - plugins.fireHook('filter:user.updateProfile', { uid: uid, data: data, fields: fields }, next); - }, - function (data, next) { - fields = data.fields; - data = data.data; + const result = await plugins.fireHook('filter:user.updateProfile', { uid: uid, data: data, fields: fields }); + fields = result.fields; + data = result.data; - validateData(uid, data, next); - }, - function (next) { - User.getUserFields(updateUid, fields, next); - }, - function (_oldData, next) { - oldData = _oldData; - async.each(fields, function (field, next) { - if (!(data[field] !== undefined && typeof data[field] === 'string')) { - return next(); - } + await validateData(uid, data); - data[field] = data[field].trim(); + const oldData = await User.getUserFields(updateUid, fields); - if (field === 'email') { - return updateEmail(updateUid, data.email, next); - } else if (field === 'username') { - return updateUsername(updateUid, data.username, next); - } else if (field === 'fullname') { - return updateFullname(updateUid, data.fullname, next); - } + await async.each(fields, async function (field) { + if (!(data[field] !== undefined && typeof data[field] === 'string')) { + return; + } - User.setUserField(updateUid, field, data[field], next); - }, next); - }, - function (next) { - plugins.fireHook('action:user.updateProfile', { uid: uid, data: data, fields: fields, oldData: oldData }); - User.getUserFields(updateUid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor'], next); - }, - ], callback); + data[field] = data[field].trim(); + + if (field === 'email') { + return await updateEmail(updateUid, data.email); + } else if (field === 'username') { + return await updateUsername(updateUid, data.username); + } else if (field === 'fullname') { + return await updateFullname(updateUid, data.fullname); + } + + await User.setUserField(updateUid, field, data[field]); + }); + plugins.fireHook('action:user.updateProfile', { uid: uid, data: data, fields: fields, oldData: oldData }); + return await User.getUserFields(updateUid, ['email', 'username', 'userslug', 'picture', 'icon:text', 'icon:bgColor']); }; - function validateData(callerUid, data, callback) { - async.series([ - async.apply(isEmailAvailable, data, data.uid), - async.apply(isUsernameAvailable, data, data.uid), - async.apply(isGroupTitleValid, data), - async.apply(isWebsiteValid, callerUid, data), - async.apply(isAboutMeValid, callerUid, data), - async.apply(isSignatureValid, callerUid, data), - ], function (err) { - callback(err); - }); + async function validateData(callerUid, data) { + await isEmailAvailable(data, data.uid); + await isUsernameAvailable(data, data.uid); + await isWebsiteValid(callerUid, data); + await isAboutMeValid(callerUid, data); + await isSignatureValid(callerUid, data); + isGroupTitleValid(data); } - function isEmailAvailable(data, uid, callback) { + async function isEmailAvailable(data, uid) { if (!data.email) { - return callback(); + return; } if (!utils.isEmailValid(data.email)) { - return callback(new Error('[[error:invalid-email]]')); + throw new Error('[[error:invalid-email]]'); + } + const email = await User.getUserField(uid, 'email'); + if (email === data.email) { + return; + } + const available = await User.email.available(data.email); + if (!available) { + throw new Error('[[error:email-taken]]'); } - - async.waterfall([ - function (next) { - User.getUserField(uid, 'email', next); - }, - function (email, next) { - if (email === data.email) { - return callback(); - } - User.email.available(data.email, next); - }, - function (available, next) { - next(!available ? new Error('[[error:email-taken]]') : null); - }, - ], callback); } - function isUsernameAvailable(data, uid, callback) { + async function isUsernameAvailable(data, uid) { if (!data.username) { - return callback(); + return; } data.username = data.username.trim(); - async.waterfall([ - function (next) { - User.getUserFields(uid, ['username', 'userslug'], next); - }, - function (userData, next) { - var userslug = utils.slugify(data.username); + const userData = await User.getUserFields(uid, ['username', 'userslug']); + var userslug = utils.slugify(data.username); - if (data.username.length < meta.config.minimumUsernameLength) { - return next(new Error('[[error:username-too-short]]')); - } + if (data.username.length < meta.config.minimumUsernameLength) { + throw new Error('[[error:username-too-short]]'); + } - if (data.username.length > meta.config.maximumUsernameLength) { - return next(new Error('[[error:username-too-long]]')); - } + if (data.username.length > meta.config.maximumUsernameLength) { + throw new Error('[[error:username-too-long]]'); + } - if (!utils.isUserNameValid(data.username) || !userslug) { - return next(new Error('[[error:invalid-username]]')); - } + if (!utils.isUserNameValid(data.username) || !userslug) { + throw new Error('[[error:invalid-username]]'); + } - if (userslug === userData.userslug) { - return callback(); - } - User.existsBySlug(userslug, next); - }, - function (exists, next) { - next(exists ? new Error('[[error:username-taken]]') : null); - }, - ], callback); + if (userslug === userData.userslug) { + return; + } + const exists = await User.existsBySlug(userslug); + if (exists) { + throw new Error('[[error:username-taken]]'); + } } - function isGroupTitleValid(data, callback) { + function isGroupTitleValid(data) { if (data.groupTitle === 'registered-users' || groups.isPrivilegeGroup(data.groupTitle)) { - callback(new Error('[[error:invalid-group-title]]')); - } else { - callback(); + throw new Error('[[error:invalid-group-title]]'); } } - function isWebsiteValid(callerUid, data, callback) { + async function isWebsiteValid(callerUid, data) { if (!data.website) { - return setImmediate(callback); + return; } - User.checkMinReputation(callerUid, data.uid, 'min:rep:website', callback); + await User.checkMinReputation(callerUid, data.uid, 'min:rep:website'); } - function isAboutMeValid(callerUid, data, callback) { + async function isAboutMeValid(callerUid, data) { if (!data.aboutme) { - return setImmediate(callback); + return; } if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) { - return callback(new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]')); + throw new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]'); } - User.checkMinReputation(callerUid, data.uid, 'min:rep:aboutme', callback); + await User.checkMinReputation(callerUid, data.uid, 'min:rep:aboutme'); } - function isSignatureValid(callerUid, data, callback) { + async function isSignatureValid(callerUid, data) { if (!data.signature) { - return setImmediate(callback); + return; } if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) { - return callback(new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]')); + throw new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]'); } - User.checkMinReputation(callerUid, data.uid, 'min:rep:signature', callback); + await User.checkMinReputation(callerUid, data.uid, 'min:rep:signature'); } - User.checkMinReputation = function (callerUid, uid, setting, callback) { + User.checkMinReputation = async function (callerUid, uid, setting) { var isSelf = parseInt(callerUid, 10) === parseInt(uid, 10); if (!isSelf || meta.config['reputation:disabled']) { - return setImmediate(callback); + return; + } + const reputation = await User.getUserField(uid, 'reputation'); + if (reputation < meta.config[setting]) { + throw new Error('[[error:not-enough-reputation-' + setting.replace(/:/g, '-') + ']]'); } - async.waterfall([ - function (next) { - User.getUserField(uid, 'reputation', next); - }, - function (reputation, next) { - if (reputation < meta.config[setting]) { - return next(new Error('[[error:not-enough-reputation-' + setting.replace(/:/g, '-') + ']]')); - } - next(); - }, - ], callback); }; - function updateEmail(uid, newEmail, callback) { - async.waterfall([ - function (next) { - User.getUserField(uid, 'email', next); - }, - function (oldEmail, next) { - oldEmail = oldEmail || ''; + async function updateEmail(uid, newEmail) { + let oldEmail = await User.getUserField(uid, 'email'); + oldEmail = oldEmail || ''; + if (oldEmail === newEmail) { + return; + } - if (oldEmail === newEmail) { - return callback(); - } - async.series([ - async.apply(db.sortedSetRemove, 'email:uid', oldEmail.toLowerCase()), - async.apply(db.sortedSetRemove, 'email:sorted', oldEmail.toLowerCase() + ':' + uid), - async.apply(User.auth.revokeAllSessions, uid), - ], function (err) { - next(err); - }); - }, - function (next) { - async.parallel([ - function (next) { - db.sortedSetAddBulk([ - ['email:uid', uid, newEmail.toLowerCase()], - ['email:sorted', 0, newEmail.toLowerCase() + ':' + uid], - ['user:' + uid + ':emails', Date.now(), newEmail + ':' + Date.now()], - ['users:notvalidated', Date.now(), uid], - ], next); - }, - function (next) { - User.setUserField(uid, 'email', newEmail, next); - }, - function (next) { - if (meta.config.requireEmailConfirmation && newEmail) { - User.email.sendValidationEmail(uid, { - email: newEmail, - subject: '[[email:email.verify-your-email.subject]]', - template: 'verify_email', - }); - } - User.setUserField(uid, 'email:confirmed', 0, next); - }, - function (next) { - User.reset.cleanByUid(uid, next); - }, - ], function (err) { - next(err); - }); - }, - ], callback); + await db.sortedSetRemove('email:uid', oldEmail.toLowerCase()); + await db.sortedSetRemove('email:sorted', oldEmail.toLowerCase() + ':' + uid); + await User.auth.revokeAllSessions(uid); + + await Promise.all([ + db.sortedSetAddBulk([ + ['email:uid', uid, newEmail.toLowerCase()], + ['email:sorted', 0, newEmail.toLowerCase() + ':' + uid], + ['user:' + uid + ':emails', Date.now(), newEmail + ':' + Date.now()], + ['users:notvalidated', Date.now(), uid], + ]), + User.setUserFields(uid, { email: newEmail, 'email:confirmed': 0 }), + User.reset.cleanByUid(uid), + ]); + + if (meta.config.requireEmailConfirmation && newEmail) { + User.email.sendValidationEmail(uid, { + email: newEmail, + subject: '[[email:email.verify-your-email.subject]]', + template: 'verify_email', + }); + } } - function updateUsername(uid, newUsername, callback) { + async function updateUsername(uid, newUsername) { if (!newUsername) { - return setImmediate(callback); + return; } - - async.waterfall([ - function (next) { - User.getUserFields(uid, ['username', 'userslug'], next); - }, - function (userData, next) { - if (userData.username === newUsername) { - return callback(); - } - async.parallel([ - function (next) { - updateUidMapping('username', uid, newUsername, userData.username, next); - }, - function (next) { - var newUserslug = utils.slugify(newUsername); - updateUidMapping('userslug', uid, newUserslug, userData.userslug, next); - }, - function (next) { - var now = Date.now(); - async.series([ - async.apply(db.sortedSetRemove, 'username:sorted', userData.username.toLowerCase() + ':' + uid), - async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid), - async.apply(db.sortedSetAdd, 'user:' + uid + ':usernames', now, newUsername + ':' + now), - ], next); - }, - ], next); - }, - ], function (err) { - callback(err); - }); + const userData = await User.getUserFields(uid, ['username', 'userslug']); + if (userData.username === newUsername) { + return; + } + const newUserslug = utils.slugify(newUsername); + const now = Date.now(); + await Promise.all([ + updateUidMapping('username', uid, newUsername, userData.username), + updateUidMapping('userslug', uid, newUserslug, userData.userslug), + db.sortedSetAdd('user:' + uid + ':usernames', now, newUsername + ':' + now), + ]); + await db.sortedSetRemove('username:sorted', userData.username.toLowerCase() + ':' + uid); + await db.sortedSetAdd('username:sorted', 0, newUsername.toLowerCase() + ':' + uid); } - function updateUidMapping(field, uid, value, oldValue, callback) { + async function updateUidMapping(field, uid, value, oldValue) { if (value === oldValue) { - return callback(); + return; + } + await db.sortedSetRemove(field + ':uid', oldValue); + await User.setUserField(uid, field, value); + if (value) { + await db.sortedSetAdd(field + ':uid', uid, value); } - - async.series([ - function (next) { - db.sortedSetRemove(field + ':uid', oldValue, next); - }, - function (next) { - User.setUserField(uid, field, value, next); - }, - function (next) { - if (value) { - db.sortedSetAdd(field + ':uid', uid, value, next); - } else { - next(); - } - }, - ], callback); } - function updateFullname(uid, newFullname, callback) { - async.waterfall([ - function (next) { - User.getUserField(uid, 'fullname', next); - }, - function (fullname, next) { - updateUidMapping('fullname', uid, newFullname, fullname, next); - }, - ], callback); + async function updateFullname(uid, newFullname) { + const fullname = await User.getUserField(uid, 'fullname'); + await updateUidMapping('fullname', uid, newFullname, fullname); } - User.changePassword = function (uid, data, callback) { + User.changePassword = async function (uid, data) { if (uid <= 0 || !data || !data.uid) { - return callback(new Error('[[error:invalid-uid]]')); + throw new Error('[[error:invalid-uid]]'); + } + await User.isPasswordValid(data.newPassword); + const [isAdmin, hasPassword] = await Promise.all([ + User.isAdministrator(uid), + User.hasPassword(uid), + ]); + + if (meta.config['password:disableEdit'] && !isAdmin) { + throw new Error('[[error:no-privileges]]'); + } + let isAdminOrPasswordMatch = false; + const isSelf = parseInt(uid, 10) === parseInt(data.uid, 10); + if ( + (isAdmin && !isSelf) || // Admins ok + (!hasPassword && isSelf) // Initial password set ok + ) { + isAdminOrPasswordMatch = true; + } else { + isAdminOrPasswordMatch = await User.isPasswordCorrect(uid, data.currentPassword, data.ip); } - async.waterfall([ - function (next) { - User.isPasswordValid(data.newPassword, next); - }, - function (next) { - async.parallel({ - isAdmin: async.apply(User.isAdministrator, uid), - hasPassword: async.apply(User.hasPassword, uid), - }, next); - }, - function (checks, next) { - if (meta.config['password:disableEdit'] && !checks.isAdmin) { - return next(new Error('[[error:no-privileges]]')); - } + if (!isAdminOrPasswordMatch) { + throw new Error('[[user:change_password_error_wrong_current]]'); + } - if ( - (checks.isAdmin && parseInt(uid, 10) !== parseInt(data.uid, 10)) || // Admins ok - (!checks.hasPassword && parseInt(uid, 10) === parseInt(data.uid, 10)) // Initial password set ok - ) { - return next(null, true); - } + const hashedPassword = await User.hashPassword(data.newPassword); + await Promise.all([ + User.setUserFields(data.uid, { + password: hashedPassword, + rss_token: utils.generateUUID(), + }), + User.reset.updateExpiry(data.uid), + User.auth.revokeAllSessions(data.uid), + ]); - User.isPasswordCorrect(uid, data.currentPassword, data.ip, next); - }, - function (isAdminOrPasswordMatch, next) { - if (!isAdminOrPasswordMatch) { - return next(new Error('[[user:change_password_error_wrong_current]]')); - } - - User.hashPassword(data.newPassword, next); - }, - function (hashedPassword, next) { - async.parallel([ - async.apply(User.setUserFields, data.uid, { - password: hashedPassword, - rss_token: utils.generateUUID(), - }), - async.apply(User.reset.updateExpiry, data.uid), - async.apply(User.auth.revokeAllSessions, data.uid), - async.apply(plugins.fireHook, 'action:password.change', { uid: uid }), - ], function (err) { - next(err); - }); - }, - ], callback); + plugins.fireHook('action:password.change', { uid: uid }); }; };