Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/small-pants-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes the download of attachments with non-unicode names on E2EE rooms
15 changes: 9 additions & 6 deletions apps/meteor/app/threads/server/functions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { IMessage } from '@rocket.chat/core-typings';
import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import { isEditedMessage } from '@rocket.chat/core-typings';
import { Messages, Subscriptions, ReadReceipts, NotificationQueue } from '@rocket.chat/models';

import { callbacks } from '../../../server/lib/callbacks';
import {
notifyOnSubscriptionChangedByRoomIdAndUserIds,
notifyOnSubscriptionChangedByRoomIdAndUserId,
Expand Down Expand Up @@ -82,19 +83,21 @@ export async function unfollow({ tmid, rid, uid }: { tmid: string; rid: string;
await Messages.removeThreadFollowerByThreadId(tmid, uid);
}

export const readThread = async ({ userId, rid, tmid }: { userId: string; rid: string; tmid: string }) => {
const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { tunread: 1 } });
export const readThread = async ({ user, room, tmid }: { user: IUser; room: IRoom; tmid: string }) => {
const sub = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { projection: { tunread: 1 } });
if (!sub) {
return;
}

// if the thread being marked as read is the last one unread also clear the unread subscription flag
const clearAlert = sub.tunread && sub.tunread?.length <= 1 && sub.tunread.includes(tmid);

const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId(rid, userId, tmid, clearAlert);
const removeUnreadThreadResponse = await Subscriptions.removeUnreadThreadByRoomIdAndUserId(room._id, user._id, tmid, clearAlert);
if (removeUnreadThreadResponse.modifiedCount) {
void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId);
void notifyOnSubscriptionChangedByRoomIdAndUserId(room._id, user._id);
}

await NotificationQueue.clearQueueByUserId(userId);
await NotificationQueue.clearQueueByUserId(user._id);

callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid });
};
6 changes: 2 additions & 4 deletions apps/meteor/app/threads/server/methods/getThreadMessages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IMessage } from '@rocket.chat/core-typings';
import type { IMessage, IUser } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Messages, Rooms } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -48,16 +48,14 @@ Meteor.methods<ServerMethods>({
}

await callbacks.run('beforeReadMessages', thread.rid, user._id);
await readThread({ userId: user._id, rid: thread.rid, tmid });
await readThread({ user: user as IUser, room, tmid });

const result = await Messages.findVisibleThreadByThreadId(tmid, {
...(skip && { skip }),
...(limit && { limit }),
sort: { ts: -1 },
}).toArray();

callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid });

return [thread, ...result];
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ const normalizeAttachments = (attachments: MessageAttachment[], name?: string, t

if (isFileAttachment(attachment)) {
if (attachment.title_link && !attachment.title_link.startsWith('/file-decrypt/')) {
attachment.title_link = `/file-decrypt${attachment.title_link}?key=${key}`;
attachment.title_link = `/file-decrypt${attachment.title_link}?key=${encodeURIComponent(key)}`;
}
if (isFileImageAttachment(attachment) && !attachment.image_url.startsWith('/file-decrypt/')) {
attachment.image_url = `/file-decrypt${attachment.image_url}?key=${key}`;
attachment.image_url = `/file-decrypt${attachment.image_url}?key=${encodeURIComponent(key)}`;
}
if (isFileAudioAttachment(attachment) && !attachment.audio_url.startsWith('/file-decrypt/')) {
attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${key}`;
attachment.audio_url = `/file-decrypt${attachment.audio_url}?key=${encodeURIComponent(key)}`;
}
if (isFileVideoAttachment(attachment) && !attachment.video_url.startsWith('/file-decrypt/')) {
attachment.video_url = `/file-decrypt${attachment.video_url}?key=${key}`;
attachment.video_url = `/file-decrypt${attachment.video_url}?key=${encodeURIComponent(key)}`;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const useReadReceiptsDetailsAction = (message: IMessage): MessageActionCo
id: 'receipt-detail',
icon: 'check-double',
label: 'Read_Receipts',
context: ['starred', 'message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads'],
context: ['starred', 'message', 'message-mobile', 'threads', 'videoconf', 'videoconf-threads', 'federated'],
type: 'duplication',
action() {
setModal(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const useImagesList = ({ roomId, startingFromId }: { roomId: IRoom['_id']
type: decrypted.type,
}),
);
decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`;
decrypted.path = `/file-decrypt${decrypted.path}?key=${encodeURIComponent(key)}`;
Object.assign(file, decrypted);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isRoomFederated, isThreadMainMessage } from '@rocket.chat/core-typings';
import { isThreadMainMessage, isRoomFederated } from '@rocket.chat/core-typings';
import { useLayout, useUser, useUserPreference, useSetting, useEndpoint, useSearchParameter } from '@rocket.chat/ui-contexts';
import type { ReactNode, RefCallback } from 'react';
import { useMemo, memo } from 'react';
Expand Down Expand Up @@ -39,8 +39,10 @@ const MessageListProvider = ({ children, messageListRef, attachmentDimension }:

const { isMobile } = useLayout();

const federationReadReceipts = useSetting('Federation_Service_EDU_Process_Receipt', false);

const autoLinkDomains = useSetting('Message_CustomDomain_AutoLink', '');
const readReceiptsEnabled = useSetting('Message_Read_Receipt_Enabled', false) && !isRoomFederated(room);
const readReceiptsEnabled = useSetting('Message_Read_Receipt_Enabled', false) && (!isRoomFederated(room) || federationReadReceipts);
const readReceiptsStoreUsers = useSetting('Message_Read_Receipt_Store_Users', false);
const apiEmbedEnabled = useSetting('API_Embed', false);
const showRealName = useSetting('UI_Use_Real_Name', false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const useFilesList = ({ rid, type, text }: { rid: Required<IUpload>['rid'
type: decrypted.type,
}),
);
decrypted.path = `/file-decrypt${decrypted.path}?key=${key}`;
decrypted.path = `/file-decrypt${decrypted.path}?key=${encodeURIComponent(key)}`;
Object.assign(file, decrypted);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MessageReads } from '@rocket.chat/core-services';
import { type IUser, type IRoom, type IMessage, isRoomFederated } from '@rocket.chat/core-typings';
import { type IUser, type IRoom, type IMessage } from '@rocket.chat/core-typings';

import { settings } from '../../../../../app/settings/server';
import { callbacks } from '../../../../../server/lib/callbacks';
Expand All @@ -11,10 +11,7 @@ callbacks.add(
if (!settings.get('Message_Read_Receipt_Enabled')) {
return;
}
// Rooms federated are not supported yet
if (isRoomFederated(room)) {
return;
}

const { uid, lastSeen, tmid } = params;

if (tmid) {
Expand Down
25 changes: 24 additions & 1 deletion apps/meteor/ee/server/hooks/federation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FederationMatrix, MeteorError, Room } from '@rocket.chat/core-services'
import { isEditedMessage, isRoomNativeFederated, isUserNativeFederated } from '@rocket.chat/core-typings';
import type { IRoomNativeFederated, IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import { validateFederatedUsername } from '@rocket.chat/federation-matrix';
import { Rooms } from '@rocket.chat/models';
import { Rooms, Users } from '@rocket.chat/models';

import { callbacks } from '../../../../server/lib/callbacks';
import { afterLeaveRoomCallback } from '../../../../server/lib/callbacks/afterLeaveRoomCallback';
Expand Down Expand Up @@ -287,3 +287,26 @@ prepareCreateRoomCallback.add(async ({ extraData }) => {
// only an empty "federation" object
(extraData as IRoomNativeFederated).federation = { version: 1 } as any;
});

callbacks.add(
'afterReadMessages',
async (room: IRoom, params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => {
if (!FederationActions.shouldPerformFederationAction(room)) {
return;
}

const user = await Users.findOneById(params.uid);
if (!user) {
return;
}

if (isUserNativeFederated(user)) {
// if the user is federated, it means the read receipt came from Matrix, so we don't need to notify Matrix again
return;
}

await FederationMatrix.notifyRoomRead({ room, userId: params.uid, threadId: params.tmid });
},
callbacks.priority.MEDIUM,
'federation-read-receipt',
);
2 changes: 2 additions & 0 deletions apps/meteor/ee/server/startup/federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const configureFederation = async () => {
allowedNonPrivateRooms: settings.get('Federation_Service_Join_Non_Private_Rooms'),
processEDUTyping: settings.get('Federation_Service_EDU_Process_Typing'),
processEDUPresence: settings.get('Federation_Service_EDU_Process_Presence'),
processEDUReceipt: settings.get('Federation_Service_EDU_Process_Receipt'),
});
} catch (err) {
logger.error({ msg: 'Failed to start federation-matrix service', err });
Expand Down Expand Up @@ -60,6 +61,7 @@ export const startFederationService = async (): Promise<void> => {
'Federation_Service_Domain',
'Federation_Service_EDU_Process_Typing',
'Federation_Service_EDU_Process_Presence',
'Federation_Service_EDU_Process_Receipt',
'Federation_Service_Matrix_Signing_Key',
'Federation_Service_Matrix_Signing_Algorithm',
'Federation_Service_Matrix_Signing_Version',
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"@rocket.chat/emitter": "^0.32.0",
"@rocket.chat/favicon": "workspace:^",
"@rocket.chat/federation-matrix": "workspace:^",
"@rocket.chat/federation-sdk": "0.3.9",
"@rocket.chat/federation-sdk": "0.4.1",
"@rocket.chat/fuselage": "^0.73.0",
"@rocket.chat/fuselage-forms": "^1.0.0",
"@rocket.chat/fuselage-hooks": "^0.40.0",
Expand Down
45 changes: 25 additions & 20 deletions apps/meteor/public/enc.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
self.addEventListener('install', function(event) {
event.waitUntil(self.skipWaiting()); // Activate worker immediately
self.addEventListener('install', function (event) {
event.waitUntil(self.skipWaiting()); // Activate worker immediately
});

self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim()); // Become available to all pages
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim()); // Become available to all pages
});

function base64Decode(string) {
Expand Down Expand Up @@ -32,7 +32,13 @@ const decrypt = async (key, iv, file) => {
const getUrlParams = (url) => {
const urlObj = new URL(url, location.origin);

const k = base64DecodeString(urlObj.searchParams.get('key'));
const rawKey = urlObj.searchParams.get('key');
if (!rawKey) {
throw new Error('Missing "key" query param');
}


const k = base64DecodeString(decodeURIComponent(rawKey));

urlObj.searchParams.delete('key');

Expand Down Expand Up @@ -74,7 +80,7 @@ self.addEventListener('fetch', (event) => {
const result = await decrypt(key, iv, file);

const newHeaders = new Headers(res.headers);
newHeaders.set('Content-Disposition', 'inline; filename="'+name+'"');
newHeaders.set('Content-Disposition', 'inline; filename="' + name + '"');
newHeaders.set('Content-Type', type);

const response = new Response(result, {
Expand Down Expand Up @@ -114,18 +120,17 @@ self.addEventListener('message', async (event) => {

const file = await res.arrayBuffer();
const result = await decrypt(key, iv, file);
event.source
.postMessage({
id: event.data.id,
type: 'attachment-download-result',
result,
});
// .catch((error) => {
// console.error('Posting message failed:', error);
// event.source.postMessage({
// id: event.data.id,
// type: 'attachment-download-result',
// error,
// });
// });
event.source.postMessage({
id: event.data.id,
type: 'attachment-download-result',
result,
});
// .catch((error) => {
// console.error('Posting message failed:', error);
// event.source.postMessage({
// id: event.data.id,
// type: 'attachment-download-result',
// error,
// });
// });
});
5 changes: 2 additions & 3 deletions apps/meteor/server/methods/readThreads.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IMessage } from '@rocket.chat/core-typings';
import type { IMessage, IUser } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Messages, Rooms } from '@rocket.chat/models';
import { check } from 'meteor/check';
Expand Down Expand Up @@ -44,8 +44,7 @@ Meteor.methods<ServerMethods>({

await callbacks.run('beforeReadMessages', thread.rid, user?._id);
if (user?._id) {
await readThread({ userId: user._id, rid: thread.rid, tmid });
callbacks.runAsync('afterReadMessages', room, { uid: user._id, tmid });
await readThread({ user: user as IUser, room, tmid });
}
},
});
14 changes: 14 additions & 0 deletions apps/meteor/server/services/room/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom';
import { createRoom } from '../../../app/lib/server/functions/createRoom'; // TODO remove this import
import { removeUserFromRoom, performUserRemoval } from '../../../app/lib/server/functions/removeUserFromRoom';
import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../app/lib/server/lib/notifyListener';
import { readThread } from '../../../app/threads/server/functions';
import { getDefaultSubscriptionPref } from '../../../app/utils/lib/getDefaultSubscriptionPref';
import { getValidRoomName } from '../../../app/utils/server/lib/getValidRoomName';
import { RoomMemberActions } from '../../../definition/IRoomTypeConfig';
import { getSubscriptionAutotranslateDefaultConfig } from '../../lib/getSubscriptionAutotranslateDefaultConfig';
import { readMessages } from '../../lib/readMessages';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { addRoomLeader } from '../../methods/addRoomLeader';
import { addRoomModerator } from '../../methods/addRoomModerator';
Expand Down Expand Up @@ -330,4 +332,16 @@ export class RoomService extends ServiceClassInternal implements IRoomService {

return insertedId;
}

async markAsRead(room: IRoom, userId: string, readThreads = false): Promise<void> {
await readMessages(room, userId, readThreads);
}

async readThread({ user, room, tmid }: { user: IUser; room: IRoom; tmid: string }): Promise<void> {
await readThread({
user,
room,
tmid,
});
}
}
9 changes: 9 additions & 0 deletions apps/meteor/server/settings/federation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ export const createFederationServiceSettings = async (): Promise<void> => {
alert: 'Federation_Service_EDU_Process_Presence_Alert',
});

await this.add('Federation_Service_EDU_Process_Receipt', false, {
type: 'boolean',
public: false,
enterprise: true,
modules: ['federation'],
invalidValue: false,
alert: 'Federation_Service_EDU_Process_Receipt_Alert',
});

await this.add('Federation_Service_Join_Encrypted_Rooms', false, {
type: 'boolean',
public: false,
Expand Down
29 changes: 29 additions & 0 deletions apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,35 @@ test.describe('E2EE File Encryption', () => {
});
});

test('File with Unicode filename uploads and downloads correctly', async ({ page }) => {
const UNICODE_FILE_NAME = 'Новый текстовый документ.txt';

await test.step('upload file with Unicode filename', async () => {
await poHomeChannel.content.sendFileMessage(TEST_FILE_TXT);
await poHomeChannel.composer.getFileByName(TEST_FILE_TXT).click();
await poHomeChannel.content.inputFileUploadName.fill(UNICODE_FILE_NAME);
await poHomeChannel.content.btnUpdateFileUpload.click();
await poHomeChannel.composer.btnSend.click();

await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getLastMessageByFileName(UNICODE_FILE_NAME)).toBeVisible();
});

await test.step('download the file and verify the Unicode filename is preserved', async () => {
await poHomeChannel.roomToolbar.openMoreOptions();
await poHomeChannel.roomToolbar.menuItemFiles.click();

await expect(poHomeChannel.tabs.files.getFileByName(UNICODE_FILE_NAME)).toBeVisible();

const [download] = await Promise.all([
page.waitForEvent('download'),
poHomeChannel.tabs.files.getFileByName(UNICODE_FILE_NAME).click(),
]);

expect(download.suggestedFilename()).toBe(UNICODE_FILE_NAME);
});
});

test('File encryption with whitelisted and blacklisted media types', async ({ api }) => {
await test.step('send a text file in channel', async () => {
const updatedFileName = `edited_${TEST_FILE_TXT}`;
Expand Down
2 changes: 1 addition & 1 deletion ee/packages/federation-matrix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.32.0",
"@rocket.chat/federation-sdk": "0.3.9",
"@rocket.chat/federation-sdk": "0.4.1",
"@rocket.chat/http-router": "workspace:^",
"@rocket.chat/license": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
Loading
Loading