element-ios/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m

706 lines
26 KiB
Objective-C

/*
Copyright 2018-2024 New Vector Ltd.
Copyright 2015 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
#import "MXKAttachment.h"
#import "MXKSwiftHeader.h"
@import MatrixSDK;
@import MobileCoreServices;
#import "MXKTools.h"
// The size of thumbnail we request from the server
// Note that this is smaller than the ones we upload: when sending, one size
// must fit all, including the web which will want relatively high res thumbnails.
// We, however, are a mobile client and so would prefer smaller thumbnails, which
// we can have if they're being generated by the media repo.
static const int kThumbnailWidth = 320;
static const int kThumbnailHeight = 240;
NSString *const kMXKAttachmentErrorDomain = @"kMXKAttachmentErrorDomain";
NSString *const kMXKAttachmentFileNameBase = @"attatchment";
@interface MXKAttachment ()
{
/**
The information on the encrypted content.
*/
MXEncryptedContentFile *contentFile;
/**
The information on the encrypted thumbnail.
*/
MXEncryptedContentFile *thumbnailFile;
/**
Observe Attachment download
*/
id onAttachmentDownloadObs;
/**
The local path used to store the attachment with its original name
*/
NSString *documentCopyPath;
/**
The attachment mimetype.
*/
NSString *mimetype;
}
@end
@implementation MXKAttachment
- (instancetype)initWithEvent:(MXEvent*)event andMediaManager:(MXMediaManager*)mediaManager
{
self = [super init];
if (self)
{
_mediaManager = mediaManager;
// Make a copy as the data can be read at anytime later
_eventId = event.eventId;
_eventRoomId = event.roomId;
_eventSentState = event.sentState;
NSDictionary *eventContent = event.content;
// Set default thumbnail orientation
_thumbnailOrientation = UIImageOrientationUp;
if (event.eventType == MXEventTypeSticker)
{
_type = MXKAttachmentTypeSticker;
MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
}
else
{
// Note: mxEvent.eventType is supposed to be MXEventTypeRoomMessage here.
NSString *msgtype = eventContent[kMXMessageTypeKey];
if ([msgtype isEqualToString:kMXMessageTypeImage])
{
_type = MXKAttachmentTypeImage;
}
else if (event.isVoiceMessage)
{
_type = MXKAttachmentTypeVoiceMessage;
}
else if ([msgtype isEqualToString:kMXMessageTypeAudio])
{
_type = MXKAttachmentTypeAudio;
}
else if ([msgtype isEqualToString:kMXMessageTypeVideo])
{
_type = MXKAttachmentTypeVideo;
MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
}
else if ([msgtype isEqualToString:kMXMessageTypeFile])
{
_type = MXKAttachmentTypeFile;
}
else
{
return nil;
}
}
MXJSONModelSetString(_originalFileName, eventContent[kMXMessageBodyKey]);
MXJSONModelSetDictionary(_contentInfo, eventContent[@"info"]);
MXJSONModelSetMXJSONModel(contentFile, MXEncryptedContentFile, eventContent[@"file"]);
// Retrieve the content url by taking into account the potential encryption.
if (contentFile)
{
_isEncrypted = YES;
_contentURL = contentFile.url;
MXJSONModelSetMXJSONModel(thumbnailFile, MXEncryptedContentFile, _contentInfo[@"thumbnail_file"]);
}
else
{
_isEncrypted = NO;
MXJSONModelSetString(_contentURL, eventContent[@"url"]);
}
mimetype = nil;
if (_contentInfo)
{
MXJSONModelSetString(mimetype, _contentInfo[@"mimetype"]);
}
_cacheFilePath = [MXMediaManager cachePathForMatrixContentURI:_contentURL andType:mimetype inFolder:_eventRoomId];
_downloadId = [MXMediaManager downloadIdForMatrixContentURI:_contentURL inFolder:_eventRoomId];
// Deduce the thumbnail information from the retrieved data.
_mxcThumbnailURI = [self getThumbnailURI];
_thumbnailMimeType = [self getThumbnailMimeType];
_thumbnailCachePath = [self getThumbnailCachePath];
_thumbnailDownloadId = [self getThumbnailDownloadId];
}
return self;
}
- (void)dealloc
{
[self destroy];
}
- (void)destroy
{
if (onAttachmentDownloadObs)
{
[[NSNotificationCenter defaultCenter] removeObserver:onAttachmentDownloadObs];
onAttachmentDownloadObs = nil;
}
// Remove the temporary file created to prepare attachment sharing
if (documentCopyPath)
{
[[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
documentCopyPath = nil;
}
_previewImage = nil;
}
- (NSString *)getThumbnailURI
{
if (thumbnailFile)
{
// there's an encrypted thumbnail: we return the mxc url
return thumbnailFile.url;
}
// Look for a clear thumbnail url
return _contentInfo[@"thumbnail_url"];
}
- (NSString *)getThumbnailMimeType
{
return _thumbnailInfo[@"mimetype"];
}
- (NSString*)getThumbnailCachePath
{
if (_mxcThumbnailURI)
{
return [MXMediaManager cachePathForMatrixContentURI:_mxcThumbnailURI andType:_thumbnailMimeType inFolder:_eventRoomId];
}
// In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
// the attachment is currently uploading.
// Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
{
return [MXMediaManager thumbnailCachePathForMatrixContentURI:_contentURL
andType:@"image/jpeg"
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale];
}
return nil;
}
- (NSString *)getThumbnailDownloadId
{
if (_mxcThumbnailURI)
{
return [MXMediaManager downloadIdForMatrixContentURI:_mxcThumbnailURI inFolder:_eventRoomId];
}
// In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
// the attachment is currently uploading.
// Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
{
return [MXMediaManager thumbnailDownloadIdForMatrixContentURI:_contentURL
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale];
}
return nil;
}
- (UIImage *)getCachedThumbnail
{
if (_thumbnailCachePath)
{
UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
if (thumb) return thumb;
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
return [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath];
}
}
return nil;
}
- (void)getThumbnail:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
// Check whether a thumbnail is defined.
if (!_thumbnailCachePath)
{
// there is no thumbnail: if we're an image, return the full size image. Otherwise, nothing we can do.
if (_type == MXKAttachmentTypeImage)
{
[self getImage:onSuccess failure:onFailure];
}
else if (onFailure)
{
onFailure(self, nil);
}
return;
}
// Check the current memory cache.
UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
if (thumb)
{
onSuccess(self, thumb);
return;
}
if (thumbnailFile)
{
MXWeakify(self);
void (^decryptAndCache)(void) = ^{
MXStrongifyAndReturnIfNil(self);
NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.thumbnailCachePath];
NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
[MXEncryptedAttachments decryptAttachment:self->thumbnailFile inputStream:instream outputStream:outstream success:^{
UIImage *img = [UIImage imageWithData:[outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]];
// Save this image to in-memory cache.
[MXMediaManager cacheImage:img withCachePath:self.thumbnailCachePath];
onSuccess(self, img);
} failure:^(NSError *err) {
if (err) {
MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
if (onFailure) onFailure(self, err);
return;
}
}];
};
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
decryptAndCache();
}
else
{
[_mediaManager downloadEncryptedMediaFromMatrixContentFile:thumbnailFile
mimeType:_thumbnailMimeType
inFolder:_eventRoomId
success:^(NSString *outputFilePath) {
decryptAndCache();
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
}
else
{
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath]);
}
else if (_mxcThumbnailURI)
{
[_mediaManager downloadMediaFromMatrixContentURI:_mxcThumbnailURI
withType:_thumbnailMimeType
inFolder:_eventRoomId
success:^(NSString *outputFilePath) {
// Here outputFilePath = thumbnailCachePath
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
else
{
// Here _thumbnailCachePath is defined, so a thumbnail is available.
// Because _mxcThumbnailURI is null, this means we have to consider the content uri (see getThumbnailCachePath).
[_mediaManager downloadThumbnailFromMatrixContentURI:_contentURL
withType:@"image/jpeg"
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale
success:^(NSString *outputFilePath) {
// Here outputFilePath = thumbnailCachePath
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
}
}
- (void)getImage:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
[self getAttachmentData:^(NSData *data) {
UIImage *img = [UIImage imageWithData:data];
if (img)
{
if (onSuccess)
{
onSuccess(self, img);
}
}
else
{
if (onFailure)
{
NSError *error = [NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_get_image_from_data"}];
onFailure(self, error);
}
}
} failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
- (void)getAttachmentData:(void (^)(NSData *))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
if (self.isEncrypted)
{
// decrypt the encrypted file
NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.cacheFilePath];
NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
[MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:instream outputStream:outstream success:^{
onSuccess([outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]);
} failure:^(NSError *err) {
if (err)
{
MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
return;
}
}];
}
else
{
onSuccess([NSData dataWithContentsOfFile:self.cacheFilePath]);
}
} failure:onFailure];
}
- (void)decryptToTempFile:(void (^)(NSString *))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
NSString *tempPath = [self getTempFile];
if (!tempPath)
{
if (onFailure) onFailure([NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_creating_temp_file"}]);
return;
}
NSInputStream *inStream = [NSInputStream inputStreamWithFileAtPath:self.cacheFilePath];
NSOutputStream *outStream = [NSOutputStream outputStreamToFileAtPath:tempPath append:NO];
[MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:inStream outputStream:outStream success:^{
onSuccess(tempPath);
} failure:^(NSError *err) {
if (err) {
if (onFailure) onFailure(err);
return;
}
}];
} failure:onFailure];
}
- (NSString *)getTempFile
{
// create a file with an appropriate extension because iOS detects based on file extension
// all over the place
NSString *ext = [MXTools fileExtensionFromContentType:mimetype];
NSString *filenameTemplate = [NSString stringWithFormat:@"%@.XXXXXX%@", kMXKAttachmentFileNameBase, ext];
NSString *template = [NSTemporaryDirectory() stringByAppendingPathComponent:filenameTemplate];
const char *templateCstr = [template fileSystemRepresentation];
char *tempPathCstr = (char *)malloc(strlen(templateCstr) + 1);
strcpy(tempPathCstr, templateCstr);
int fd = mkstemps(tempPathCstr, (int)ext.length);
if (!fd)
{
return nil;
}
close(fd);
NSString *tempPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempPathCstr
length:strlen(tempPathCstr)];
free(tempPathCstr);
return tempPath;
}
+ (void)clearCache
{
NSString *temporaryDirectoryPath = NSTemporaryDirectory();
NSDirectoryEnumerator<NSString *> *enumerator = [NSFileManager.defaultManager enumeratorAtPath:temporaryDirectoryPath];
NSString *filePath;
while (filePath = [enumerator nextObject]) {
if(![filePath containsString:kMXKAttachmentFileNameBase]) {
continue;
}
NSError *error;
BOOL result = [NSFileManager.defaultManager removeItemAtPath:[temporaryDirectoryPath stringByAppendingPathComponent:filePath] error:&error];
if (!result && error) {
MXLogErrorDetails(@"[MXKAttachment] Failed deleting temporary file with error", @{
@"error": error ?: @"unknown"
});
}
}
}
- (void)prepare:(void (^)(void))onAttachmentReady failure:(void (^)(NSError *error))onFailure
{
if ([[NSFileManager defaultManager] fileExistsAtPath:_cacheFilePath])
{
// Done
if (onAttachmentReady)
{
onAttachmentReady();
}
}
else
{
// Trigger download if it is not already in progress
MXMediaLoader* loader = [MXMediaManager existingDownloaderWithIdentifier:_downloadId];
if (!loader)
{
if (_isEncrypted)
{
loader = [_mediaManager downloadEncryptedMediaFromMatrixContentFile:contentFile
mimeType:mimetype
inFolder:_eventRoomId];
}
else
{
loader = [_mediaManager downloadMediaFromMatrixContentURI:_contentURL
withType:mimetype
inFolder:_eventRoomId];
}
}
if (loader)
{
MXWeakify(self);
// Add observers
onAttachmentDownloadObs = [[NSNotificationCenter defaultCenter] addObserverForName:kMXMediaLoaderStateDidChangeNotification object:loader queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
MXMediaLoader *loader = (MXMediaLoader*)notif.object;
switch (loader.state) {
case MXMediaLoaderStateDownloadCompleted:
[[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
self->onAttachmentDownloadObs = nil;
if (onAttachmentReady)
{
onAttachmentReady ();
}
break;
case MXMediaLoaderStateDownloadFailed:
case MXMediaLoaderStateCancelled:
[[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
self->onAttachmentDownloadObs = nil;
if (onFailure)
{
onFailure (loader.error);
}
break;
default:
break;
}
}];
}
else if (onFailure)
{
onFailure (nil);
}
}
}
- (void)save:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
if (_type == MXKAttachmentTypeImage || _type == MXKAttachmentTypeVideo)
{
MXWeakify(self);
if (self.isEncrypted) {
[self decryptToTempFile:^(NSString *path) {
MXStrongifyAndReturnIfNil(self);
NSURL* url = [NSURL fileURLWithPath:path];
[MXMediaManager saveMediaToPhotosLibrary:url
isImage:(self.type == MXKAttachmentTypeImage)
success:^(NSURL *assetURL){
if (onSuccess)
{
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
onSuccess();
}
}
failure:onFailure];
} failure:onFailure];
}
else
{
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
NSURL* url = [NSURL fileURLWithPath:self.cacheFilePath];
[MXMediaManager saveMediaToPhotosLibrary:url
isImage:(self.type == MXKAttachmentTypeImage)
success:^(NSURL *assetURL){
if (onSuccess)
{
onSuccess();
}
}
failure:onFailure];
} failure:onFailure];
}
}
else
{
// Not supported
if (onFailure)
{
onFailure(nil);
}
}
}
- (void)copy:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
if (self.type == MXKAttachmentTypeImage)
{
[self getImage:^(MXKAttachment *attachment, UIImage *img) {
MXKPasteboardManager.shared.pasteboard.image = img;
if (onSuccess)
{
onSuccess();
}
} failure:^(MXKAttachment *attachment, NSError *error) {
if (onFailure) onFailure(error);
}];
}
else
{
MXWeakify(self);
[self getAttachmentData:^(NSData *data) {
if (data)
{
MXStrongifyAndReturnIfNil(self);
NSString* UTI = (__bridge_transfer NSString *) UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[self.cacheFilePath pathExtension] , NULL);
if (UTI)
{
[MXKPasteboardManager.shared.pasteboard setData:data forPasteboardType:UTI];
if (onSuccess)
{
onSuccess();
}
}
}
} failure:onFailure];
}
// Unexpected error
if (onFailure)
{
onFailure(nil);
}
} failure:onFailure];
}
- (MXKUTI *)uti
{
return [[MXKUTI alloc] initWithMimeType:mimetype];
}
- (void)prepareShare:(void (^)(NSURL *fileURL))onReadyToShare failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
void (^haveFile)(NSString *) = ^(NSString *path) {
// Prepare the file URL by considering the original file name (if any)
NSURL *fileUrl;
MXStrongifyAndReturnIfNil(self);
// Check whether the original name retrieved from event body has extension
if (self.originalFileName && [self.originalFileName pathExtension].length)
{
// Copy the cached file to restore its original name
// Note: We used previously symbolic link (instead of copy) but UIDocumentInteractionController failed to open Office documents (.docx, .pptx...).
self->documentCopyPath = [[MXMediaManager getCachePath] stringByAppendingPathComponent:self.originalFileName];
[[NSFileManager defaultManager] removeItemAtPath:self->documentCopyPath error:nil];
if ([[NSFileManager defaultManager] copyItemAtPath:path toPath:self->documentCopyPath error:nil])
{
fileUrl = [NSURL fileURLWithPath:self->documentCopyPath];
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
}
if (!fileUrl)
{
// Use the cached file by default
fileUrl = [NSURL fileURLWithPath:path];
self->documentCopyPath = path;
}
onReadyToShare (fileUrl);
};
if (self.isEncrypted)
{
[self decryptToTempFile:^(NSString *path) {
haveFile(path);
} failure:onFailure];
}
else
{
// First download data if it is not already done
[self prepare:^{
haveFile(self.cacheFilePath);
} failure:onFailure];
}
}
- (void)onShareEnded
{
// Remove the temporary file created to prepare attachment sharing
if (documentCopyPath)
{
[[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
documentCopyPath = nil;
}
}
@end