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
299 lines
7.7 KiB
TypeScript
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;
|
|
};
|