'use strict'; const async = require('async'); const validator = require('validator'); const utils = require('../utils'); const meta = require('../meta'); const db = require('../database'); const groups = require('../groups'); const plugins = require('../plugins'); module.exports = function (User) { User.updateProfile = async function (uid, data) { let fields = [ 'username', 'email', 'fullname', 'website', 'location', 'groupTitle', 'birthday', 'signature', 'aboutme', ]; if (!data.uid) { throw new Error('[[error:invalid-update-uid]]'); } const updateUid = data.uid; const result = await plugins.fireHook('filter:user.updateProfile', { uid: uid, data: data, fields: fields }); fields = result.fields; data = result.data; await validateData(uid, data); const oldData = await User.getUserFields(updateUid, fields); await async.each(fields, async function (field) { if (!(data[field] !== undefined && typeof data[field] === 'string')) { return; } 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']); }; 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); isFullnameValid(data); isLocationValid(data); isBirthdayValid(data); isGroupTitleValid(data); } async function isEmailAvailable(data, uid) { if (!data.email) { return; } if (!utils.isEmailValid(data.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 function isUsernameAvailable(data, uid) { if (!data.username) { return; } data.username = data.username.trim(); const userData = await User.getUserFields(uid, ['username', 'userslug']); var userslug = utils.slugify(data.username); if (data.username.length < meta.config.minimumUsernameLength) { throw new Error('[[error:username-too-short]]'); } if (data.username.length > meta.config.maximumUsernameLength) { throw new Error('[[error:username-too-long]]'); } if (!utils.isUserNameValid(data.username) || !userslug) { throw new Error('[[error:invalid-username]]'); } if (userslug === userData.userslug) { return; } const exists = await User.existsBySlug(userslug); if (exists) { throw new Error('[[error:username-taken]]'); } } async function isWebsiteValid(callerUid, data) { if (!data.website) { return; } if (data.website.length > 255) { throw new Error('[[error:invalid-website]]'); } await User.checkMinReputation(callerUid, data.uid, 'min:rep:website'); } async function isAboutMeValid(callerUid, data) { if (!data.aboutme) { return; } if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) { throw new Error('[[error:about-me-too-long, ' + meta.config.maximumAboutMeLength + ']]'); } await User.checkMinReputation(callerUid, data.uid, 'min:rep:aboutme'); } async function isSignatureValid(callerUid, data) { if (!data.signature) { return; } if (data.signature !== undefined && data.signature.length > meta.config.maximumSignatureLength) { throw new Error('[[error:signature-too-long, ' + meta.config.maximumSignatureLength + ']]'); } await User.checkMinReputation(callerUid, data.uid, 'min:rep:signature'); } function isFullnameValid(data) { if (data.fullname && (validator.isURL(data.fullname) || data.fullname.length > 255)) { throw new Error('[[error:invalid-fullname]]'); } } function isLocationValid(data) { if (data.location && (validator.isURL(data.location) || data.location.length > 255)) { throw new Error('[[error:invalid-location]]'); } } function isBirthdayValid(data) { if (!data.birthday) { return; } const result = new Date(data.birthday); if (result && result.toString() === 'Invalid Date') { throw new Error('[[error:invalid-birthday]]'); } } function isGroupTitleValid(data) { function checkTitle(title) { if (title === 'registered-users' || groups.isPrivilegeGroup(title)) { throw new Error('[[error:invalid-group-title]]'); } } if (!data.groupTitle) { return; } let groupTitles = []; if (validator.isJSON(data.groupTitle)) { groupTitles = JSON.parse(data.groupTitle); if (!Array.isArray(groupTitles)) { throw new Error('[[error:invalid-group-title]]'); } groupTitles.forEach(title => checkTitle(title)); } else { groupTitles = [data.groupTitle]; checkTitle(data.groupTitle); } if (!meta.config.allowMultipleBadges && groupTitles.length > 1) { data.groupTitle = JSON.stringify(groupTitles[0]); } } User.checkMinReputation = async function (callerUid, uid, setting) { const isSelf = parseInt(callerUid, 10) === parseInt(uid, 10); if (!isSelf || meta.config['reputation:disabled']) { return; } const reputation = await User.getUserField(uid, 'reputation'); if (reputation < meta.config[setting]) { throw new Error('[[error:not-enough-reputation-' + setting.replace(/:/g, '-') + ']]'); } }; async function updateEmail(uid, newEmail) { let oldEmail = await User.getUserField(uid, 'email'); oldEmail = oldEmail || ''; if (oldEmail === newEmail) { return; } 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) { await User.email.sendValidationEmail(uid, { email: newEmail, subject: '[[email:email.verify-your-email.subject]]', template: 'verify_email', }); } } async function updateUsername(uid, newUsername) { if (!newUsername) { return; } 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); } async function updateUidMapping(field, uid, value, oldValue) { if (value === oldValue) { return; } await db.sortedSetRemove(field + ':uid', oldValue); await User.setUserField(uid, field, value); if (value) { await db.sortedSetAdd(field + ':uid', uid, value); } } async function updateFullname(uid, newFullname) { const fullname = await User.getUserField(uid, 'fullname'); await updateUidMapping('fullname', uid, newFullname, fullname); } User.changePassword = async function (uid, data) { if (uid <= 0 || !data || !data.uid) { throw new Error('[[error:invalid-uid]]'); } 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); } if (!isAdminOrPasswordMatch) { throw new Error('[[user:change_password_error_wrong_current]]'); } 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), ]); plugins.fireHook('action:password.change', { uid: uid, targetUid: data.uid }); }; };