diff --git a/app/api/server/lib/getUploadFormData.js b/app/api/server/lib/getUploadFormData.js new file mode 100644 index 0000000000000..59a21968887d9 --- /dev/null +++ b/app/api/server/lib/getUploadFormData.js @@ -0,0 +1,27 @@ +import Busboy from 'busboy'; + +export const getUploadFormData = async ({ request }) => new Promise((resolve, reject) => { + const busboy = new Busboy({ headers: request.headers }); + + const fields = {}; + + busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + const fileData = []; + + file.on('data', (data) => fileData.push(data)); + + file.on('end', () => { + if (fields.hasOwnProperty(fieldname)) { + return reject('Just 1 file is allowed'); + } + + fields[fieldname] = { file, filename, encoding, mimetype, fileBuffer: Buffer.concat(fileData) }; + }); + }); + + busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); + + busboy.on('finish', () => resolve(fields)); + + request.pipe(busboy); +}); diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js index 5e51d734d8a22..975780f58e833 100644 --- a/app/api/server/v1/rooms.js +++ b/app/api/server/v1/rooms.js @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import Busboy from 'busboy'; import { FileUpload } from '../../../file-upload'; import { Rooms, Messages } from '../../../models'; @@ -9,6 +8,7 @@ import { sendFile, sendViaEmail } from '../../../../server/lib/channelExport'; import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { Media } from '../../../../server/sdk'; import { settings } from '../../../settings/server/index'; +import { getUploadFormData } from '../lib/getUploadFormData'; function findRoomByIdOrName({ params, checkedArchived = true }) { if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { @@ -63,33 +63,6 @@ API.v1.addRoute('rooms.get', { authRequired: true }, { }, }); -const getFiles = Meteor.wrapAsync(({ request }, callback) => { - const busboy = new Busboy({ headers: request.headers }); - const files = []; - - const fields = {}; - - - busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { - if (fieldname !== 'file') { - return callback(new Meteor.Error('invalid-field')); - } - - const fileDate = []; - file.on('data', (data) => fileDate.push(data)); - - file.on('end', () => { - files.push({ fieldname, file, filename, encoding, mimetype, fileBuffer: Buffer.concat(fileDate) }); - }); - }); - - busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); - - busboy.on('finish', Meteor.bindEnvironment(() => callback(null, { files, fields }))); - - request.pipe(busboy); -}); - API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { post() { const room = Meteor.call('canAccessRoom', this.urlParams.rid, this.userId); @@ -98,21 +71,14 @@ API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { return API.v1.unauthorized(); } - - const { files, fields } = getFiles({ + const { file, ...fields } = Promise.await(getUploadFormData({ request: this.request, - }); - - if (files.length === 0) { - return API.v1.failure('File required'); - } + })); - if (files.length > 1) { - return API.v1.failure('Just 1 file is allowed'); + if (!file) { + throw new Meteor.Error('invalid-field'); } - const file = files[0]; - const details = { name: file.filename, size: file.fileBuffer.length, @@ -121,25 +87,21 @@ API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { userId: this.userId, }; - const fileData = Meteor.runAsUser(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)); - } - const uploadedFile = fileStore.insertSync(details, file.fileBuffer); - - uploadedFile.description = fields.description; + 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)); + } + const uploadedFile = fileStore.insertSync(details, file.fileBuffer); - delete fields.description; + uploadedFile.description = fields.description; - Meteor.call('sendFileMessage', this.urlParams.rid, null, uploadedFile, fields); + delete fields.description; - return uploadedFile; - }); + Meteor.call('sendFileMessage', this.urlParams.rid, null, uploadedFile, fields); - return API.v1.success({ message: Messages.getMessageByFileIdAndUsername(fileData._id, this.userId) }); + return API.v1.success({ message: Messages.getMessageByFileIdAndUsername(uploadedFile._id, this.userId) }); }, }); diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js index 9964c900b1f9e..3c245d75ed592 100644 --- a/app/apps/server/communication/rest.js +++ b/app/apps/server/communication/rest.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; -import Busboy from 'busboy'; import { API } from '../../../api/server'; +import { getUploadFormData } from '../../../api/server/lib/getUploadFormData'; import { getWorkspaceAccessToken, getUserCloudAccessToken } from '../../../cloud/server'; import { settings } from '../../../settings'; import { Info } from '../../../utils'; @@ -24,24 +24,6 @@ export class AppsRestApi { this.loadAPI(); } - _handleMultipartFormData(request) { - const busboy = new Busboy({ headers: request.headers }); - return Meteor.wrapAsync((callback) => { - const formFields = {}; - busboy.on('file', Meteor.bindEnvironment((fieldname, file) => { - const fileData = []; - file.on('data', Meteor.bindEnvironment((data) => { - fileData.push(data); - })); - - file.on('end', Meteor.bindEnvironment(() => { formFields[fieldname] = Buffer.concat(fileData); })); - })); - busboy.on('field', (fieldname, val) => { formFields[fieldname] = val; }); - busboy.on('finish', Meteor.bindEnvironment(() => callback(undefined, formFields))); - request.pipe(busboy); - })(); - } - async loadAPI() { this.api = new API.ApiClass({ version: 'apps', @@ -56,7 +38,6 @@ export class AppsRestApi { addManagementRoutes() { const orchestrator = this._orch; const manager = this._manager; - const multipartFormDataHandler = this._handleMultipartFormData; const handleError = (message, e) => { // when there is no `response` field in the error, it means the request @@ -239,8 +220,10 @@ export class AppsRestApi { return API.v1.failure({ error: 'Direct installation of an App is disabled.' }); } - const formData = multipartFormDataHandler(this.request); - buff = formData?.app; + const formData = Promise.await(getUploadFormData({ + request: this.request, + })); + buff = formData?.app?.fileBuffer; permissionsGranted = (() => { try { const permissions = JSON.parse(formData?.permissions || ''); @@ -460,8 +443,10 @@ export class AppsRestApi { return API.v1.failure({ error: 'Direct updating of an App is disabled.' }); } - const formData = multipartFormDataHandler(this.request); - buff = formData?.app; + const formData = Promise.await(getUploadFormData({ + request: this.request, + })); + buff = formData?.app?.fileBuffer; permissionsGranted = (() => { try { const permissions = JSON.parse(formData?.permissions || ''); diff --git a/app/livechat/imports/server/rest/upload.js b/app/livechat/imports/server/rest/upload.js index 3a2cff1938963..ca89ec139e9c8 100644 --- a/app/livechat/imports/server/rest/upload.js +++ b/app/livechat/imports/server/rest/upload.js @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import Busboy from 'busboy'; import filesize from 'filesize'; import { settings } from '../../../../settings'; @@ -7,6 +6,7 @@ import { Settings, LivechatRooms, LivechatVisitors } from '../../../../models'; import { fileUploadIsValidContentType } from '../../../../utils'; import { FileUpload } from '../../../../file-upload'; import { API } from '../../../../api/server'; +import { getUploadFormData } from '../../../../api/server/lib/getUploadFormData'; let maxFileSize; @@ -36,40 +36,9 @@ API.v1.addRoute('livechat/upload/:rid', { return API.v1.unauthorized(); } - const busboy = new Busboy({ headers: this.request.headers }); - const files = []; - const fields = {}; - - Meteor.wrapAsync((callback) => { - busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { - if (fieldname !== 'file') { - return files.push(new Meteor.Error('invalid-field')); - } - - const fileDate = []; - file.on('data', (data) => fileDate.push(data)); - - file.on('end', () => { - files.push({ fieldname, file, filename, encoding, mimetype, fileBuffer: Buffer.concat(fileDate) }); - }); - }); - - busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); - - busboy.on('finish', Meteor.bindEnvironment(() => callback())); - - this.request.pipe(busboy); - })(); - - if (files.length === 0) { - return API.v1.failure('File required'); - } - - if (files.length > 1) { - return API.v1.failure('Just 1 file is allowed'); - } - - const file = files[0]; + const { file, ...fields } = Promise.await(getUploadFormData({ + request: this.request, + })); if (!fileUploadIsValidContentType(file.mimetype)) { return API.v1.failure({ @@ -95,7 +64,7 @@ API.v1.addRoute('livechat/upload/:rid', { visitorToken, }; - const uploadedFile = Meteor.wrapAsync(fileStore.insert.bind(fileStore))(details, file.fileBuffer); + const uploadedFile = fileStore.insertSync(details, file.fileBuffer); if (!uploadedFile) { return API.v1.error('Invalid file'); }