Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

299 lines
7.7 KiB
TypeScript

import { AppEvents, 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 { isAbsoluteURL } from '@rocket.chat/tools';
import { Match, check } from 'meteor/check';
import { isRelativeURL } from '../../../lib/utils/isRelativeURL';
import { afterSaveMessage } from '../../hooks/messages/afterSaveMessage';
import { settings } from '../../settings';
import { hasPermissionAsync } from '../authorization/hasPermission';
import { FileUpload } from '../media/file-upload';
import { validateCustomMessageFields } from '../messaging/validateCustomMessageFields';
import { notifyOnRoomChangedById } from '../notifyListener';
export type SendMessageOptions = {
upsert?: boolean;
previewUrls?: string[];
skipNotifications?: boolean;
};
// 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 (!isAbsoluteURL(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) && !isAbsoluteURL(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<boolean>('Message_CustomFields_Enabled'),
messageCustomFields: settings.get<string>('Message_CustomFields'),
});
}
};
export function prepareMessageObject(
message: Partial<IMessage>,
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, options: SendMessageOptions = {}) {
const { upsert = false, previewUrls } = options;
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 prevent = await Apps.self?.triggerEvent(AppEvents.IPreMessageSentPrevent, message);
if (prevent) {
return;
}
const result = await Apps.self?.triggerEvent(
AppEvents.IPreMessageSentModify,
await Apps.self?.triggerEvent(AppEvents.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);
}
}
message = await Message.beforeSave({ message, room, user, previewUrls, parseUrls: message.parseUrls });
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 ? AppEvents.IPostSystemMessageSent : AppEvents.IPostMessageSent;
void Apps.self?.triggerEvent(messageEvent, message);
}
await afterSaveMessage(message, room, user, { options });
void notifyOnRoomChangedById(message.rid);
return message;
};