react-native/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m
Ben Roth 7fab093fc8 Fix crash when trying to load photo library assets with nil image url
Summary:
This avoids a crash when we try to load a PHAsset with nil image url. Specifically, the following condition evaluates to true when `imageURL` is nil:

```objc
if ([imageURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
    assetID = [imageURL absoluteString];
    results = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil];
}
```

The crash will be "attempt to insert nil object from objects[0]" when we build the `@[imageURL]` array literal.

We've seen this emerge as a very common crash among Expo users, so I wanted to at least provide a clear error message instead of terminating the app.

Load an image from the photo library with a nil request url.
Closes https://github.com/facebook/react-native/pull/15952

Differential Revision: D5835219

Pulled By: ericnakagawa

fbshipit-source-id: 7be00a15e674a0905cf5c27c526ce9085d1b308f
2017-09-14 12:02:27 -07:00

114 lines
4.2 KiB
Objective-C

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTPhotoLibraryImageLoader.h"
#import <Photos/Photos.h>
#import <React/RCTUtils.h>
@implementation RCTPhotoLibraryImageLoader
RCT_EXPORT_MODULE()
@synthesize bridge = _bridge;
#pragma mark - RCTImageLoader
- (BOOL)canLoadImageURL:(NSURL *)requestURL
{
if (![PHAsset class]) {
return NO;
}
return [requestURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame ||
[requestURL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame;
}
- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
{
// Using PhotoKit for iOS 8+
// The 'ph://' prefix is used by FBMediaKit to differentiate between
// assets-library. It is prepended to the local ID so that it is in the
// form of an, NSURL which is what assets-library uses.
NSString *assetID = @"";
PHFetchResult *results;
if (!imageURL) {
completionHandler(RCTErrorWithMessage(@"Cannot load a photo library asset with no URL"), nil);
return ^{};
} else if ([imageURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
assetID = [imageURL absoluteString];
results = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil];
} else {
assetID = [imageURL.absoluteString substringFromIndex:@"ph://".length];
results = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetID] options:nil];
}
if (results.count == 0) {
NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", assetID];
completionHandler(RCTErrorWithMessage(errorText), nil);
return ^{};
}
PHAsset *asset = [results firstObject];
PHImageRequestOptions *imageOptions = [PHImageRequestOptions new];
// Allow PhotoKit to fetch images from iCloud
imageOptions.networkAccessAllowed = YES;
if (progressHandler) {
imageOptions.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary<NSString *, id> *info) {
static const double multiplier = 1e6;
progressHandler(progress * multiplier, multiplier);
};
}
// Note: PhotoKit defaults to a deliveryMode of PHImageRequestOptionsDeliveryModeOpportunistic
// which means it may call back multiple times - we probably don't want that
imageOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero);
CGSize targetSize;
if (useMaximumSize) {
targetSize = PHImageManagerMaximumSize;
imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone;
} else {
targetSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale));
imageOptions.resizeMode = PHImageRequestOptionsResizeModeFast;
}
PHImageContentMode contentMode = PHImageContentModeAspectFill;
if (resizeMode == RCTResizeModeContain) {
contentMode = PHImageContentModeAspectFit;
}
PHImageRequestID requestID =
[[PHImageManager defaultManager] requestImageForAsset:asset
targetSize:targetSize
contentMode:contentMode
options:imageOptions
resultHandler:^(UIImage *result, NSDictionary<NSString *, id> *info) {
if (result) {
completionHandler(nil, result);
} else {
completionHandler(info[PHImageErrorKey], nil);
}
}];
return ^{
[[PHImageManager defaultManager] cancelImageRequest:requestID];
};
}
@end