From 40dcc7c873a53a2506d642981ef6aa035ccc2983 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 13 Apr 2017 12:54:03 -0400 Subject: [PATCH] Honor attachment filenames. // FREEBIE --- .../Attachments/OWSAttachmentsProcessor.m | 3 +- .../Attachments/TSAttachmentPointer.h | 4 +- .../Attachments/TSAttachmentPointer.m | 2 + src/Messages/Attachments/TSAttachmentStream.h | 2 + src/Messages/Attachments/TSAttachmentStream.m | 2 + .../DeviceSyncing/OWSSyncContactsMessage.m | 6 +- .../DeviceSyncing/OWSSyncGroupsMessage.m | 6 +- src/Messages/Interactions/TSMessage.h | 1 + src/Messages/Interactions/TSMessage.m | 1 - src/Messages/Interactions/TSOutgoingMessage.h | 7 ++- src/Messages/Interactions/TSOutgoingMessage.m | 19 +++++- src/Messages/OWSMessageSender.h | 1 + src/Messages/OWSMessageSender.m | 5 ++ src/Util/MIMETypeUtil.h | 10 +-- src/Util/MIMETypeUtil.m | 62 ++++++++++++++++++- 15 files changed, 113 insertions(+), 18 deletions(-) diff --git a/src/Messages/Attachments/OWSAttachmentsProcessor.m b/src/Messages/Attachments/OWSAttachmentsProcessor.m index 766a372b7..87a869a42 100644 --- a/src/Messages/Attachments/OWSAttachmentsProcessor.m +++ b/src/Messages/Attachments/OWSAttachmentsProcessor.m @@ -75,7 +75,8 @@ NS_ASSUME_NONNULL_BEGIN key:attachmentProto.key digest:digest contentType:attachmentProto.contentType - relay:relay]; + relay:relay + filename:attachmentProto.fileName]; [attachmentIds addObject:pointer.uniqueId]; diff --git a/src/Messages/Attachments/TSAttachmentPointer.h b/src/Messages/Attachments/TSAttachmentPointer.h index cb8143fdb..9daf32879 100644 --- a/src/Messages/Attachments/TSAttachmentPointer.h +++ b/src/Messages/Attachments/TSAttachmentPointer.h @@ -21,9 +21,11 @@ typedef NS_ENUM(NSUInteger, TSAttachmentPointerState) { key:(NSData *)key digest:(nullable NSData *)digest contentType:(NSString *)contentType - relay:(NSString *)relay NS_DESIGNATED_INITIALIZER; + relay:(NSString *)relay + filename:(nullable NSString *)filename NS_DESIGNATED_INITIALIZER; @property (nonatomic, readonly) NSString *relay; +@property (nonatomic, readonly, nullable) NSString *filename; @property (atomic) TSAttachmentPointerState state; // Though now required, `digest` may be null for pre-existing records or from diff --git a/src/Messages/Attachments/TSAttachmentPointer.m b/src/Messages/Attachments/TSAttachmentPointer.m index 73ca07a53..7097a204a 100644 --- a/src/Messages/Attachments/TSAttachmentPointer.m +++ b/src/Messages/Attachments/TSAttachmentPointer.m @@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN digest:(nullable NSData *)digest contentType:(NSString *)contentType relay:(NSString *)relay + filename:(nullable NSString *)filename { self = [super initWithServerId:serverId encryptionKey:key contentType:contentType]; if (!self) { @@ -39,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN _digest = digest; _state = TSAttachmentPointerStateEnqueued; _relay = relay; + _filename = filename; return self; } diff --git a/src/Messages/Attachments/TSAttachmentStream.h b/src/Messages/Attachments/TSAttachmentStream.h index 63b33bcd9..42bb0fcbe 100644 --- a/src/Messages/Attachments/TSAttachmentStream.h +++ b/src/Messages/Attachments/TSAttachmentStream.h @@ -24,6 +24,8 @@ NS_ASSUME_NONNULL_BEGIN // This only applies for attachments being uploaded. @property (atomic) BOOL isUploaded; +@property (nonatomic, readonly, nullable) NSString *filename; + #if TARGET_OS_IPHONE - (nullable UIImage *)image; #endif diff --git a/src/Messages/Attachments/TSAttachmentStream.m b/src/Messages/Attachments/TSAttachmentStream.m index 7ce6e5bf4..289549228 100644 --- a/src/Messages/Attachments/TSAttachmentStream.m +++ b/src/Messages/Attachments/TSAttachmentStream.m @@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN // state, but this constructor is used only for new incoming // attachments which don't need to be uploaded. _isUploaded = YES; + _filename = pointer.filename; return self; } @@ -115,6 +116,7 @@ NS_ASSUME_NONNULL_BEGIN { return [MIMETypeUtil filePathForAttachment:self.uniqueId ofMIMEType:self.contentType + filename:self.filename inFolder:[[self class] attachmentsFolder]]; } diff --git a/src/Messages/DeviceSyncing/OWSSyncContactsMessage.m b/src/Messages/DeviceSyncing/OWSSyncContactsMessage.m index 438ea8e9e..0f3266c92 100644 --- a/src/Messages/DeviceSyncing/OWSSyncContactsMessage.m +++ b/src/Messages/DeviceSyncing/OWSSyncContactsMessage.m @@ -1,4 +1,6 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "OWSSyncContactsMessage.h" #import "Contact.h" @@ -39,7 +41,7 @@ NS_ASSUME_NONNULL_BEGIN } OWSSignalServiceProtosAttachmentPointer *attachmentProto = - [self buildAttachmentProtoForAttachmentId:self.attachmentIds[0]]; + [self buildAttachmentProtoForAttachmentId:self.attachmentIds[0] filename:nil]; OWSSignalServiceProtosSyncMessageContactsBuilder *contactsBuilder = [OWSSignalServiceProtosSyncMessageContactsBuilder new]; diff --git a/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.m b/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.m index 87ffdcffe..084bb2e55 100644 --- a/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.m +++ b/src/Messages/DeviceSyncing/OWSSyncGroupsMessage.m @@ -1,4 +1,6 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "OWSSyncGroupsMessage.h" #import "NSDate+millisecondTimeStamp.h" @@ -25,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN (unsigned long)self.attachmentIds.count); } OWSSignalServiceProtosAttachmentPointer *attachmentProto = - [self buildAttachmentProtoForAttachmentId:self.attachmentIds[0]]; + [self buildAttachmentProtoForAttachmentId:self.attachmentIds[0] filename:nil]; OWSSignalServiceProtosSyncMessageGroupsBuilder *groupsBuilder = [OWSSignalServiceProtosSyncMessageGroupsBuilder new]; diff --git a/src/Messages/Interactions/TSMessage.h b/src/Messages/Interactions/TSMessage.h index 65716cda9..6dbf2062e 100644 --- a/src/Messages/Interactions/TSMessage.h +++ b/src/Messages/Interactions/TSMessage.h @@ -23,6 +23,7 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { @interface TSMessage : TSInteraction @property (nonatomic, readonly) NSMutableArray *attachmentIds; +// A map of attachment id-to-filename. @property (nullable, nonatomic) NSString *body; @property (nonatomic) TSGroupMetaMessage groupMetaMessage; @property (nonatomic) uint32_t expiresInSeconds; diff --git a/src/Messages/Interactions/TSMessage.m b/src/Messages/Interactions/TSMessage.m index 92e4a784f..e98b6b063 100644 --- a/src/Messages/Interactions/TSMessage.m +++ b/src/Messages/Interactions/TSMessage.m @@ -128,7 +128,6 @@ static const NSUInteger OWSMessageSchemaVersion = 3; } if (!_attachmentIds) { - // previously allowed nil _attachmentIds _attachmentIds = [NSMutableArray new]; } diff --git a/src/Messages/Interactions/TSOutgoingMessage.h b/src/Messages/Interactions/TSOutgoingMessage.h index b58d72d95..62ebf53fd 100644 --- a/src/Messages/Interactions/TSOutgoingMessage.h +++ b/src/Messages/Interactions/TSOutgoingMessage.h @@ -57,6 +57,7 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) { @property BOOL hasSyncedTranscript; @property NSString *customMessage; @property (atomic, readonly) NSString *mostRecentFailureText; +@property (nonatomic, readonly) NSMutableDictionary *attachmentFilenameMap; /** * Whether the message should be serialized as a modern aka Content, or the old style legacy message. @@ -94,10 +95,14 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) { * @param attachmentId * id of an AttachmentStream containing the meta data used when populating the attachment proto * + * @param filename + * optional filename of the attachment. + * * @return * An attachment pointer protobuf suitable for including in various container protobuf builders */ -- (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId; +- (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId + filename:(nullable NSString *)filename; @end diff --git a/src/Messages/Interactions/TSOutgoingMessage.m b/src/Messages/Interactions/TSOutgoingMessage.m index b53014ccb..832cb40ce 100644 --- a/src/Messages/Interactions/TSOutgoingMessage.m +++ b/src/Messages/Interactions/TSOutgoingMessage.m @@ -15,7 +15,13 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithCoder:(NSCoder *)coder { - return [super initWithCoder:coder]; + self = [super initWithCoder:coder]; + if (self) { + if (!_attachmentFilenameMap) { + _attachmentFilenameMap = [NSMutableDictionary new]; + } + } + return self; } - (instancetype)initWithTimestamp:(uint64_t)timestamp @@ -86,6 +92,7 @@ NS_ASSUME_NONNULL_BEGIN } else { self.groupMetaMessage = TSGroupMessageNone; } + _attachmentFilenameMap = [NSMutableDictionary new]; OWSAssert(self.receivedAtDate); @@ -142,7 +149,8 @@ NS_ASSUME_NONNULL_BEGIN case TSGroupMessageNew: { if (gThread.groupModel.groupImage != nil && self.attachmentIds.count == 1) { attachmentWasGroupAvatar = YES; - [groupBuilder setAvatar:[self buildAttachmentProtoForAttachmentId:self.attachmentIds[0]]]; + [groupBuilder + setAvatar:[self buildAttachmentProtoForAttachmentId:self.attachmentIds[0] filename:nil]]; } [groupBuilder setMembersArray:gThread.groupModel.groupMemberIds]; @@ -160,7 +168,8 @@ NS_ASSUME_NONNULL_BEGIN if (!attachmentWasGroupAvatar) { NSMutableArray *attachments = [NSMutableArray new]; for (NSString *attachmentId in self.attachmentIds) { - [attachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId]]; + NSString *filename = self.attachmentFilenameMap[attachmentId]; + [attachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId filename:filename]]; } [builder setAttachmentsArray:attachments]; } @@ -191,7 +200,10 @@ NS_ASSUME_NONNULL_BEGIN } - (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId + filename:(nullable NSString *)filename { + OWSAssert(attachmentId.length > 0); + TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentId]; if (![attachment isKindOfClass:[TSAttachmentStream class]]) { DDLogError(@"Unexpected type for attachment builder: %@", attachment); @@ -202,6 +214,7 @@ NS_ASSUME_NONNULL_BEGIN OWSSignalServiceProtosAttachmentPointerBuilder *builder = [OWSSignalServiceProtosAttachmentPointerBuilder new]; [builder setId:attachmentStream.serverId]; [builder setContentType:attachmentStream.contentType]; + [builder setFileName:filename]; [builder setKey:attachmentStream.encryptionKey]; [builder setDigest:attachmentStream.digest]; diff --git a/src/Messages/OWSMessageSender.h b/src/Messages/OWSMessageSender.h index 371c54ddc..267b2fa45 100644 --- a/src/Messages/OWSMessageSender.h +++ b/src/Messages/OWSMessageSender.h @@ -55,6 +55,7 @@ NS_SWIFT_NAME(MessageSender) */ - (void)sendAttachmentData:(NSData *)attachmentData contentType:(NSString *)contentType + filename:(nullable NSString *)filename inMessage:(TSOutgoingMessage *)outgoingMessage success:(void (^)())successHandler failure:(void (^)(NSError *error))failureHandler; diff --git a/src/Messages/OWSMessageSender.m b/src/Messages/OWSMessageSender.m index efd0e0284..24f28a66c 100644 --- a/src/Messages/OWSMessageSender.m +++ b/src/Messages/OWSMessageSender.m @@ -437,6 +437,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [self sendAttachmentData:attachmentData contentType:contentType + filename:nil inMessage:message success:successWithDeleteHandler failure:failureWithDeleteHandler]; @@ -444,6 +445,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; - (void)sendAttachmentData:(NSData *)data contentType:(NSString *)contentType + filename:(nullable NSString *)filename inMessage:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(void (^)(NSError *error))failureHandler @@ -467,6 +469,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [attachmentStream save]; [message.attachmentIds addObject:attachmentStream.uniqueId]; + if (filename) { + message.attachmentFilenameMap[attachmentStream.uniqueId] = filename; + } [message save]; dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/src/Util/MIMETypeUtil.h b/src/Util/MIMETypeUtil.h index 82d463c9c..8ca864fe6 100644 --- a/src/Util/MIMETypeUtil.h +++ b/src/Util/MIMETypeUtil.h @@ -29,11 +29,11 @@ extern NSString *const OWSMimeTypeUnknownForTests; + (BOOL)isVideo:(NSString *)contentType; + (BOOL)isAudio:(NSString *)contentType; -+ (NSString *)filePathForAttachment:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; -+ (NSString *)filePathForImage:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; -+ (NSString *)filePathForVideo:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; -+ (NSString *)filePathForAudio:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; -+ (NSString *)filePathForAnimated:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; +// filename is optional and should not be trusted. ++ (NSString *)filePathForAttachment:(NSString *)uniqueId + ofMIMEType:(NSString *)contentType + filename:(nullable NSString *)filename + inFolder:(NSString *)folder; + (NSURL *)simLinkCorrectExtensionOfFile:(NSURL *)mediaURL ofMIMEType:(NSString *)contentType; diff --git a/src/Util/MIMETypeUtil.m b/src/Util/MIMETypeUtil.m index d9c38c765..a823e8474 100644 --- a/src/Util/MIMETypeUtil.m +++ b/src/Util/MIMETypeUtil.m @@ -261,7 +261,65 @@ NSString *const OWSMimeTypeUnknownForTests = @"unknown/mimetype"; + (NSString *)filePathForAttachment:(NSString *)uniqueId ofMIMEType:(NSString *)contentType - inFolder:(NSString *)folder { + filename:(nullable NSString *)filename + inFolder:(NSString *)folder +{ + NSString *kDefaultFileExtension = @"bin"; + + if (filename.length > 0) { + NSString *normalizedFilename = + [filename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + // Ensure that the filename is a valid filesystem name, + // replacing invalid characters with an underscore. + for (NSCharacterSet *invalidCharacterSet in @[ + [NSCharacterSet whitespaceCharacterSet], + [NSCharacterSet newlineCharacterSet], + [NSCharacterSet illegalCharacterSet], + [NSCharacterSet controlCharacterSet], + [NSCharacterSet characterSetWithCharactersInString:@"<>|\\:()&;?*"], + ]) { + normalizedFilename = [[normalizedFilename componentsSeparatedByCharactersInSet:invalidCharacterSet] + componentsJoinedByString:@"_"]; + } + + NSString *fileExtension = [[normalizedFilename pathExtension] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSString *filenameWithoutExtension = [[[normalizedFilename lastPathComponent] stringByDeletingPathExtension] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + // If the filename has not file extension, deduce one + // from the MIME type. + if (fileExtension.length < 1) { + fileExtension = [self fileExtensionForMIMEType:contentType]; + if (fileExtension.length < 1) { + fileExtension = kDefaultFileExtension; + } + } + fileExtension = [fileExtension lowercaseString]; + + if (filenameWithoutExtension.length > 0) { + // Store the file in a subdirectory whose name is the uniqueId of this attachment, + // to avoid collisions between multiple attachments with the same name. + NSString *attachmentFolderPath = [folder stringByAppendingPathComponent:uniqueId]; + NSError *error = nil; + BOOL attachmentFolderPathExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFolderPath]; + if (!attachmentFolderPathExists) { + [[NSFileManager defaultManager] createDirectoryAtPath:attachmentFolderPath + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + DDLogError(@"Failed to create attachment directory: %@", error); + OWSAssert(0); + return nil; + } + } + return [attachmentFolderPath + stringByAppendingPathComponent:[NSString + stringWithFormat:@"%@.%@", filenameWithoutExtension, fileExtension]]; + } + } + if ([self isVideo:contentType]) { return [MIMETypeUtil filePathForVideo:uniqueId ofMIMEType:contentType inFolder:folder]; } else if ([self isAudio:contentType]) { @@ -291,7 +349,7 @@ NSString *const OWSMimeTypeUnknownForTests = @"unknown/mimetype"; DDLogError(@"Got asked for path of file %@ which is unsupported", contentType); // Use a fallback file extension. - return [self filePathForData:uniqueId withFileExtension:@"bin" inFolder:folder]; + return [self filePathForData:uniqueId withFileExtension:kDefaultFileExtension inFolder:folder]; } + (NSURL *)simLinkCorrectExtensionOfFile:(NSURL *)mediaURL ofMIMEType:(NSString *)contentType {