[NEW] Remove exif metadata from uploaded files (#22044)

pull/21539/head
Kevin Aleman 5 years ago committed by GitHub
parent f1a500ae6b
commit 1600bcb6bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      app/api/server/v1/rooms.js
  2. 3
      app/emoji-custom/server/methods/uploadEmojiCustom.js
  3. 5
      app/lib/server/startup/settings.js
  4. 32
      package-lock.json
  5. 1
      package.json
  6. 2
      packages/rocketchat-i18n/i18n/en.i18n.json
  7. 4
      server/sdk/types/IMediaService.ts
  8. 52
      server/services/image/service.ts

@ -7,6 +7,8 @@ import { API } from '../api';
import { findAdminRooms, findChannelAndPrivateAutocomplete, findAdminRoom, findRoomsAvailableForTeams } from '../lib/rooms';
import { sendFile, sendViaEmail } from '../../../../server/lib/channelExport';
import { canAccessRoom, hasPermission } from '../../../authorization/server';
import { Media } from '../../../../server/sdk';
import { settings } from '../../../settings/server/index';
function findRoomByIdOrName({ params, checkedArchived = true }) {
if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) {
@ -120,7 +122,12 @@ API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, {
};
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;

@ -39,7 +39,8 @@ Meteor.methods({
if (['gif', 'x-icon', 'bmp', 'webm'].includes(emojiData.extension)) {
fileBuffer = file;
} else {
const { data: resizedEmojiBuffer } = await Media.resizeFromBuffer(file, 128, 128, true, false, false, 'inside');
// This is to support the idea of having "sticker-like" emojis
const { data: resizedEmojiBuffer } = await Media.resizeFromBuffer(file, 512, 512, true, false, false, 'inside');
fileBuffer = resizedEmojiBuffer;
}

@ -1037,6 +1037,11 @@ settings.addGroup('Message', function() {
public: true,
i18nDescription: 'Message_Attachments_GroupAttachDescription',
});
this.add('Message_Attachments_Strip_Exif', false, {
type: 'boolean',
public: true,
i18nDescription: 'Message_Attachments_Strip_ExifDescription',
});
});
this.section('Message_Audio', function() {
this.add('Message_AudioRecorderEnabled', true, {

32
package-lock.json generated

@ -13664,8 +13664,7 @@
"@types/chai": {
"version": "4.2.12",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz",
"integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==",
"dev": true
"integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ=="
},
"@types/chai-spies": {
"version": "1.0.1",
@ -14036,8 +14035,7 @@
"@types/mocha": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.3.tgz",
"integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==",
"dev": true
"integrity": "sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg=="
},
"@types/mock-require": {
"version": "2.0.0",
@ -14297,6 +14295,14 @@
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
"dev": true
},
"@types/stream-buffers": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.3.tgz",
"integrity": "sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ==",
"requires": {
"@types/node": "*"
}
},
"@types/string-strip-html": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/string-strip-html/-/string-strip-html-5.0.0.tgz",
@ -21846,6 +21852,24 @@
}
}
},
"exif-be-gone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/exif-be-gone/-/exif-be-gone-1.2.0.tgz",
"integrity": "sha512-FIfhEo2jJwXX94iLIONdxm2koKKarpwN6E8wMk8nfgTjRFZySMZRoWzWUvUEaTK+L6iAOzHpSNv4mgdL1JlLdQ==",
"requires": {
"@types/chai": "^4.2.12",
"@types/mocha": "^8.0.2",
"@types/node": "^14.0.27",
"@types/stream-buffers": "^3.0.3"
},
"dependencies": {
"@types/node": {
"version": "14.14.45",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.45.tgz",
"integrity": "sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw=="
}
}
},
"exit-hook": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",

@ -200,6 +200,7 @@
"emojione": "^4.5.0",
"eslint-plugin-import": "^2.22.0",
"eventemitter3": "^4.0.7",
"exif-be-gone": "^1.2.0",
"express": "^4.17.1",
"express-rate-limit": "^5.1.3",
"fflate": "^0.5.3",

@ -2718,6 +2718,8 @@
"Message_Attachments": "Message Attachments",
"Message_Attachments_GroupAttach": "Group Attachment Buttons",
"Message_Attachments_GroupAttachDescription": "This groups the icons under an expandable menu. Takes up less screen space.",
"Message_Attachments_Strip_Exif": "Remove EXIF metadata from supported files",
"Message_Attachments_Strip_ExifDescription": "Strips out EXIF metadata from image files (jpeg, tiff, etc). This setting is not retroactive, so files uploaded while disabled will have EXIF data",
"Message_Audio": "Audio Message",
"Message_Audio_bitRate": "Audio Message Bit Rate",
"Message_AudioRecorderEnabled": "Audio Recorder Enabled",

@ -1,4 +1,4 @@
import { Readable } from 'stream';
import { Readable, Stream } from 'stream';
import sharp from 'sharp';
@ -12,4 +12,6 @@ export interface IMediaService {
resizeFromBuffer(input: Buffer, width: number, height: number, keepType: boolean, blur: boolean, enlarge: boolean, fit?: keyof sharp.FitEnum | undefined): Promise<ResizeResult>;
resizeFromStream(input: Readable, width: number, height: number, keepType: boolean, blur: boolean, enlarge: boolean, fit?: keyof sharp.FitEnum | undefined): Promise<ResizeResult>;
isImage(buff: Buffer): boolean;
stripExifFromImageStream(stream: Stream): Readable;
stripExifFromBuffer(buffer: Buffer): Promise<Buffer>;
}

@ -1,4 +1,4 @@
import { Readable } from 'stream';
import stream, { Readable } from 'stream';
import fileType from 'file-type';
import sharp from 'sharp';
@ -7,6 +7,11 @@ import isSvg from 'is-svg';
import { ServiceClass } from '../../sdk/types/ServiceClass';
import { IMediaService, ResizeResult } from '../../sdk/types/IMediaService';
/* eslint-disable @typescript-eslint/no-var-requires */
const ExifTransformer = require('exif-be-gone');
/* eslint-enable @typescript-eslint/no-var-requires */
export class MediaService extends ServiceClass implements IMediaService {
protected name = 'media';
@ -32,27 +37,11 @@ export class MediaService extends ServiceClass implements IMediaService {
]);
async resizeFromBuffer(input: Buffer, width: number, height: number, keepType: boolean, blur: boolean, enlarge: boolean, fit?: keyof sharp.FitEnum | undefined): Promise<ResizeResult> {
const transformer = sharp(input)
.resize({ width, height, fit, withoutEnlargement: !enlarge });
if (!keepType) {
transformer.jpeg();
}
if (blur) {
transformer.blur();
}
const { data, info: { width: widthInfo, height: heightInfo } } = await transformer.toBuffer({ resolveWithObject: true });
return {
data,
width: widthInfo,
height: heightInfo,
};
const stream = this.bufferToStream(input);
return this.resizeFromStream(stream, width, height, keepType, blur, enlarge, fit);
}
async resizeFromStream(input: Readable, width: number, height: number, keepType: boolean, blur: boolean, enlarge: boolean, fit?: keyof sharp.FitEnum | undefined): Promise<ResizeResult> {
async resizeFromStream(input: stream.Stream, width: number, height: number, keepType: boolean, blur: boolean, enlarge: boolean, fit?: keyof sharp.FitEnum | undefined): Promise<ResizeResult> {
const transformer = sharp()
.resize({ width, height, fit, withoutEnlargement: !enlarge });
@ -86,4 +75,27 @@ export class MediaService extends ServiceClass implements IMediaService {
isSvgImage(buff: Buffer): boolean {
return isSvg(buff);
}
stripExifFromBuffer(buffer: Buffer): Promise<Buffer> {
return this.streamToBuffer(this.stripExifFromImageStream(this.bufferToStream(buffer)));
}
stripExifFromImageStream(stream: stream.Stream): Readable {
return stream.pipe(new ExifTransformer());
}
private bufferToStream(buffer: Buffer): stream.PassThrough {
const bufferStream = new stream.PassThrough();
bufferStream.end(buffer);
return bufferStream;
}
private streamToBuffer(stream: stream.Stream): Promise<Buffer> {
return new Promise((resolve) => {
const chunks: Array<Buffer> = [];
stream
.on('data', (data) => chunks.push(data))
.on('end', () => resolve(Buffer.concat(chunks)));
});
}
}

Loading…
Cancel
Save