Skip to content

Commit

Permalink
Merge branch 'develop' into case-insensitive-email-handling
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored Nov 7, 2022
2 parents ee1723b + c9ac4eb commit 5ebcdf3
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 106 deletions.
157 changes: 100 additions & 57 deletions apps/meteor/app/api/server/lib/getUploadFormData.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import type { Readable } from 'stream';

import { Meteor } from 'meteor/meteor';
import type { Request } from 'express';
import busboy from 'busboy';
import type { ValidateFunction } from 'ajv';

type UploadResult = {
file: Readable;
import { MeteorError } from '../../../../server/sdk/errors';

type UploadResult<K> = {
file: Readable & { truncated: boolean };
fieldname: string;
filename: string;
encoding: string;
mimetype: string;
fileBuffer: Buffer;
fields: K;
};

export const getUploadFormData = async <
export async function getUploadFormData<
T extends string,
K extends Record<string, string> = Record<string, string>,
V extends ValidateFunction<K> = ValidateFunction<K>,
Expand All @@ -22,63 +25,103 @@ export const getUploadFormData = async <
options: {
field?: T;
validate?: V;
sizeLimit?: number;
} = {},
): Promise<[UploadResult, K, T]> =>
new Promise((resolve, reject) => {
const bb = busboy({ headers: request.headers, defParamCharset: 'utf8' });
const fields = Object.create(null) as K;

let uploadedFile: UploadResult | undefined;

let assetName: T | undefined;

bb.on(
'file',
(
fieldname: string,
file: Readable,
{ filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string },
) => {
const fileData: Uint8Array[] = [];

file.on('data', (data: any) => fileData.push(data));

file.on('end', () => {
if (uploadedFile) {
return reject('Just 1 file is allowed');
}
if (options.field && fieldname !== options.field) {
return reject(new Meteor.Error('invalid-field'));
}
uploadedFile = {
file,
filename,
encoding,
mimetype,
fileBuffer: Buffer.concat(fileData),
};

assetName = fieldname as T;
});
},
);

bb.on('field', (fieldname: keyof K, value: K[keyof K]) => {
fields[fieldname] = value;
): Promise<UploadResult<K>> {
const limits = {
files: 1,
...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }),
};

const bb = busboy({ headers: request.headers, defParamCharset: 'utf8', limits });
const fields = Object.create(null) as K;

let uploadedFile: UploadResult<K> | undefined;

let returnResult = (_value: UploadResult<K>) => {
// noop
};
let returnError = (_error?: Error | string | null | undefined) => {
// noop
};

function onField(fieldname: keyof K, value: K[keyof K]) {
fields[fieldname] = value;
}

function onEnd() {
if (!uploadedFile) {
return returnError(new MeteorError('No file uploaded'));
}
if (options.validate !== undefined && !options.validate(fields)) {
return returnError(new MeteorError(`Invalid fields ${options.validate.errors?.join(', ')}`));
}
return returnResult(uploadedFile);
}

function onFile(
fieldname: string,
file: Readable & { truncated: boolean },
{ filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string },
) {
if (options.field && fieldname !== options.field) {
file.resume();
return returnError(new MeteorError('invalid-field'));
}

const fileChunks: Uint8Array[] = [];
file.on('data', function (chunk) {
fileChunks.push(chunk);
});

bb.on('finish', () => {
if (!uploadedFile || !assetName) {
return reject('No file uploaded');
}
if (options.validate === undefined) {
return resolve([uploadedFile, fields, assetName]);
}
if (!options.validate(fields)) {
return reject(`Invalid fields${options.validate.errors?.join(', ')}`);
file.on('end', function () {
if (file.truncated) {
fileChunks.length = 0;
return returnError(new MeteorError('error-file-too-large'));
}
return resolve([uploadedFile, fields, assetName]);

uploadedFile = {
file,
filename,
encoding,
mimetype,
fieldname,
fields,
fileBuffer: Buffer.concat(fileChunks),
};
});
}

function cleanup() {
request.unpipe(bb);
request.on('readable', request.read.bind(request));
bb.removeAllListeners();
}

bb.on('field', onField);
bb.on('file', onFile);
bb.on('close', cleanup);
bb.on('end', onEnd);
bb.on('finish', onEnd);

bb.on('error', function (err: Error) {
returnError(err);
});

bb.on('partsLimit', function () {
returnError();
});
bb.on('filesLimit', function () {
returnError('Just 1 file is allowed');
});
bb.on('fieldsLimit', function () {
returnError();
});

request.pipe(bb);

request.pipe(bb);
return new Promise((resolve, reject) => {
returnResult = resolve;
returnError = reject;
});
}
13 changes: 9 additions & 4 deletions apps/meteor/app/api/server/v1/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,33 @@ import { isAssetsUnsetAssetProps } from '@rocket.chat/rest-typings';
import { RocketChatAssets } from '../../../assets/server';
import { API } from '../api';
import { getUploadFormData } from '../lib/getUploadFormData';
import { settings } from '../../../settings/server';

API.v1.addRoute(
'assets.setAsset',
{ authRequired: true },
{
async post() {
const [asset, { refreshAllClients, assetName: customName }, fileName] = await getUploadFormData(
const asset = await getUploadFormData(
{
request: this.request,
},
{ field: 'asset' },
{ field: 'asset', sizeLimit: settings.get('FileUpload_MaxFileSize') },
);

const assetName = customName || fileName;
const { fileBuffer, fields, filename, mimetype } = asset;

const { refreshAllClients, assetName: customName } = fields;

const assetName = customName || filename;
const assetsKeys = Object.keys(RocketChatAssets.assets);

const isValidAsset = assetsKeys.includes(assetName);
if (!isValidAsset) {
throw new Meteor.Error('error-invalid-asset', 'Invalid asset');
}

Meteor.call('setAsset', asset.fileBuffer, asset.mimetype, assetName);
Meteor.call('setAsset', fileBuffer, mimetype, assetName);
if (refreshAllClients) {
Meteor.call('refreshClients');
}
Expand Down
27 changes: 16 additions & 11 deletions apps/meteor/app/api/server/v1/emoji-custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getUploadFormData } from '../lib/getUploadFormData';
import { findEmojisCustom } from '../lib/emoji-custom';
import { Media } from '../../../../server/sdk';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { settings } from '../../../settings/server';

API.v1.addRoute(
'emoji-custom.list',
Expand Down Expand Up @@ -68,19 +69,21 @@ API.v1.addRoute(
{ authRequired: true },
{
async post() {
const [emoji, fields] = await getUploadFormData(
const emoji = await getUploadFormData(
{
request: this.request,
},
{ field: 'emoji' },
{ field: 'emoji', sizeLimit: settings.get('FileUpload_MaxFileSize') },
);

const isUploadable = await Media.isImage(emoji.fileBuffer);
const { fields, fileBuffer, mimetype } = emoji;

const isUploadable = await Media.isImage(fileBuffer);
if (!isUploadable) {
throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image");
}

const [, extension] = emoji.mimetype.split('/');
const [, extension] = mimetype.split('/');
fields.extension = extension;

try {
Expand All @@ -89,7 +92,7 @@ API.v1.addRoute(
newFile: true,
aliases: fields.aliases || '',
});
Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, {
Meteor.call('uploadEmojiCustom', fileBuffer, mimetype, {
...fields,
newFile: true,
aliases: fields.aliases || '',
Expand All @@ -109,13 +112,15 @@ API.v1.addRoute(
{ authRequired: true },
{
async post() {
const [emoji, fields] = await getUploadFormData(
const emoji = await getUploadFormData(
{
request: this.request,
},
{ field: 'emoji' },
{ field: 'emoji', sizeLimit: settings.get('FileUpload_MaxFileSize') },
);

const { fields, fileBuffer, mimetype } = emoji;

if (!fields._id) {
throw new Meteor.Error('The required "_id" query param is missing.');
}
Expand All @@ -128,23 +133,23 @@ API.v1.addRoute(
fields.previousName = emojiToUpdate.name;
fields.previousExtension = emojiToUpdate.extension;
fields.aliases = fields.aliases || '';
const newFile = Boolean(emoji?.fileBuffer.length);
const newFile = Boolean(emoji && fileBuffer.length);

if (fields.newFile) {
const isUploadable = await Media.isImage(emoji.fileBuffer);
const isUploadable = await Media.isImage(fileBuffer);
if (!isUploadable) {
throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image");
}

const [, extension] = emoji.mimetype.split('/');
const [, extension] = mimetype.split('/');
fields.extension = extension;
} else {
fields.extension = emojiToUpdate.extension;
}

Meteor.call('insertOrUpdateEmoji', { ...fields, newFile });
if (fields.newFile) {
Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, { ...fields, newFile });
Meteor.call('uploadEmojiCustom', fileBuffer, mimetype, { ...fields, newFile });
}
return API.v1.success();
},
Expand Down
26 changes: 14 additions & 12 deletions apps/meteor/app/api/server/v1/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,39 +81,41 @@ API.v1.addRoute(
'rooms.upload/:rid',
{ authRequired: true },
{
post() {
async post() {
if (!canAccessRoomId(this.urlParams.rid, this.userId)) {
return API.v1.unauthorized();
}

const [file, fields] = Promise.await(
getUploadFormData(
{
request: this.request,
},
{ field: 'file' },
),
const file = await getUploadFormData(
{
request: this.request,
},
{ field: 'file', sizeLimit: settings.get('FileUpload_MaxFileSize') },
);

if (!file) {
throw new Meteor.Error('invalid-field');
}

const { fields } = file;
let { fileBuffer } = file;

const details = {
name: file.filename,
size: file.fileBuffer.length,
size: fileBuffer.length,
type: file.mimetype,
rid: this.urlParams.rid,
userId: this.userId,
};

const stripExif = settings.get('Message_Attachments_Strip_Exif');
const fileStore = FileUpload.getStore('Uploads');
if (stripExif) {
// No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc)
file.fileBuffer = Promise.await(Media.stripExifFromBuffer(file.fileBuffer));
fileBuffer = await Media.stripExifFromBuffer(fileBuffer);
}
const uploadedFile = fileStore.insertSync(details, file.fileBuffer);

const fileStore = FileUpload.getStore('Uploads');
const uploadedFile = await fileStore.insert(details, fileBuffer);

uploadedFile.description = fields.description;

Expand Down
10 changes: 5 additions & 5 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,19 +189,19 @@ API.v1.addRoute(
return API.v1.success();
}

const [image, fields] = await getUploadFormData(
const image = await getUploadFormData(
{
request: this.request,
},
{
field: 'image',
},
{ field: 'image', sizeLimit: settings.get('FileUpload_MaxFileSize') },
);

if (!image) {
return API.v1.failure("The 'image' param is required");
}

const { fields, fileBuffer, mimetype } = image;

const sentTheUserByFormData = fields.userId || fields.username;
if (sentTheUserByFormData) {
if (fields.userId) {
Expand All @@ -220,7 +220,7 @@ API.v1.addRoute(
}
}

setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest');
setUserAvatar(user, fileBuffer, mimetype, 'rest');

return API.v1.success();
},
Expand Down
Loading

0 comments on commit 5ebcdf3

Please sign in to comment.