import { api } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; import { Users } from '@rocket.chat/models'; import type { Response } from '@rocket.chat/server-fetch'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Meteor } from 'meteor/meteor'; import type { ClientSession } from 'mongodb'; import { isRenderableImageType } from '../../../lib/renderableImageTypes'; import { onceTransactionCommitedSuccessfully } from '../../database/utils'; import { settings } from '../../settings'; import { hasPermissionAsync } from '../authorization/hasPermission'; import { SystemLogger } from '../logger/system'; import { RocketChatFile } from '../media/file'; import { FileUpload } from '../media/file-upload'; export const setAvatarFromServiceWithValidation = async ( userId: string, dataURI: string, contentType?: string, service?: string, targetUserId?: string, ): Promise => { if (!dataURI) { throw new Meteor.Error('error-invalid-data', 'Invalid dataURI', { method: 'setAvatarFromService', }); } if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setAvatarFromService', }); } if (!settings.get('Accounts_AllowUserAvatarChange')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setAvatarFromService', }); } let user: Pick | null; if (targetUserId && targetUserId !== userId) { if (!(await hasPermissionAsync(userId, 'edit-other-user-avatar'))) { throw new Meteor.Error('error-unauthorized', 'Unauthorized', { method: 'setAvatarFromService', }); } user = await Users.findOneById(targetUserId, { projection: { _id: 1, username: 1 } }); } else { user = await Users.findOneById(userId, { projection: { _id: 1, username: 1 } }); } if (!user) { throw new Meteor.Error('error-invalid-desired-user', 'Invalid desired user', { method: 'setAvatarFromService', }); } return setUserAvatar(user, dataURI, contentType, service); }; export function setUserAvatar( user: Pick, dataURI: Buffer, contentType: string, service: 'rest', etag?: string, updater?: Updater, session?: ClientSession, ): Promise; export function setUserAvatar( user: Pick, dataURI: string, contentType?: string, service?: 'initials' | 'url' | 'rest' | string, etag?: string, updater?: Updater, session?: ClientSession, ): Promise; export async function setUserAvatar( user: Pick, dataURI: string | Buffer, contentType: string | undefined, service?: 'initials' | 'url' | 'rest' | string, etag?: string, updater?: Updater, session?: ClientSession, ): Promise { if (service === 'initials') { if (updater) { updater.set('avatarOrigin', service); } else { await Users.setAvatarData(user._id, service, null, { session }); } return; } const { buffer, type } = await (async (): Promise<{ buffer: Buffer; type: string }> => { if (service === 'url' && typeof dataURI === 'string') { let response: Response; try { response = await fetch(dataURI, { ignoreSsrfValidation: false, allowList: settings.get('SSRF_Allowlist'), }); } catch (e) { SystemLogger.info({ msg: 'Not a valid response from the avatar url', url: dataURI, err: e, }); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'setUserAvatar', url: dataURI, }); } if (response.status !== 200) { if (response.status !== 404) { SystemLogger.info({ msg: 'Error while handling the setting of the avatar from a url', url: dataURI, username: user.username, status: response.status, }); throw new Meteor.Error( 'error-avatar-url-handling', `Error while handling avatar setting from a URL (${dataURI}) for ${user.username}`, { function: 'RocketChat.setUserAvatar', url: dataURI, username: user.username }, ); } SystemLogger.info({ msg: 'Not a valid response from the avatar url', status: response.status, url: dataURI, }); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'setUserAvatar', url: dataURI, }); } const contentType = response.headers.get('content-type'); if (!isRenderableImageType(contentType)) { SystemLogger.info({ msg: 'Not a valid content-type from the provided avatar url', contentType, url: dataURI, }); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'setUserAvatar', url: dataURI, }); } return { buffer: Buffer.from(await response.arrayBuffer()), type: response.headers.get('content-type') || '', }; } if (service === 'rest') { if (!contentType) { throw new Meteor.Error('error-avatar-invalid-content-type', 'Invalid avatar content type', { function: 'setUserAvatar', }); } return { buffer: typeof dataURI === 'string' ? Buffer.from(dataURI, 'binary') : dataURI, type: contentType, }; } const fileData = RocketChatFile.dataURIParse(dataURI); return { buffer: Buffer.from(fileData.image, 'base64'), type: fileData.contentType, }; })(); const fileStore = FileUpload.getStore('Avatars'); user.username && (await fileStore.deleteByName(user.username, { session })); const file = { userId: user._id, type, size: buffer.length, }; const result = await fileStore.insert(file, buffer, { session }); const avatarETag = etag || result?.etag || ''; if (service) { if (updater) { updater.set('avatarOrigin', service); updater.set('avatarETag', avatarETag); } else { // TODO: Why was this timeout added? setTimeout(async () => Users.setAvatarData(user._id, service, avatarETag, { session }), 500); } await onceTransactionCommitedSuccessfully(async () => { void api.broadcast('user.avatarUpdate', { username: user.username, avatarETag, }); }, session); } }