import { Apps } from '@rocket.chat/apps'; import { Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { parseUrlsInMessage } from './parseUrlsInMessage'; import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; import { isURL } from '../../../../lib/utils/isURL'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; import { afterSaveMessage } from '../lib/afterSaveMessage'; import { notifyOnRoomChangedById, notifyOnMessageChange } from '../lib/notifyListener'; import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; // TODO: most of the types here are wrong, but I don't want to change them now /** * IMPORTANT * * This validator prevents malicious href values * intending to run arbitrary js code in anchor tags. * You should use it whenever the value you're checking * is going to be rendered in the href attribute of a * link. */ const validFullURLParam = Match.Where((value) => { check(value, String); if (!isURL(value) && !value.startsWith(FileUpload.getPath())) { throw new Error('Invalid href value provided'); } if (/^javascript:/i.test(value)) { throw new Error('Invalid href value provided'); } return true; }); const validPartialURLParam = Match.Where((value) => { check(value, String); if (!isRelativeURL(value) && !isURL(value) && !value.startsWith(FileUpload.getPath())) { throw new Error('Invalid href value provided'); } if (/^javascript:/i.test(value)) { throw new Error('Invalid href value provided'); } return true; }); const objectMaybeIncluding = (types: any) => Match.Where((value: any) => { Object.keys(types).forEach((field) => { if (value[field] != null) { try { check(value[field], types[field]); } catch (error: any) { error.path = field; throw error; } } }); return true; }); const validateAttachmentsFields = (attachmentField: any) => { check( attachmentField, objectMaybeIncluding({ short: Boolean, title: String, value: Match.OneOf(String, Number, Boolean), }), ); if (!attachmentField.value || !attachmentField.title) { throw new Error('Invalid attachment field, title and value is required'); } }; const validateAttachmentsActions = (attachmentActions: any) => { check( attachmentActions, objectMaybeIncluding({ type: String, text: String, url: validFullURLParam, image_url: validFullURLParam, is_webview: Boolean, webview_height_ratio: String, msg: String, msg_in_chat_window: Boolean, }), ); }; const validateAttachment = (attachment: any) => { check( attachment, objectMaybeIncluding({ color: String, text: String, ts: Match.OneOf(String, Number), thumb_url: validFullURLParam, button_alignment: String, actions: [Match.Any], message_link: validFullURLParam, collapsed: Boolean, author_name: String, author_link: validFullURLParam, author_icon: validFullURLParam, title: String, title_link: validFullURLParam, title_link_download: Boolean, image_dimensions: Object, image_url: validFullURLParam, image_preview: String, image_type: String, image_size: Number, audio_url: validFullURLParam, audio_type: String, audio_size: Number, video_url: validFullURLParam, video_type: String, video_size: Number, fields: [Match.Any], }), ); if (attachment.fields?.length) { attachment.fields.map(validateAttachmentsFields); } if (attachment.actions?.length) { attachment.actions.map(validateAttachmentsActions); } }; const validateBodyAttachments = (attachments: any[]) => attachments.map(validateAttachment); export const validateMessage = async (message: any, room: any, user: any) => { check( message, objectMaybeIncluding({ _id: String, msg: String, text: String, alias: String, emoji: String, tmid: String, tshow: Boolean, avatar: validPartialURLParam, attachments: [Match.Any], blocks: [Match.Any], }), ); if (message.alias || message.avatar) { const isLiveChatGuest = !message.avatar && user.token && user.token === room.v?.token; if (!isLiveChatGuest && !(await hasPermissionAsync(user._id, 'message-impersonate', room._id))) { throw new Error('Not enough permission'); } } if (Array.isArray(message.attachments) && message.attachments.length) { validateBodyAttachments(message.attachments); } if (message.customFields) { validateCustomMessageFields({ customFields: message.customFields, messageCustomFieldsEnabled: settings.get('Message_CustomFields_Enabled'), messageCustomFields: settings.get('Message_CustomFields'), }); } }; export function prepareMessageObject( message: Partial, rid: IRoom['_id'], user: { _id: string; username?: string; name?: string }, ): asserts message is IMessage { if (!message.ts) { message.ts = new Date(); } if (message.tshow !== true) { delete message.tshow; } const { _id, username, name } = user; message.u = { _id, username: username as string, // FIXME: this is wrong but I don't want to change it now name, }; message.rid = rid; if (!Match.test(message.msg, String)) { message.msg = ''; } if (message.ts == null) { message.ts = new Date(); } } /** * Validates and sends the message object. This function does not verify the Message_MaxAllowedSize settings. * Caller of the function should verify the Message_MaxAllowedSize if needed. * There might be same use cases which needs to override this setting. Example - sending error logs. */ export const sendMessage = async function (user: any, message: any, room: any, upsert = false, previewUrls?: string[]) { if (!user || !message || !room._id) { return false; } await validateMessage(message, room, user); prepareMessageObject(message, room._id, user); if (settings.get('Message_Read_Receipt_Enabled')) { message.unread = true; } // For the Rocket.Chat Apps :) if (Apps.self?.isLoaded()) { const listenerBridge = Apps.getBridges()?.getListenerBridge(); const prevent = await listenerBridge?.messageEvent('IPreMessageSentPrevent', message); if (prevent) { return; } const result = await listenerBridge?.messageEvent( 'IPreMessageSentModify', await listenerBridge?.messageEvent('IPreMessageSentExtend', message), ); if (typeof result === 'object') { message = Object.assign(message, result); // Some app may have inserted malicious/invalid values in the message, let's check it again await validateMessage(message, room, user); } } parseUrlsInMessage(message, previewUrls); message = await Message.beforeSave({ message, room, user }); if (!message) { return; } if (message._id && upsert) { const { _id } = message; delete message._id; await Messages.updateOne( { _id, 'u._id': message.u._id, }, { $set: message }, { upsert: true }, ); message._id = _id; } else { const messageAlreadyExists = message._id && (await Messages.findOneById(message._id, { projection: { _id: 1 } })); if (messageAlreadyExists) { return; } const { insertedId } = await Messages.insertOne(message); message._id = insertedId; } if (Apps.self?.isLoaded()) { // If the message has a type (system message), we should notify the listener about it const messageEvent = message.t ? 'IPostSystemMessageSent' : 'IPostMessageSent'; void Apps.getBridges()?.getListenerBridge().messageEvent(messageEvent, message); } // TODO: is there an opportunity to send returned data to notifyOnMessageChange? await afterSaveMessage(message, room, user); void notifyOnMessageChange({ id: message._id }); void notifyOnRoomChangedById(message.rid); return message; };