Merge branch 'mkirk/images' into release/2.29.1

pull/1/head
Michael Kirk 7 years ago
commit e7c70db38c

@ -1 +1 @@
Subproject commit 4c3935aa74dfe52f047d664197bbf3f9d7b50c86 Subproject commit 9a3f6876d4a6086d10501383b96fb2d9d47a75b6

@ -12,6 +12,7 @@
#import <AssetsLibrary/AssetsLibrary.h> #import <AssetsLibrary/AssetsLibrary.h>
#import <SignalMessaging/NSString+OWS.h> #import <SignalMessaging/NSString+OWS.h>
#import <SignalMessaging/OWSUnreadIndicator.h> #import <SignalMessaging/OWSUnreadIndicator.h>
#import <SignalServiceKit/NSData+Image.h>
#import <SignalServiceKit/OWSContact.h> #import <SignalServiceKit/OWSContact.h>
#import <SignalServiceKit/TSInteraction.h> #import <SignalServiceKit/TSInteraction.h>
@ -482,8 +483,20 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if ([self.attachmentStream isAnimated]) { if ([self.attachmentStream isAnimated]) {
self.messageCellType = OWSMessageCellType_AnimatedImage; self.messageCellType = OWSMessageCellType_AnimatedImage;
} else if ([self.attachmentStream isImage]) { } else if ([self.attachmentStream isImage]) {
if (![self.attachmentStream isValidImage]) {
DDLogWarn(@"Treating invalid image as generic attachment.");
self.messageCellType = OWSMessageCellType_GenericAttachment;
return;
}
self.messageCellType = OWSMessageCellType_StillImage; self.messageCellType = OWSMessageCellType_StillImage;
} else if ([self.attachmentStream isVideo]) { } else if ([self.attachmentStream isVideo]) {
if (![self.attachmentStream isValidVideo]) {
DDLogWarn(@"Treating invalid video as generic attachment.");
self.messageCellType = OWSMessageCellType_GenericAttachment;
return;
}
self.messageCellType = OWSMessageCellType_Video; self.messageCellType = OWSMessageCellType_Video;
} else { } else {
OWSFail(@"%@ unexpected attachment type.", self.logTag); OWSFail(@"%@ unexpected attachment type.", self.logTag);

@ -196,6 +196,11 @@ class GifPickerCell: UICollectionViewCell {
clearViewState() clearViewState()
return return
} }
guard NSData.ows_isValidImage(atPath: asset.filePath, mimeType: OWSMimeTypeImageGif) else {
owsFail("\(logTag) invalid asset.")
clearViewState()
return
}
guard let image = YYImage(contentsOfFile: asset.filePath) else { guard let image = YYImage(contentsOfFile: asset.filePath) else {
owsFail("\(logTag) could not load asset.") owsFail("\(logTag) could not load asset.")
clearViewState() clearViewState()

@ -207,6 +207,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
} }
private func createImagePreview() { private func createImagePreview() {
guard attachment.isValidImage else {
createGenericPreview()
return
}
guard let image = attachment.image() else { guard let image = attachment.image() else {
createGenericPreview() createGenericPreview()
return return
@ -225,6 +229,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
} }
private func createVideoPreview() { private func createVideoPreview() {
guard attachment.isValidVideo else {
createGenericPreview()
return
}
guard let image = attachment.videoPreview() else { guard let image = attachment.videoPreview() else {
createGenericPreview() createGenericPreview()
return return

@ -141,6 +141,11 @@ public class SignalAttachment: NSObject {
return dataSource.isValidImage() return dataSource.isValidImage()
} }
@objc
public var isValidVideo: Bool {
return dataSource.isValidVideo()
}
// This flag should be set for text attachments that can be sent as text messages. // This flag should be set for text attachments that can be sent as text messages.
@objc @objc
public var isConvertibleToTextMessage = false public var isConvertibleToTextMessage = false

@ -198,7 +198,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
avatarImage = [self.cnContactAvatarCache objectForKey:contactId]; avatarImage = [self.cnContactAvatarCache objectForKey:contactId];
if (!avatarImage) { if (!avatarImage) {
NSData *_Nullable avatarData = [self avatarDataForCNContactId:contactId]; NSData *_Nullable avatarData = [self avatarDataForCNContactId:contactId];
if (avatarData) { if (avatarData && [avatarData ows_isValidImage]) {
avatarImage = [UIImage imageWithData:avatarData]; avatarImage = [UIImage imageWithData:avatarData];
} }
if (avatarImage) { if (avatarImage) {

@ -77,9 +77,10 @@ NS_ASSUME_NONNULL_BEGIN
// Non-nil for attachments which need "lazy backup restore." // Non-nil for attachments which need "lazy backup restore."
- (nullable OWSBackupFragment *)lazyRestoreFragment; - (nullable OWSBackupFragment *)lazyRestoreFragment;
#pragma mark - Image Validation #pragma mark - Validation
- (BOOL)isValidImage; - (BOOL)isValidImage;
- (BOOL)isValidVideo;
#pragma mark - Update With... Methods #pragma mark - Update With... Methods

@ -14,6 +14,8 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
const CGFloat kMaxVideoStillSize = 1 * 1024;
@interface TSAttachmentStream () @interface TSAttachmentStream ()
// We only want to generate the file path for this attachment once, so that // We only want to generate the file path for this attachment once, so that
@ -315,19 +317,18 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Image Validation #pragma mark - Image Validation
- (BOOL)isValidImageWithData:(NSData *)data - (BOOL)isValidImage
{ {
OWSAssert(self.isImage || self.isAnimated); OWSAssert(self.isImage || self.isAnimated);
OWSAssert(data);
return [data ows_isValidImageWithMimeType:self.contentType]; return [NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType];
} }
- (BOOL)isValidImage - (BOOL)isValidVideo
{ {
OWSAssert(self.isImage || self.isAnimated); OWSAssert(self.isVideo);
return [NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType]; return [NSData ows_isValidVideoAtURL:self.mediaURL];
} }
#pragma mark - #pragma mark -
@ -341,11 +342,10 @@ NS_ASSUME_NONNULL_BEGIN
if (!mediaUrl) { if (!mediaUrl) {
return nil; return nil;
} }
NSData *data = [NSData dataWithContentsOfURL:mediaUrl]; if (![self isValidImage]) {
if (![self isValidImageWithData:data]) {
return nil; return nil;
} }
return [UIImage imageWithData:data]; return [[UIImage alloc] initWithContentsOfFile:self.filePath];
} else { } else {
return nil; return nil;
} }
@ -362,17 +362,12 @@ NS_ASSUME_NONNULL_BEGIN
return nil; return nil;
} }
NSURL *_Nullable mediaUrl = [self mediaURL]; if (![NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType]) {
if (!mediaUrl) { OWSFail(@"%@ skipping invalid image", self.logTag);
return nil; return nil;
} }
NSData *data = [NSData dataWithContentsOfURL:mediaUrl]; return [NSData dataWithContentsOfFile:self.filePath];
if (![self isValidImageWithData:data]) {
return nil;
}
return data;
} }
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType + (BOOL)hasThumbnailForMimeType:(NSString *)contentType
@ -462,6 +457,11 @@ NS_ASSUME_NONNULL_BEGIN
CGImageRelease(thumbnail); CGImageRelease(thumbnail);
} else if (self.isVideo) { } else if (self.isVideo) {
if (![self isValidVideo]) {
DDLogWarn(@"%@ skipping thumbnail for invalid video at path: %@", self.logTag, self.filePath);
return;
}
result = [self videoStillImageWithMaxSize:CGSizeMake(thumbnailSize, thumbnailSize)]; result = [self videoStillImageWithMaxSize:CGSizeMake(thumbnailSize, thumbnailSize)];
} else { } else {
OWSFail(@"%@ trying to generate thumnail for unexpected attachment: %@ of type: %@", OWSFail(@"%@ trying to generate thumnail for unexpected attachment: %@ of type: %@",
@ -471,7 +471,7 @@ NS_ASSUME_NONNULL_BEGIN
} }
if (result == nil) { if (result == nil) {
OWSFail(@"%@ Unable to build thumbnail for attachmentId: %@", self.logTag, self.uniqueId); DDLogError(@"Unable to build thumbnail for attachmentId: %@", self.uniqueId);
return; return;
} }
@ -484,12 +484,18 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable UIImage *)videoStillImage - (nullable UIImage *)videoStillImage
{ {
if (![self isValidVideo]) {
return nil;
}
// Uses the assets intrinsic size by default // Uses the assets intrinsic size by default
return [self videoStillImageWithMaxSize:CGSizeZero]; return [self videoStillImageWithMaxSize:CGSizeMake(kMaxVideoStillSize, kMaxVideoStillSize)];
} }
- (nullable UIImage *)videoStillImageWithMaxSize:(CGSize)maxSize - (nullable UIImage *)videoStillImageWithMaxSize:(CGSize)maxSize
{ {
maxSize.width = MIN(maxSize.width, kMaxVideoStillSize);
maxSize.height = MIN(maxSize.height, kMaxVideoStillSize);
NSURL *_Nullable mediaUrl = [self mediaURL]; NSURL *_Nullable mediaUrl = [self mediaURL];
if (!mediaUrl) { if (!mediaUrl) {
return nil; return nil;
@ -502,6 +508,10 @@ NS_ASSUME_NONNULL_BEGIN
NSError *err = NULL; NSError *err = NULL;
CMTime time = CMTimeMake(1, 60); CMTime time = CMTimeMake(1, 60);
CGImageRef imgRef = [generator copyCGImageAtTime:time actualTime:NULL error:&err]; CGImageRef imgRef = [generator copyCGImageAtTime:time actualTime:NULL error:&err];
if (imgRef == NULL) {
DDLogError(@"Could not generate video still: %@", self.filePath.pathExtension);
return nil;
}
return [[UIImage alloc] initWithCGImage:imgRef]; return [[UIImage alloc] initWithCGImage:imgRef];
} }
@ -531,6 +541,9 @@ NS_ASSUME_NONNULL_BEGIN
- (CGSize)calculateImageSize - (CGSize)calculateImageSize
{ {
if ([self isVideo]) { if ([self isVideo]) {
if (![self isValidVideo]) {
return CGSizeZero;
}
return [self videoStillImage].size; return [self videoStillImage].size;
} else if ([self isImage] || [self isAnimated]) { } else if ([self isImage] || [self isAnimated]) {
NSURL *_Nullable mediaUrl = [self mediaURL]; NSURL *_Nullable mediaUrl = [self mediaURL];

@ -164,7 +164,7 @@ static NSString *const OWSMediaGalleryFinderExtensionName = @"OWSMediaGalleryFin
YapDatabaseViewOptions *options = [YapDatabaseViewOptions new]; YapDatabaseViewOptions *options = [YapDatabaseViewOptions new];
options.allowedCollections = [[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:TSMessage.collection]]; options.allowedCollections = [[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:TSMessage.collection]];
return [[YapDatabaseAutoView alloc] initWithGrouping:grouping sorting:sorting versionTag:@"1" options:options]; return [[YapDatabaseAutoView alloc] initWithGrouping:grouping sorting:sorting versionTag:@"2" options:options];
} }
+ (BOOL)attachmentIdShouldAppearInMediaGallery:(NSString *)attachmentId transaction:(YapDatabaseReadTransaction *)transaction + (BOOL)attachmentIdShouldAppearInMediaGallery:(NSString *)attachmentId transaction:(YapDatabaseReadTransaction *)transaction
@ -176,8 +176,20 @@ static NSString *const OWSMediaGalleryFinderExtensionName = @"OWSMediaGalleryFin
if (![attachment isKindOfClass:[TSAttachmentStream class]]) { if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
return NO; return NO;
} }
return attachment.isImage || attachment.isVideo || attachment.isAnimated; if (attachment.isImage && attachment.isValidImage) {
return YES;
}
if (attachment.isVideo && attachment.isValidVideo) {
return YES;
}
if (attachment.isAnimated && attachment.isValidImage) {
return YES;
}
return NO;
} }
@end @end

@ -31,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)isValidImage; - (BOOL)isValidImage;
- (BOOL)isValidVideo;
@end @end
#pragma mark - #pragma mark -

@ -73,6 +73,11 @@ NS_ASSUME_NONNULL_BEGIN
return [data ows_isValidImage]; return [data ows_isValidImage];
} }
- (BOOL)isValidVideo
{
return [NSData ows_isValidVideoAtURL:self.dataUrl];
}
- (void)setSourceFilename:(nullable NSString *)sourceFilename - (void)setSourceFilename:(nullable NSString *)sourceFilename
{ {
_sourceFilename = sourceFilename.filterFilename; _sourceFilename = sourceFilename.filterFilename;

@ -11,4 +11,6 @@
- (BOOL)ows_isValidImage; - (BOOL)ows_isValidImage;
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType; - (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType;
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url;
@end @end

@ -4,6 +4,7 @@
#import "NSData+Image.h" #import "NSData+Image.h"
#import "MIMETypeUtil.h" #import "MIMETypeUtil.h"
#import <AVFoundation/AVFoundation.h>
typedef NS_ENUM(NSInteger, ImageFormat) { typedef NS_ENUM(NSInteger, ImageFormat) {
ImageFormat_Unknown, ImageFormat_Unknown,
@ -23,18 +24,127 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
- (BOOL)ows_isValidImage - (BOOL)ows_isValidImage
{ {
return [self ows_isValidImageWithMimeType:nil]; if (![self ows_isValidImageWithMimeType:nil]) {
return NO;
}
if (![self ows_hasValidImageDimensions]) {
return NO;
}
return YES;
} }
+ (BOOL)ows_isValidImageAtPath:(NSString *)filePath mimeType:(nullable NSString *)mimeType + (BOOL)ows_isValidImageAtPath:(NSString *)filePath mimeType:(nullable NSString *)mimeType
{ {
NSError *error = nil; NSError *error = nil;
NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error]; NSData *_Nullable data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
if (error) { if (!data || error) {
DDLogError(@"%@ could not read image data: %@", self.logTag, error); DDLogError(@"%@ could not read image data: %@", self.logTag, error);
return NO;
} }
return [data ows_isValidImageWithMimeType:mimeType]; if (![data ows_isValidImageWithMimeType:mimeType]) {
return NO;
}
if (![self ows_hasValidImageDimensionsAtPath:filePath]) {
DDLogError(@"%@ image had invalid dimensions.", self.logTag);
return NO;
}
return YES;
}
- (BOOL)ows_hasValidImageDimensions
{
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self, NULL);
if (imageSource == NULL) {
return NO;
}
BOOL result = [NSData ows_hasValidImageDimensionWithImageSource:imageSource];
CFRelease(imageSource);
return result;
}
+ (BOOL)ows_hasValidImageDimensionsAtPath:(NSString *)path
{
NSURL *url = [NSURL fileURLWithPath:path];
if (!url) {
return NO;
}
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
if (imageSource == NULL) {
return NO;
}
BOOL result = [self ows_hasValidImageDimensionWithImageSource:imageSource];
CFRelease(imageSource);
return result;
}
+ (BOOL)ows_hasValidImageDimensionWithImageSource:(CGImageSourceRef)imageSource
{
OWSAssert(imageSource);
NSDictionary *imageProperties
= (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (!imageProperties) {
return NO;
}
NSNumber *widthNumber = imageProperties[(__bridge NSString *)kCGImagePropertyPixelWidth];
if (!widthNumber) {
DDLogError(@"widthNumber was unexpectedly nil");
return NO;
}
CGFloat width = widthNumber.floatValue;
NSNumber *heightNumber = imageProperties[(__bridge NSString *)kCGImagePropertyPixelHeight];
if (!heightNumber) {
DDLogError(@"heightNumber was unexpectedly nil");
return NO;
}
CGFloat height = heightNumber.floatValue;
/* The number of bits in each color sample of each pixel. The value of this
* key is a CFNumberRef. */
NSNumber *depthNumber = imageProperties[(__bridge NSString *)kCGImagePropertyDepth];
if (!depthNumber) {
DDLogError(@"depthNumber was unexpectedly nil");
return NO;
}
NSUInteger depthBits = depthNumber.unsignedIntegerValue;
CGFloat depthBytes = (CGFloat)ceil(depthBits / 8.f);
/* The color model of the image such as "RGB", "CMYK", "Gray", or "Lab".
* The value of this key is CFStringRef. */
NSString *colorModel = imageProperties[(__bridge NSString *)kCGImagePropertyColorModel];
if (!colorModel) {
DDLogError(@"colorModel was unexpectedly nil");
return NO;
}
if (![colorModel isEqualToString:(__bridge NSString *)kCGImagePropertyColorModelRGB]
&& ![colorModel isEqualToString:(__bridge NSString *)kCGImagePropertyColorModelGray]) {
DDLogError(@"Invalid colorModel: %@", colorModel);
return NO;
}
// We only support (A)RGB and (A)Grayscale, so worst case is 4.
CGFloat kWorseCastComponentsPerPixel = 4;
CGFloat bytesPerPixel = kWorseCastComponentsPerPixel * depthBytes;
CGFloat kMaxDimension = 2 * 1024;
CGFloat kExpectedBytePerPixel = 4;
CGFloat kMaxBytes = kMaxDimension * kMaxDimension * kExpectedBytePerPixel;
CGFloat actualBytes = width * height * bytesPerPixel;
if (actualBytes > kMaxBytes) {
DDLogWarn(@"invalid dimensions width: %f, height %f, bytesPerPixel: %f", width, height, bytesPerPixel);
return NO;
}
return YES;
} }
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType - (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType
@ -151,4 +261,27 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize); return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize);
} }
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url
{
OWSAssert(url);
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
CGSize maxSize = CGSizeZero;
for (AVAssetTrack *track in [asset tracksWithMediaType:AVMediaTypeVideo]) {
CGSize trackSize = track.naturalSize;
maxSize.width = MAX(maxSize.width, trackSize.width);
maxSize.height = MAX(maxSize.height, trackSize.height);
}
if (maxSize.width < 1.f || maxSize.height < 1.f) {
DDLogError(@"Invalid video size: %@", NSStringFromCGSize(maxSize));
return NO;
}
const CGFloat kMaxSize = 3 * 1024.f;
if (maxSize.width > kMaxSize || maxSize.height > kMaxSize) {
DDLogError(@"Invalid video dimensions: %@", NSStringFromCGSize(maxSize));
return NO;
}
return YES;
}
@end @end

Loading…
Cancel
Save