Files
NodeBB/src/controllers/accounts/helpers.js

383 lines
13 KiB
JavaScript

'use strict';
const validator = require('validator');
const nconf = require('nconf');
const db = require('../../database');
const user = require('../../user');
const groups = require('../../groups');
const plugins = require('../../plugins');
const meta = require('../../meta');
const utils = require('../../utils');
const privileges = require('../../privileges');
const translator = require('../../translator');
const messaging = require('../../messaging');
const categories = require('../../categories');
const posts = require('../../posts');
const activitypub = require('../../activitypub');
const flags = require('../../flags');
const slugify = require('../../slugify');
const relative_path = nconf.get('relative_path');
const helpers = module.exports;
helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {}) {
const uid = await user.getUidByUserslug(userslug);
if (!uid) {
return null;
}
const [results, canFlag, flagged, flagId] = await Promise.all([
getAllData(uid, callerUID),
privileges.users.canFlag(callerUID, uid),
flags.exists('user', uid, callerUID),
flags.getFlagIdByTarget('user', uid),
]);
if (!results.userData) {
throw new Error('[[error:invalid-uid]]');
}
await parseAboutMe(results.userData);
let { userData } = results;
const { userSettings, isAdmin, isGlobalModerator, isModerator, canViewInfo } = results;
const isSelf = parseInt(callerUID, 10) === parseInt(userData.uid, 10);
if (meta.config['reputation:disabled']) {
delete userData.reputation;
}
userData.age = Math.max(
0,
userData.birthday ? Math.floor((new Date().getTime() - new Date(userData.birthday).getTime()) / 31536000000) : 0
);
userData = await user.hidePrivateData(userData, callerUID);
userData.emailHidden = !userSettings.showemail;
userData.emailClass = userSettings.showemail ? 'hide' : '';
// If email unconfirmed, hide from result set
if (!userData['email:confirmed']) {
userData.email = '';
}
if (isAdmin || isSelf || (canViewInfo && !results.isTargetAdmin)) {
userData.ips = results.ips;
}
if (!isAdmin && !isGlobalModerator && !isModerator) {
userData.moderationNote = undefined;
}
userData.isBlocked = results.isBlocked;
userData.yourid = callerUID;
userData.theirid = userData.uid;
userData.isTargetAdmin = results.isTargetAdmin;
userData.isAdmin = isAdmin;
userData.isGlobalModerator = isGlobalModerator;
userData.isModerator = isModerator;
userData.isAdminOrGlobalModerator = isAdmin || isGlobalModerator;
userData.isAdminOrGlobalModeratorOrModerator = isAdmin || isGlobalModerator || isModerator;
userData.isSelfOrAdminOrGlobalModerator = isSelf || isAdmin || isGlobalModerator;
userData.canEdit = results.canEdit;
userData.canBan = results.canBanUser;
userData.canMute = results.canMuteUser;
userData.canFlag = canFlag.flag;
userData.flagId = flagged ? flagId : null;
userData.canChangePassword = isAdmin || (isSelf && !meta.config['password:disableEdit']);
userData.isSelf = isSelf;
userData.isFollowing = results.isFollowing;
userData.isFollowPending = results.isFollowPending;
userData.canChat = results.canChat;
userData.hasPrivateChat = results.hasPrivateChat;
userData.iconBackgrounds = results.iconBackgrounds;
userData.showHidden = results.canEdit; // remove in v1.19.0
userData.allowProfilePicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:profile-picture'];
userData.allowCoverPicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:cover-picture'];
userData.allowProfileImageUploads = meta.config.allowProfileImageUploads;
userData.allowedProfileImageExtensions = user.getAllowedProfileImageExtensions().map(ext => `.${ext}`).join(', ');
userData.groups = Array.isArray(results.groups) && results.groups.length ? results.groups[0] : [];
userData.selectedGroup = userData.groups.filter(group => group && userData.groupTitleArray.includes(group.name))
.sort((a, b) => userData.groupTitleArray.indexOf(a.name) - userData.groupTitleArray.indexOf(b.name));
userData.disableSignatures = meta.config.disableSignatures === 1;
userData['reputation:disabled'] = meta.config['reputation:disabled'] === 1;
userData['downvote:disabled'] = meta.config['downvote:disabled'] === 1;
userData['email:confirmed'] = !!userData['email:confirmed'];
userData.profile_links = filterLinks(results.profile_menu.links, {
self: isSelf,
other: !isSelf,
moderator: isModerator,
globalMod: isGlobalModerator,
admin: isAdmin,
canViewInfo: canViewInfo,
});
userData.banned = Boolean(userData.banned);
userData.muted = parseInt(userData.mutedUntil, 10) > Date.now();
userData.fullname = escape(userData.fullname);
userData.signature = escape(userData.signature);
userData.birthday = validator.escape(String(userData.birthday || ''));
userData.moderationNote = validator.escape(String(userData.moderationNote || ''));
if (userData['cover:url']) {
userData['cover:url'] = userData['cover:url'].startsWith('http') ? userData['cover:url'] : (nconf.get('relative_path') + userData['cover:url']);
} else {
userData['cover:url'] = require('../../coverPhoto').getDefaultProfileCover(userData.uid);
}
userData['cover:position'] = validator.escape(String(userData['cover:position'] || '50% 50%'));
userData['username:disableEdit'] = !userData.isAdmin && meta.config['username:disableEdit'];
userData['email:disableEdit'] = !userData.isAdmin && meta.config['email:disableEdit'];
await getCounts(userData, callerUID);
const hookData = await plugins.hooks.fire('filter:helpers.getUserDataByUserSlug', {
userData: userData,
callerUID: callerUID,
query: query,
});
return hookData.userData;
};
helpers.getCustomUserFields = async function (callerUID, userData) {
// Remote users' fields are serialized in hash
if (!utils.isNumber(userData.uid)) {
const customFields = await user.getUserField(userData.uid, 'customFields');
const fields = Array
.from(new URLSearchParams(customFields))
.reduce((memo, [name, value]) => {
const isUrl = validator.isURL(value);
memo.push({
key: slugify(name),
name,
value,
linkValue: validator.escape(String(value.replace('http://', '').replace('https://', ''))),
type: isUrl ? 'input-link' : 'input-text',
'min-rep': '',
icon: 'fa-solid fa-circle-info',
});
return memo;
}, []);
return fields;
}
const keys = await db.getSortedSetRange('user-custom-fields', 0, -1);
const allFields = (await db.getObjects(keys.map(k => `user-custom-field:${k}`))).filter(Boolean);
const isSelf = String(callerUID) === String(userData.uid);
const [isAdmin, isModOfAny] = await Promise.all([
privileges.users.isAdministrator(callerUID),
user.isModeratorOfAnyCategory(callerUID),
]);
const fields = allFields.filter((field) => {
const visibility = field.visibility || 'all';
const visibilityCheck = isAdmin || isModOfAny || isSelf || visibility === 'all' ||
(
visibility === 'loggedin' &&
String(callerUID) !== '0' &&
String(callerUID) !== '-1'
);
const minRep = field['min:rep'] || 0;
const repCheck = userData.reputation >= minRep || meta.config['reputation:disabled'];
return visibilityCheck && repCheck;
});
fields.forEach((f) => {
let userValue = userData[f.key];
if (f.type === 'select-multi' && userValue) {
userValue = JSON.parse(userValue || '[]');
}
if (f.type === 'input-link' && userValue) {
f.linkValue = validator.escape(String(userValue.replace('http://', '').replace('https://', '')));
}
f['select-options'] = f['select-options'].split('\n').filter(Boolean).map(
opt => ({
value: opt,
selected: Array.isArray(userValue) ?
userValue.includes(opt) :
opt === userValue,
})
);
if (userValue) {
if (Array.isArray(userValue)) {
userValue = userValue.join(', ');
}
f.value = validator.escape(String(userValue));
}
});
return fields;
};
function escape(value) {
return translator.escape(validator.escape(String(value || '')));
}
async function getAllData(uid, callerUID) {
// loading these before caches them, so the big promiseParallel doesn't make extra db calls
const [[isTargetAdmin, isCallerAdmin], isGlobalModerator] = await Promise.all([
user.isAdministrator([uid, callerUID]),
user.isGlobalModerator(callerUID),
]);
return await utils.promiseParallel({
userData: user.getUserData(uid),
isTargetAdmin: isTargetAdmin,
userSettings: user.getSettings(uid),
isAdmin: isCallerAdmin,
isGlobalModerator: isGlobalModerator,
isModerator: user.isModeratorOfAnyCategory(callerUID),
isFollowing: user.isFollowing(callerUID, uid),
isFollowPending: user.isFollowPending(callerUID, uid),
ips: user.getIPs(uid, 4),
profile_menu: getProfileMenu(uid, callerUID),
groups: groups.getUserGroups([uid]),
canEdit: privileges.users.canEdit(callerUID, uid),
canBanUser: privileges.users.canBanUser(callerUID, uid),
canMuteUser: privileges.users.canMuteUser(callerUID, uid),
isBlocked: user.blocks.is(uid, callerUID),
canViewInfo: privileges.global.can('view:users:info', callerUID),
canChat: canChat(callerUID, uid),
hasPrivateChat: messaging.hasPrivateChat(callerUID, uid),
iconBackgrounds: user.getIconBackgrounds(),
});
}
async function canChat(callerUID, uid) {
try {
await messaging.canMessageUser(callerUID, uid);
} catch (err) {
if (err.message.startsWith('[[error:')) {
return false;
}
throw err;
}
return true;
}
async function getCounts(userData, callerUID) {
const { uid } = userData;
const isRemote = activitypub.helpers.isUri(uid);
const cids = await categories.getCidsByPrivilege('categories:cid', callerUID, 'topics:read');
const promises = {
posts: db.sortedSetsCardSum(cids.map(c => `cid:${c}:uid:${uid}:pids`)),
topics: db.sortedSetsCardSum(cids.map(c => `cid:${c}:uid:${uid}:tids`)),
};
if (userData.isAdmin || userData.isSelf) {
promises.uploaded = db.sortedSetCard(`uid:${uid}:uploads`);
promises.categoriesWatched = user.getWatchedCategories(uid);
promises.tagsWatched = db.sortedSetCard(`uid:${uid}:followed_tags`);
promises.blocks = user.getUserField(userData.uid, 'blocksCount');
}
const counts = await utils.promiseParallel(promises);
counts.posts = isRemote ? userData.postcount : counts.posts;
counts.categoriesWatched = counts.categoriesWatched && counts.categoriesWatched.length;
counts.groups = userData.groups.length;
counts.following = userData.followingCount;
counts.followers = userData.followerCount;
userData.blocksCount = counts.blocks || 0; // for backwards compatibility, remove in 1.16.0
userData.counts = counts;
}
async function getProfileMenu(uid, callerUID) {
const links = [{
id: 'info',
route: 'info',
name: '[[user:account-info]]',
icon: 'fa-info',
visibility: {
self: false,
other: false,
moderator: false,
globalMod: false,
admin: true,
canViewInfo: true,
},
}, {
id: 'sessions',
route: 'sessions',
name: '[[pages:account/sessions]]',
icon: 'fa-group',
visibility: {
self: true,
other: false,
moderator: false,
globalMod: false,
admin: false,
canViewInfo: false,
},
}];
if (meta.config.gdpr_enabled) {
links.push({
id: 'consent',
route: 'consent',
name: '[[user:consent.title]]',
icon: 'fa-thumbs-o-up',
visibility: {
self: true,
other: false,
moderator: false,
globalMod: false,
admin: false,
canViewInfo: false,
},
});
}
const data = await plugins.hooks.fire('filter:user.profileMenu', {
uid: uid,
callerUID: callerUID,
links: links,
});
const userslug = await user.getUserField(uid, 'userslug');
data.links.forEach((link) => {
if (!link.hasOwnProperty('url')) {
link.url = `${relative_path}/user/${userslug}/${link.route}`;
}
});
return data;
}
async function parseAboutMe(userData) {
if (!userData.aboutme) {
userData.aboutme = '';
userData.aboutmeParsed = '';
return;
} else if (activitypub.helpers.isUri(userData.uid)) {
userData.aboutme = posts.sanitize(userData.aboutme);
userData.aboutmeParsed = userData.aboutme;
return;
}
userData.aboutme = validator.escape(String(userData.aboutme || ''));
const parsed = await plugins.hooks.fire('filter:parse.aboutme', userData.aboutme);
userData.aboutme = translator.escape(userData.aboutme);
userData.aboutmeParsed = translator.escape(parsed);
}
function filterLinks(links, states) {
return links.filter((link, index) => {
// Default visibility
link.visibility = {
self: true,
other: true,
moderator: true,
globalMod: true,
admin: true,
canViewInfo: true,
...link.visibility,
};
const permit = Object.keys(states).some(state => states[state] && link.visibility[state]);
links[index].public = permit;
return permit;
});
}
require('../../promisify')(helpers);