snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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 1s
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 1s
This commit is contained in:
298
apps/meteor/server/lib/messages/sendMessage.ts
Normal file
298
apps/meteor/server/lib/messages/sendMessage.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user