You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/js/modules/types/attachment.js

327 lines
9.2 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

const is = require('@sindresorhus/is');
const AttachmentTS = require('../../../ts/types/Attachment');
const GoogleChrome = require('../../../ts/util/GoogleChrome');
const MIME = require('../../../ts/types/MIME');
const { toLogFormat } = require('./errors');
const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util');
const { autoOrientImage } = require('../auto_orient_image');
const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_system');
// // Incoming message attachment fields
// {
// id: string
// contentType: MIMEType
// data: ArrayBuffer
// digest: ArrayBuffer
// fileName?: string
// flags: null
// key: ArrayBuffer
// size: integer
// thumbnail: ArrayBuffer
// }
// // Outgoing message attachment fields
// {
// contentType: MIMEType
// data: ArrayBuffer
// fileName: string
// size: integer
// }
// Returns true if `rawAttachment` is a valid attachment based on our current schema.
// Over time, we can expand this definition to become more narrow, e.g. require certain
// fields, etc.
exports.isValid = rawAttachment => {
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
// deserialized by protobuf:
if (!rawAttachment) {
return false;
}
return true;
};
// Upgrade steps
// NOTE: This step strips all EXIF metadata from JPEG images as
// part of re-encoding the image:
exports.autoOrientJPEG = async attachment => {
if (!MIME.isJPEG(attachment.contentType)) {
return attachment;
}
// If we haven't downloaded the attachment yet, we won't have the data
if (!attachment.data) {
return attachment;
}
const dataBlob = await arrayBufferToBlob(attachment.data, attachment.contentType);
const newDataBlob = await dataURLToBlob(await autoOrientImage(dataBlob));
const newDataArrayBuffer = await blobToArrayBuffer(newDataBlob);
// IMPORTANT: We overwrite the existing `data` `ArrayBuffer` losing the original
// image data. Ideally, wed preserve the original image data for users who want to
// retain it but due to reports of data loss, we dont want to overburden IndexedDB
// by potentially doubling stored image data.
// See: https://github.com/signalapp/Signal-Desktop/issues/1589
const newAttachment = Object.assign({}, attachment, {
data: newDataArrayBuffer,
size: newDataArrayBuffer.byteLength,
});
// `digest` is no longer valid for auto-oriented image data, so we discard it:
delete newAttachment.digest;
return newAttachment;
};
const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D';
const UNICODE_RIGHT_TO_LEFT_OVERRIDE = '\u202E';
const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
const INVALID_CHARACTERS_PATTERN = new RegExp(
`[${UNICODE_LEFT_TO_RIGHT_OVERRIDE}${UNICODE_RIGHT_TO_LEFT_OVERRIDE}]`,
'g'
);
// NOTE: Expose synchronous version to do property-based testing using `testcheck`,
// which currently doesnt support async testing:
// https://github.com/leebyron/testcheck-js/issues/45
exports._replaceUnicodeOrderOverridesSync = attachment => {
if (!is.string(attachment.fileName)) {
return attachment;
}
const normalizedFilename = attachment.fileName.replace(
INVALID_CHARACTERS_PATTERN,
UNICODE_REPLACEMENT_CHARACTER
);
const newAttachment = Object.assign({}, attachment, {
fileName: normalizedFilename,
});
return newAttachment;
};
exports.replaceUnicodeOrderOverrides = async attachment =>
exports._replaceUnicodeOrderOverridesSync(attachment);
// \u202A-\u202E is LRE, RLE, PDF, LRO, RLO
// \u2066-\u2069 is LRI, RLI, FSI, PDI
// \u200E is LRM
// \u200F is RLM
// \u061C is ALM
const V2_UNWANTED_UNICODE = /[\u202A-\u202E\u2066-\u2069\u200E\u200F\u061C]/g;
exports.replaceUnicodeV2 = async attachment => {
if (!is.string(attachment.fileName)) {
return attachment;
}
const fileName = attachment.fileName.replace(V2_UNWANTED_UNICODE, UNICODE_REPLACEMENT_CHARACTER);
return {
...attachment,
fileName,
};
};
exports.removeSchemaVersion = ({ attachment, logger }) => {
if (!exports.isValid(attachment)) {
logger.error('Attachment.removeSchemaVersion: Invalid input attachment:', attachment);
return attachment;
}
const attachmentWithoutSchemaVersion = Object.assign({}, attachment);
delete attachmentWithoutSchemaVersion.schemaVersion;
return attachmentWithoutSchemaVersion;
};
exports.migrateDataToFileSystem = migrateDataToFileSystem;
// hasData :: Attachment -> Boolean
exports.hasData = attachment =>
attachment.data instanceof ArrayBuffer || ArrayBuffer.isView(attachment.data);
// loadData :: (RelativePath -> IO (Promise ArrayBuffer))
// Attachment ->
// IO (Promise Attachment)
exports.loadData = readAttachmentData => {
if (!is.function(readAttachmentData)) {
throw new TypeError("'readAttachmentData' must be a function");
}
return async attachment => {
if (!exports.isValid(attachment)) {
throw new TypeError("'attachment' is not valid");
}
const isAlreadyLoaded = exports.hasData(attachment);
if (isAlreadyLoaded) {
return attachment;
}
if (!is.string(attachment.path)) {
throw new TypeError("'attachment.path' is required");
}
const data = await readAttachmentData(attachment.path);
return Object.assign({}, attachment, { data });
};
};
// deleteData :: (RelativePath -> IO Unit)
// Attachment ->
// IO Unit
exports.deleteData = deleteOnDisk => {
if (!is.function(deleteOnDisk)) {
throw new TypeError('deleteData: deleteOnDisk must be a function');
}
return async attachment => {
if (!exports.isValid(attachment)) {
throw new TypeError('deleteData: attachment is not valid');
}
const { path, thumbnail, screenshot } = attachment;
if (is.string(path)) {
await deleteOnDisk(path);
}
if (thumbnail && is.string(thumbnail.path)) {
await deleteOnDisk(thumbnail.path);
}
if (screenshot && is.string(screenshot.path)) {
await deleteOnDisk(screenshot.path);
}
};
};
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
exports.save = AttachmentTS.save;
exports.getFileExtension = AttachmentTS.getFileExtension;
exports.arrayBufferFromFile = AttachmentTS.arrayBufferFromFile;
const THUMBNAIL_SIZE = 150;
const THUMBNAIL_CONTENT_TYPE = 'image/png';
exports.captureDimensionsAndScreenshot = async (
attachment,
{
writeNewAttachmentData,
getAbsoluteAttachmentPath,
makeObjectUrl,
revokeObjectUrl,
getImageDimensions,
makeImageThumbnail,
makeVideoScreenshot,
logger,
}
) => {
const { contentType } = attachment;
if (
!GoogleChrome.isImageTypeSupported(contentType) &&
!GoogleChrome.isVideoTypeSupported(contentType)
) {
return attachment;
}
// If the attachment hasn't been downloaded yet, we won't have a path
if (!attachment.path) {
return attachment;
}
const absolutePath = await getAbsoluteAttachmentPath(attachment.path);
if (GoogleChrome.isImageTypeSupported(contentType)) {
try {
const { width, height } = await getImageDimensions({
objectUrl: absolutePath,
logger,
});
const thumbnailBuffer = await blobToArrayBuffer(
await makeImageThumbnail({
size: THUMBNAIL_SIZE,
objectUrl: absolutePath,
contentType: THUMBNAIL_CONTENT_TYPE,
logger,
})
);
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
return {
...attachment,
width,
height,
thumbnail: {
path: thumbnailPath,
contentType: THUMBNAIL_CONTENT_TYPE,
width: THUMBNAIL_SIZE,
height: THUMBNAIL_SIZE,
},
};
} catch (error) {
logger.error(
'captureDimensionsAndScreenshot:',
'error processing image; skipping screenshot generation',
toLogFormat(error)
);
return attachment;
}
}
let screenshotObjectUrl;
try {
const screenshotBuffer = await blobToArrayBuffer(
await makeVideoScreenshot({
objectUrl: absolutePath,
contentType: THUMBNAIL_CONTENT_TYPE,
logger,
})
);
screenshotObjectUrl = makeObjectUrl(screenshotBuffer, THUMBNAIL_CONTENT_TYPE);
const { width, height } = await getImageDimensions({
objectUrl: screenshotObjectUrl,
logger,
});
const screenshotPath = await writeNewAttachmentData(screenshotBuffer);
const thumbnailBuffer = await blobToArrayBuffer(
await makeImageThumbnail({
size: THUMBNAIL_SIZE,
objectUrl: screenshotObjectUrl,
contentType: THUMBNAIL_CONTENT_TYPE,
logger,
})
);
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
return {
...attachment,
screenshot: {
contentType: THUMBNAIL_CONTENT_TYPE,
path: screenshotPath,
width,
height,
},
thumbnail: {
path: thumbnailPath,
contentType: THUMBNAIL_CONTENT_TYPE,
width: THUMBNAIL_SIZE,
height: THUMBNAIL_SIZE,
},
width,
height,
};
} catch (error) {
logger.error(
'captureDimensionsAndScreenshot: error processing video; skipping screenshot generation',
toLogFormat(error)
);
return attachment;
} finally {
revokeObjectUrl(screenshotObjectUrl);
}
};