Skip to content

Commit 02dd875

Browse files
feat: Use application/octet-stream as a fallback media type to avoid "Unknown media type" errors (#32471)
1 parent 3039968 commit 02dd875

File tree

11 files changed

+100
-16
lines changed

11 files changed

+100
-16
lines changed

.changeset/breezy-pens-sing.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
"@rocket.chat/i18n": minor
4+
---
5+
6+
Removed "Unknown media type" errors on the client side by using `application/octet-stream` as a fallback media type (MIME type) for all files

apps/meteor/app/api/server/lib/getUploadFormData.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { ValidateFunction } from 'ajv';
55
import busboy from 'busboy';
66
import type { Request } from 'express';
77

8+
import { getMimeType } from '../../../utils/lib/mimeTypes';
9+
810
type UploadResult<K> = {
911
file: Readable & { truncated: boolean };
1012
fieldname: string;
@@ -61,7 +63,7 @@ export async function getUploadFormData<
6163
function onFile(
6264
fieldname: string,
6365
file: Readable & { truncated: boolean },
64-
{ filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string },
66+
{ filename, encoding }: { filename: string; encoding: string },
6567
) {
6668
if (options.field && fieldname !== options.field) {
6769
file.resume();
@@ -83,7 +85,7 @@ export async function getUploadFormData<
8385
file,
8486
filename,
8587
encoding,
86-
mimetype,
88+
mimetype: getMimeType(filename),
8789
fieldname,
8890
fields,
8991
fileBuffer: Buffer.concat(fileChunks),

apps/meteor/app/utils/lib/mimeTypes.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ const getExtension = (param: string): string => {
1212
return !extension || typeof extension === 'boolean' ? '' : extension;
1313
};
1414

15-
export { mime, getExtension };
15+
const getMimeType = (fileName: string): string => {
16+
const fileMimeType = mime.lookup(fileName);
17+
return typeof fileMimeType === 'string' ? fileMimeType : 'application/octet-stream';
18+
};
19+
20+
export { mime, getExtension, getMimeType };

apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const useFileUploadDropTarget = (): readonly [
2929

3030
const t = useTranslation();
3131

32-
const fileUploadEnabled = useSetting('FileUpload_Enabled') as boolean;
32+
const fileUploadEnabled = useSetting<boolean>('FileUpload_Enabled');
3333
const user = useUser();
3434
const fileUploadAllowedForUser = useReactiveValue(
3535
useCallback(() => !roomCoordinator.readOnly(room._id, { username: user?.username }), [room._id, user?.username]),
@@ -38,8 +38,7 @@ export const useFileUploadDropTarget = (): readonly [
3838
const chat = useChat();
3939

4040
const onFileDrop = useMutableCallback(async (files: File[]) => {
41-
const { mime } = await import('../../../../../app/utils/lib/mimeTypes');
42-
41+
const { getMimeType } = await import('../../../../../app/utils/lib/mimeTypes');
4342
const getUniqueFiles = () => {
4443
const uniqueFiles: File[] = [];
4544
const st: Set<number> = new Set();
@@ -55,7 +54,7 @@ export const useFileUploadDropTarget = (): readonly [
5554
const uniqueFiles = getUniqueFiles();
5655

5756
const uploads = Array.from(uniqueFiles).map((file) => {
58-
Object.defineProperty(file, 'type', { value: mime.lookup(file.name) });
57+
Object.defineProperty(file, 'type', { value: getMimeType(file.name) });
5958
return file;
6059
});
6160

apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const fileInputProps = { type: 'file', multiple: true };
99

1010
export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => {
1111
const t = useTranslation();
12-
const fileUploadEnabled = useSetting('FileUpload_Enabled');
12+
const fileUploadEnabled = useSetting<boolean>('FileUpload_Enabled');
1313
const fileInputRef = useFileInput(fileInputProps);
1414
const chat = useChat();
1515

@@ -23,10 +23,10 @@ export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps =>
2323
};
2424

2525
const handleUploadChange = async () => {
26-
const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes');
26+
const { getMimeType } = await import('../../../../../../../app/utils/lib/mimeTypes');
2727
const filesToUpload = Array.from(fileInputRef?.current?.files ?? []).map((file) => {
2828
Object.defineProperty(file, 'type', {
29-
value: mime.lookup(file.name),
29+
value: getMimeType(file.name),
3030
});
3131
return file;
3232
});

apps/meteor/server/settings/file-upload.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const createFileUploadSettings = () =>
2323
type: 'string',
2424
public: true,
2525
i18nDescription: 'FileUpload_MediaTypeBlackListDescription',
26+
alert: 'FileUpload_MediaTypeBlackList_Alert',
2627
});
2728

2829
await this.add('FileUpload_ProtectFiles', true, {
+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const targetUser = 'rocket.cat';
22
export const imgURL = './public/images/logo/1024x1024.png';
33
export const lstURL = './tests/e2e/fixtures/files/lst-test.lst';
4+
export const drawioURL = './tests/e2e/fixtures/files/diagram.drawio';
45
export const svgLogoURL = './public/images/logo/logo.svg';
56
export const svgLogoFileName = 'logo.svg';

apps/meteor/tests/e2e/file-upload.spec.ts

+21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Users } from './fixtures/userStates';
22
import { HomeChannel } from './page-objects';
33
import { createTargetChannel } from './utils';
4+
import { setSettingValueById } from './utils/setSettingValueById';
45
import { expect, test } from './utils/test';
56

67
test.use({ storageState: Users.user1.state });
@@ -10,6 +11,7 @@ test.describe.serial('file-upload', () => {
1011
let targetChannel: string;
1112

1213
test.beforeAll(async ({ api }) => {
14+
await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml');
1315
targetChannel = await createTargetChannel(api);
1416
});
1517

@@ -21,6 +23,7 @@ test.describe.serial('file-upload', () => {
2123
});
2224

2325
test.afterAll(async ({ api }) => {
26+
await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'image/svg+xml');
2427
expect((await api.post('/channels.delete', { roomName: targetChannel })).status()).toBe(200);
2528
});
2629

@@ -54,4 +57,22 @@ test.describe.serial('file-upload', () => {
5457
await expect(poHomeChannel.content.getFileDescription).toHaveText('lst_description');
5558
await expect(poHomeChannel.content.lastMessageFileName).toContainText('lst-test.lst');
5659
});
60+
61+
test('expect send drawio (unknown media type) file succesfully', async ({ page }) => {
62+
await page.reload();
63+
await poHomeChannel.content.sendFileMessage('diagram.drawio');
64+
await poHomeChannel.content.descriptionInput.fill('drawio_description');
65+
await poHomeChannel.content.btnModalConfirm.click();
66+
67+
await expect(poHomeChannel.content.getFileDescription).toHaveText('drawio_description');
68+
await expect(poHomeChannel.content.lastMessageFileName).toContainText('diagram.drawio');
69+
});
70+
71+
test('expect not to send drawio file (unknown media type) when the default media type is blocked', async ({ api, page }) => {
72+
await setSettingValueById(api, 'FileUpload_MediaTypeBlackList', 'application/octet-stream');
73+
74+
await page.reload();
75+
await poHomeChannel.content.sendFileMessage('diagram.drawio');
76+
await expect(poHomeChannel.content.btnModalConfirm).not.toBeVisible();
77+
});
5778
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<mxfile host="app.diagrams.net" modified="2024-05-21T16:10:09.295Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" etag="ZxZRCTHi-kxhlzk7b9_Z" version="24.4.4" type="device">
2+
<diagram name="Página-1" id="9eBILa8281JaQ4yUkDbp">
3+
<mxGraphModel dx="1434" dy="786" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
4+
<root>
5+
<mxCell id="0" />
6+
<mxCell id="1" parent="0" />
7+
<mxCell id="dopCU4gkJe7Sfp6IDYO1-1" value="&lt;b&gt;&lt;font style=&quot;font-size: 30px;&quot;&gt;Rocket.Chat&lt;/font&gt;&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
8+
<mxGeometry x="314" y="350" width="200" height="50" as="geometry" />
9+
</mxCell>
10+
</root>
11+
</mxGraphModel>
12+
</diagram>
13+
</mxfile>

apps/meteor/tests/end-to-end/api/09-rooms.js

+41-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { after, afterEach, before, beforeEach, describe, it } from 'mocha';
77
import { sleep } from '../../../lib/utils/sleep';
88
import { getCredentials, api, request, credentials } from '../../data/api-data.js';
99
import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper';
10-
import { imgURL, lstURL, svgLogoFileName, svgLogoURL } from '../../data/interactions';
10+
import { drawioURL, imgURL, lstURL, svgLogoFileName, svgLogoURL } from '../../data/interactions';
1111
import { getSettingValueById, updateEEPermission, updatePermission, updateSetting } from '../../data/permissions.helper';
1212
import { createRoom, deleteRoom } from '../../data/rooms.helper';
1313
import { deleteTeam } from '../../data/teams.helper';
@@ -183,8 +183,8 @@ describe('[Rooms]', function () {
183183
});
184184
});
185185

186-
it('should upload a LST file to room', (done) => {
187-
request
186+
it('should upload a LST file to room', () => {
187+
return request
188188
.post(api(`rooms.upload/${testChannel._id}`))
189189
.set(credentials)
190190
.attach('file', lstURL)
@@ -200,12 +200,33 @@ describe('[Rooms]', function () {
200200
expect(res.body.message).to.have.property('files');
201201
expect(res.body.message.files).to.be.an('array').of.length(1);
202202
expect(res.body.message.files[0]).to.have.property('name', 'lst-test.lst');
203-
})
204-
.end(done);
203+
expect(res.body.message.files[0]).to.have.property('type', 'text/plain');
204+
});
205+
});
206+
207+
it('should upload a DRAWIO file (unknown media type) to room', () => {
208+
return request
209+
.post(api(`rooms.upload/${testChannel._id}`))
210+
.set(credentials)
211+
.attach('file', drawioURL)
212+
.expect('Content-Type', 'application/json')
213+
.expect(200)
214+
.expect((res) => {
215+
expect(res.body).to.have.property('success', true);
216+
expect(res.body).to.have.property('message');
217+
expect(res.body.message).to.have.property('attachments');
218+
expect(res.body.message.attachments).to.be.an('array').of.length(1);
219+
expect(res.body.message.attachments[0]).to.have.property('format', 'DRAWIO');
220+
expect(res.body.message.attachments[0]).to.have.property('title', 'diagram.drawio');
221+
expect(res.body.message).to.have.property('files');
222+
expect(res.body.message.files).to.be.an('array').of.length(1);
223+
expect(res.body.message.files[0]).to.have.property('name', 'diagram.drawio');
224+
expect(res.body.message.files[0]).to.have.property('type', 'application/octet-stream');
225+
});
205226
});
206227

207228
it('should not allow uploading a blocked media type to a room', async () => {
208-
await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream');
229+
await updateSetting('FileUpload_MediaTypeBlackList', 'text/plain');
209230
await request
210231
.post(api(`rooms.upload/${testChannel._id}`))
211232
.set(credentials)
@@ -218,6 +239,20 @@ describe('[Rooms]', function () {
218239
});
219240
});
220241

242+
it('should not allow uploading an unknown media type to a room if the default one is blocked', async () => {
243+
await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream');
244+
await request
245+
.post(api(`rooms.upload/${testChannel._id}`))
246+
.set(credentials)
247+
.attach('file', drawioURL)
248+
.expect('Content-Type', 'application/json')
249+
.expect(400)
250+
.expect((res) => {
251+
expect(res.body).to.have.property('success', false);
252+
expect(res.body).to.have.property('errorType', 'error-invalid-file-type');
253+
});
254+
});
255+
221256
it('should be able to get the file', async () => {
222257
await request.get(fileNewUrl).set(credentials).expect('Content-Type', 'image/png').expect(200);
223258
await request.get(fileOldUrl).set(credentials).expect('Content-Type', 'image/png').expect(200);

packages/i18n/src/locales/en.i18n.json

+1
Original file line numberDiff line numberDiff line change
@@ -2383,6 +2383,7 @@
23832383
"FileUpload_MediaType_NotAccepted": "Media Types Not Accepted",
23842384
"FileUpload_MediaTypeBlackList": "Blocked Media Types",
23852385
"FileUpload_MediaTypeBlackListDescription": "Comma-separated list of media types. This setting has priority over the Accepted Media Types.",
2386+
"FileUpload_MediaTypeBlackList_Alert": "The default media type for unknown file extensions is \"application/octet-stream\", to work only with known file extensions you can add it to the \"Blocked Media Types\" list.",
23862387
"FileUpload_MediaTypeWhiteList": "Accepted Media Types",
23872388
"FileUpload_MediaTypeWhiteListDescription": "Comma-separated list of media types. Leave it blank for accepting all media types.",
23882389
"FileUpload_ProtectFiles": "Protect Uploaded Files",

0 commit comments

Comments
 (0)