diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 3431def50..667995586 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */; }; 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; }; 1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; }; + 1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1345A8381B26592900583190 /* RCTImageRequestHandler.m */; }; 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; }; @@ -36,6 +37,8 @@ 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImageManager.m; sourceTree = ""; }; 1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTGIFImage.h; sourceTree = ""; }; 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = ""; }; + 1345A8371B26592900583190 /* RCTImageRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageRequestHandler.h; sourceTree = ""; }; + 1345A8381B26592900583190 /* RCTImageRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageRequestHandler.m; sourceTree = ""; }; 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCameraRollManager.h; sourceTree = ""; }; 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = ""; }; 143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = ""; }; @@ -71,6 +74,8 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */, 58B511891A9E6BD600147676 /* RCTImageDownloader.h */, 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */, + 1345A8371B26592900583190 /* RCTImageRequestHandler.h */, + 1345A8381B26592900583190 /* RCTImageRequestHandler.m */, 58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */, 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */, 58B5118D1A9E6BD600147676 /* RCTNetworkImageViewManager.h */, @@ -152,6 +157,7 @@ 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */, 58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */, + 1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */, 58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */, 1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */, 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */, diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index 4d23a628e..186a53cd1 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -23,6 +23,4 @@ + (void)loadImageWithTag:(NSString *)tag callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback; -+ (BOOL)isSystemImageURI:(NSString *)uri; - @end diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 1fda32191..f28502d7e 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -16,11 +16,23 @@ #import #import "RCTConvert.h" +#import "RCTDefines.h" #import "RCTGIFImage.h" #import "RCTImageDownloader.h" #import "RCTLog.h" #import "RCTUtils.h" +static void RCTDispatchCallbackOnMainQueue(void (^ __nonnull callback)(NSError *, id), NSError *error, UIImage *image) +{ + if ([NSThread isMainThread]) { + callback(error, image); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + callback(error, image); + }); + } +} + static dispatch_queue_t RCTImageLoaderQueue(void) { static dispatch_queue_t queue = NULL; @@ -137,11 +149,4 @@ static dispatch_queue_t RCTImageLoaderQueue(void) } } -+ (BOOL)isSystemImageURI:(NSString *)uri -{ - return uri != nil && ( - [uri hasPrefix:@"assets-library"] || - [uri hasPrefix:@"ph://"]); -} - @end diff --git a/Libraries/Network/RCTDataQuery.h b/Libraries/Image/RCTImageRequestHandler.h similarity index 52% rename from Libraries/Network/RCTDataQuery.h rename to Libraries/Image/RCTImageRequestHandler.h index 4588cbbe4..0f0359885 100644 --- a/Libraries/Network/RCTDataQuery.h +++ b/Libraries/Image/RCTImageRequestHandler.h @@ -7,15 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTBridgeModule.h" +#import "RCTURLRequestHandler.h" -@protocol RCTDataQueryExecutor - -- (void)addQuery:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender; - -@optional - -@property (nonatomic, weak) RCTBridge *bridge; -@property (nonatomic, assign) BOOL sendIncrementalUpdates; +@interface RCTImageRequestHandler : NSObject @end diff --git a/Libraries/Image/RCTImageRequestHandler.m b/Libraries/Image/RCTImageRequestHandler.m new file mode 100644 index 000000000..e5eb3bfd4 --- /dev/null +++ b/Libraries/Image/RCTImageRequestHandler.m @@ -0,0 +1,62 @@ +// +// RCTImageRequestHandler.m +// RCTImage +// +// Created by Nick Lockwood on 09/06/2015. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTImageRequestHandler.h" + +#import + +#import "RCTImageLoader.h" +#import "RCTUtils.h" + +@implementation RCTImageRequestHandler +{ + NSInteger _currentToken; +} + +RCT_EXPORT_MODULE() + +- (BOOL)canHandleRequest:(NSURLRequest *)request +{ + return [@[@"assets-library", @"ph"] containsObject:[request.URL.scheme lowercaseString]]; +} + +- (id)sendRequest:(NSURLRequest *)request + withDelegate:(id)delegate +{ + NSNumber *requestToken = @(++_currentToken); + NSString *URLString = [request.URL absoluteString]; + [RCTImageLoader loadImageWithTag:URLString callback:^(NSError *error, UIImage *image) { + if (error) { + [delegate URLRequest:requestToken didCompleteWithError:error]; + return; + } + + NSString *mimeType = nil; + NSData *imageData = nil; + if (RCTImageHasAlpha(image.CGImage)) { + mimeType = @"image/png"; + imageData = UIImagePNGRepresentation(image); + } else { + mimeType = @"image/jpeg"; + imageData = UIImageJPEGRepresentation(image, 1.0); + } + + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL + MIMEType:mimeType + expectedContentLength:imageData.length + textEncodingName:nil]; + + [delegate URLRequest:requestToken didReceiveResponse:response]; + [delegate URLRequest:requestToken didReceiveData:imageData]; + [delegate URLRequest:requestToken didCompleteWithError:nil]; + }]; + + return requestToken; +} + +@end diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m index 135214106..a05e942fb 100644 --- a/Libraries/Network/RCTDataManager.m +++ b/Libraries/Network/RCTDataManager.m @@ -11,44 +11,452 @@ #import "RCTAssert.h" #import "RCTConvert.h" -#import "RCTDataQuery.h" +#import "RCTURLRequestHandler.h" #import "RCTEventDispatcher.h" -#import "RCTHTTPQueryExecutor.h" +#import "RCTHTTPRequestHandler.h" #import "RCTLog.h" #import "RCTUtils.h" +typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result); + +@interface RCTDataManager () + +- (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError *error, NSDictionary *result))callback; + +@end + +/** + * Helper to convert FormData payloads into multipart/formdata requests. + */ +@interface RCTHTTPFormDataHelper : NSObject + +@property (nonatomic, weak) RCTDataManager *dataManager; + +@end + +@implementation RCTHTTPFormDataHelper +{ + NSMutableArray *parts; + NSMutableData *multipartBody; + RCTHTTPQueryResult _callback; + NSString *boundary; +} + +- (void)process:(NSArray *)formData callback:(nonnull void (^)(NSError *error, NSDictionary *result))callback +{ + if (![formData count]) { + callback(nil, nil); + return; + } + parts = [formData mutableCopy]; + _callback = callback; + multipartBody = [[NSMutableData alloc] init]; + boundary = [self generateBoundary]; + + NSDictionary *currentPart = [parts objectAtIndex: 0]; + [_dataManager processDataForHTTPQuery:currentPart callback:^(NSError *e, NSDictionary *r) { + [self handleResult:r error:e]; + }]; +} + +- (NSString *)generateBoundary +{ + NSString *const boundaryChars = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./"; + const NSUInteger boundaryLength = 70; + + NSMutableString *output = [NSMutableString stringWithCapacity:boundaryLength]; + NSUInteger numchars = [boundaryChars length]; + for (NSUInteger i = 0; i < boundaryLength; i++) { + [output appendFormat:@"%C", [boundaryChars characterAtIndex:arc4random_uniform((u_int32_t)numchars)]]; + } + return output; +} + +- (void)handleResult:(NSDictionary *)result error:(NSError *)error +{ + if (error) { + _callback(error, nil); + return; + } + NSDictionary *currentPart = parts[0]; + [parts removeObjectAtIndex:0]; + + // Start with boundary. + [multipartBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] + dataUsingEncoding:NSUTF8StringEncoding]]; + + // Print headers. + NSMutableDictionary *headers = [(NSDictionary*)currentPart[@"headers"] mutableCopy]; + NSString *partContentType = result[@"contentType"]; + if (partContentType != nil) { + [headers setObject:partContentType forKey:@"content-type"]; + } + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) { + [multipartBody appendData:[[NSString stringWithFormat:@"%@: %@\r\n", parameterKey, parameterValue] + dataUsingEncoding:NSUTF8StringEncoding]]; + }]; + + // Add the body. + [multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + [multipartBody appendData:result[@"body"]]; + [multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; + + if ([parts count]) { + NSDictionary *nextPart = [parts objectAtIndex: 0]; + [_dataManager processDataForHTTPQuery:nextPart callback:^(NSError *e, NSDictionary *r) { + [self handleResult:r error:e]; + }]; + return; + } + + // We've processed the last item. Finish and return. + [multipartBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] + dataUsingEncoding:NSUTF8StringEncoding]]; + NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=\"%@\"", boundary]; + _callback(nil, @{@"body": multipartBody, @"contentType": contentType}); +} + +@end + +/** + * Helper to package in-flight requests together with their response data. + */ +@interface RCTActiveURLRequest : NSObject + +@property (nonatomic, strong) NSNumber *requestID; +@property (nonatomic, strong) NSURLRequest *request; +@property (nonatomic, strong) id handler; +@property (nonatomic, assign) BOOL incrementalUpdates; +@property (nonatomic, strong) NSURLResponse *response; +@property (nonatomic, strong) NSMutableData *data; + +@end + +@implementation RCTActiveURLRequest + +- (void)setResponse:(NSURLResponse *)response; +{ + _response = response; + if (!_incrementalUpdates) { + _data = [[NSMutableData alloc] initWithCapacity:(NSUInteger)MAX(0, response.expectedContentLength)]; + } +} + +@end + +/** + * Helper to load request body data using a handler. + */ +@interface RCTDataLoader : NSObject + +@end + +typedef void (^RCTDataLoaderCallback)(NSData *data, NSString *MIMEType, NSError *error); + +@implementation RCTDataLoader +{ + RCTDataLoaderCallback _callback; + RCTActiveURLRequest *_request; + id _requestToken; +} + +- (instancetype)initWithRequest:(NSURLRequest *)request + handler:(id)handler + callback:(RCTDataLoaderCallback)callback +{ + if ((self = [super init])) { + _callback = callback; + _request = [[RCTActiveURLRequest alloc] init]; + _request.request = request; + _request.handler = handler; + _request.incrementalUpdates = NO; + _requestToken = [handler sendRequest:request withDelegate:self]; + } + return self; +} + +- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response +{ + RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen"); + _request.response = response; +} + +- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data +{ + RCTAssert([requestToken isEqual:_requestToken], @"Shouldn't ever happen"); + [_request.data appendData:data]; +} + +- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error +{ + RCTAssert(_callback != nil, @"The callback property must be set"); + _callback(_request.data, _request.response.MIMEType, error); +} + +@end + +/** + * Bridge module that provides the JS interface to the network stack. + */ @implementation RCTDataManager +{ + NSInteger _currentRequestID; + NSMapTable *_activeRequests; +} @synthesize bridge = _bridge; RCT_EXPORT_MODULE() -/** - * Executes a network request. - * The responseSender block won't be called on same thread as called. - */ -RCT_EXPORT_METHOD(queryData:(NSString *)queryType - withQuery:(NSDictionary *)query - sendIncrementalUpdates:(BOOL)incrementalUpdates - responseSender:(RCTResponseSenderBlock)responseSender) +- (instancetype)init { - id executor = nil; - if ([queryType isEqualToString:@"http"]) { - executor = [RCTHTTPQueryExecutor sharedInstance]; - } else { - RCTLogError(@"unsupported query type %@", queryType); + if ((self = [super init])) { + _currentRequestID = 0; + _activeRequests = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory + valueOptions:NSPointerFunctionsStrongMemory + capacity:0]; + } + return self; +} + +- (void)buildRequest:(NSDictionary *)query + responseSender:(RCTResponseSenderBlock)responseSender +{ + NSURL *URL = [RCTConvert NSURL:query[@"url"]]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; + request.HTTPMethod = [[RCTConvert NSString:query[@"method"]] uppercaseString] ?: @"GET"; + request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]]; + + BOOL incrementalUpdates = [RCTConvert BOOL:query[@"incrementalUpdates"]]; + + NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]]; + [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) { + if (error) { + RCTLogError(@"Error processing request body: %@", error); + // Ideally we'd circle back to JS here and notify an error/abort on the request. + return; + } + request.HTTPBody = result[@"body"]; + NSString *contentType = result[@"contentType"]; + if (contentType) { + [request setValue:contentType forHTTPHeaderField:@"content-type"]; + } + [self sendRequest:request + incrementalUpdates:incrementalUpdates + responseSender:responseSender]; + }]; +} + +- (id)handlerForRequest:(NSURLRequest *)request +{ + NSMutableArray *handlers = [NSMutableArray array]; + for (id module in _bridge.modules.allValues) { + if ([module conformsToProtocol:@protocol(RCTURLRequestHandler)]) { + if ([(id)module canHandleRequest:request]) { + [handlers addObject:module]; + } + } + } + [handlers sortUsingComparator:^NSComparisonResult(id a, id b) { + float priorityA = [a respondsToSelector:@selector(handlerPriority)] ? [a handlerPriority] : 0; + float priorityB = [b respondsToSelector:@selector(handlerPriority)] ? [b handlerPriority] : 0; + if (priorityA < priorityB) { + return NSOrderedAscending; + } else if (priorityA > priorityB) { + return NSOrderedDescending; + } else { + RCTLogError(@"The RCTURLRequestHandlers %@ and %@ both reported that" + " they can handle the request %@, and have equal priority" + " (%g). This could result in non-deterministic behavior.", + a, b, request, priorityA); + + return NSOrderedSame; + } + }]; + id handler = [handlers lastObject]; + if (!handler) { + RCTLogError(@"No suitable request handler found for %@", request); + } + return handler; +} + +/** + * Process the 'data' part of an HTTP query. + * + * 'data' can be a JSON value of the following forms: + * + * - {"string": "..."}: a simple JS string that will be UTF-8 encoded and sent as the body + * + * - {"uri": "some-uri://..."}: reference to a system resource, e.g. an image in the asset library + * + * - {"formData": [...]}: list of data payloads that will be combined into a multipart/form-data request + * + * If successful, the callback be called with a result dictionary containing the following (optional) keys: + * + * - @"body" (NSData): the body of the request + * + * - @"contentType" (NSString): the content type header of the request + * + */ +- (void)processDataForHTTPQuery:(NSDictionary *)query callback:(nonnull void (^)(NSError *error, NSDictionary *result))callback +{ + if (!query) { + callback(nil, nil); + return; + } + NSData *body = [RCTConvert NSData:query[@"string"]]; + if (body) { + callback(nil, @{@"body": body}); + return; + } + NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]]; + if (request) { + id handler = [self handlerForRequest:request]; + (void)[[RCTDataLoader alloc] initWithRequest:request handler:handler callback:^(NSData *data, NSString *MIMEType, NSError *error) { + if (data) { + callback(nil, @{@"body": data, @"contentType": MIMEType}); + } else { + callback(error, nil); + } + }]; + return; + } + NSDictionaryArray *formData = [RCTConvert NSDictionaryArray:query[@"formData"]]; + if (formData != nil) { + RCTHTTPFormDataHelper *formDataHelper = [[RCTHTTPFormDataHelper alloc] init]; + formDataHelper.dataManager = self; + [formDataHelper process:formData callback:callback]; + return; + } + // Nothing in the data payload, at least nothing we could understand anyway. + // Ignore and treat it as if it were null. + callback(nil, nil); +} + +- (void)sendRequest:(NSURLRequest *)request + incrementalUpdates:(BOOL)incrementalUpdates + responseSender:(RCTResponseSenderBlock)responseSender +{ + id handler = [self handlerForRequest:request]; + id token = [handler sendRequest:request withDelegate:self]; + if (token) { + RCTActiveURLRequest *activeRequest = [[RCTActiveURLRequest alloc] init]; + activeRequest.requestID = @(++_currentRequestID); + activeRequest.request = request; + activeRequest.handler = handler; + activeRequest.incrementalUpdates = incrementalUpdates; + [_activeRequests setObject:activeRequest forKey:token]; + responseSender(@[activeRequest.requestID]); + } +} + +- (void)sendData:(NSData *)data forRequestToken:(id)requestToken +{ + if (data.length == 0) { return; } - RCTAssert(executor != nil, @"executor must be defined"); + RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; - if ([executor respondsToSelector:@selector(setBridge:)]) { - executor.bridge = _bridge; + // Get text encoding + NSURLResponse *response = request.response; + NSStringEncoding encoding = NSUTF8StringEncoding; + if (response.textEncodingName) { + CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } - if ([executor respondsToSelector:@selector(setSendIncrementalUpdates:)]) { - executor.sendIncrementalUpdates = incrementalUpdates; + + NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding]; + if (!responseText && data.length) { + RCTLogError(@"Received data was invalid."); + return; + } + + NSArray *responseJSON = @[request.requestID, responseText ?: @""]; + [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData" + body:responseJSON]; +} + +#pragma mark - RCTURLRequestDelegate + +- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response +{ + RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; + RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); + + request.response = response; + + NSHTTPURLResponse *httpResponse = nil; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + // Might be a local file request + httpResponse = (NSHTTPURLResponse *)response; + } + + NSArray *responseJSON = @[request.requestID, + @(httpResponse.statusCode ?: 200), + httpResponse.allHeaderFields ?: @{}, + ]; + + [_bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" + body:responseJSON]; +} + +- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data +{ + RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; + RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); + + if (request.incrementalUpdates) { + [self sendData:data forRequestToken:requestToken]; + } else { + [request.data appendData:data]; + } +} + +- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error +{ + RCTActiveURLRequest *request = [_activeRequests objectForKey:requestToken]; + RCTAssert(request != nil, @"Unrecognized request token: %@", requestToken); + + if (!request.incrementalUpdates) { + [self sendData:request.data forRequestToken:requestToken]; + } + + NSArray *responseJSON = @[request.requestID, + error.localizedDescription ?: [NSNull null] + ]; + + [_bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" + body:responseJSON]; + + [_activeRequests removeObjectForKey:requestToken]; +} + +#pragma mark - JS API + +RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query + responseSender:(RCTResponseSenderBlock)responseSender) +{ + [self buildRequest:query responseSender:responseSender]; +} + +RCT_EXPORT_METHOD(cancelRequest:(NSNumber *)requestID) +{ + id requestToken = nil; + RCTActiveURLRequest *activeRequest = nil; + for (id token in _activeRequests) { + RCTActiveURLRequest *request = [_activeRequests objectForKey:token]; + if ([request.requestID isEqualToNumber:requestID]) { + activeRequest = request; + requestToken = token; + break; + } + } + + id handler = activeRequest.handler; + if ([handler respondsToSelector:@selector(cancelRequest:)]) { + [activeRequest.handler cancelRequest:requestToken]; } - [executor addQuery:query responseSender:responseSender]; } @end diff --git a/Libraries/Network/RCTHTTPQueryExecutor.h b/Libraries/Network/RCTHTTPQueryExecutor.h deleted file mode 100644 index 196a4da5f..000000000 --- a/Libraries/Network/RCTHTTPQueryExecutor.h +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 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 - -#import "RCTDataQuery.h" - -@interface RCTHTTPQueryExecutor : NSObject - -+ (instancetype)sharedInstance; - -/** - * Process the 'data' part of an HTTP query. - * - * 'data' can be a JSON value of the following forms: - * - * - {"string": "..."}: a simple JS string that will be UTF-8 encoded and sent as the body - * - * - {"uri": "some-uri://..."}: reference to a system resource, e.g. an image in the asset library - * - * - {"formData": [...]}: list of data payloads that will be combined into a multipart/form-data request - * - * If successful, the callback be called with a result dictionary containing the following (optional) keys: - * - * - @"body" (NSData): the body of the request - * - * - @"contentType" (NSString): the content type header of the request - * - */ -+ (void)processDataForHTTPQuery:(NSDictionary *)data - callback:(void (^)(NSError *error, NSDictionary *result))callback; - -@end diff --git a/Libraries/Network/RCTHTTPQueryExecutor.m b/Libraries/Network/RCTHTTPQueryExecutor.m deleted file mode 100644 index 7eb858f18..000000000 --- a/Libraries/Network/RCTHTTPQueryExecutor.m +++ /dev/null @@ -1,314 +0,0 @@ -/** - * 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 "RCTHTTPQueryExecutor.h" - -#import "RCTAssert.h" -#import "RCTConvert.h" -#import "RCTEventDispatcher.h" -#import "RCTImageLoader.h" -#import "RCTLog.h" -#import "RCTUtils.h" - -/** - * Helper to convert FormData payloads into multipart/formdata requests. - */ -@interface RCTHTTPFormDataHelper : NSObject - -- (void)process:(NSArray *)formData callback:(void (^)(NSError *error, NSDictionary *result))callback; - -@end - -@implementation RCTHTTPFormDataHelper -{ - NSMutableArray *parts; - NSMutableData *multipartBody; - RCTResultOrErrorBlock callback; - NSString *boundary; -} - -- (void)process:(NSArray *)formData callback:(void (^)(NSError *error, NSDictionary *result))cb -{ - if (![formData count]) { - RCTDispatchCallbackOnMainQueue(cb, nil, nil); - return; - } - parts = [formData mutableCopy]; - callback = cb; - multipartBody = [[NSMutableData alloc] init]; - boundary = [self generateBoundary]; - - NSDictionary *currentPart = [parts objectAtIndex: 0]; - [RCTHTTPQueryExecutor processDataForHTTPQuery:currentPart callback:^(NSError *e, NSDictionary *r) { - [self handleResult:r error:e]; - }]; -} - -- (NSString *)generateBoundary -{ - NSString *const boundaryChars = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./"; - const NSUInteger boundaryLength = 70; - - NSMutableString *output = [NSMutableString stringWithCapacity:boundaryLength]; - NSUInteger numchars = [boundaryChars length]; - for (NSUInteger i = 0; i < boundaryLength; i++) { - [output appendFormat:@"%C", [boundaryChars characterAtIndex:arc4random_uniform((u_int32_t)numchars)]]; - } - return output; -} - -- (void)handleResult:(NSDictionary *)result error:(NSError *)error -{ - if (error) { - RCTDispatchCallbackOnMainQueue(callback, error, nil); - return; - } - NSDictionary *currentPart = parts[0]; - [parts removeObjectAtIndex:0]; - - // Start with boundary. - [multipartBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] - dataUsingEncoding:NSUTF8StringEncoding]]; - - // Print headers. - NSMutableDictionary *headers = [(NSDictionary*)currentPart[@"headers"] mutableCopy]; - NSString *partContentType = result[@"contentType"]; - if (partContentType != nil) { - [headers setObject:partContentType forKey:@"content-type"]; - } - [headers enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) { - [multipartBody appendData:[[NSString stringWithFormat:@"%@: %@\r\n", parameterKey, parameterValue] - dataUsingEncoding:NSUTF8StringEncoding]]; - }]; - - // Add the body. - [multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; - [multipartBody appendData:result[@"body"]]; - [multipartBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; - - if ([parts count]) { - NSDictionary *nextPart = [parts objectAtIndex: 0]; - [RCTHTTPQueryExecutor processDataForHTTPQuery:nextPart callback:^(NSError *e, NSDictionary *r) { - [self handleResult:r error:e]; - }]; - return; - } - - // We've processed the last item. Finish and return. - [multipartBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] - dataUsingEncoding:NSUTF8StringEncoding]]; - NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=\"%@\"", boundary]; - callback(nil, @{@"body": multipartBody, @"contentType": contentType}); -} - -@end - -@interface RCTHTTPQueryExecutor () - -@end - -@implementation RCTHTTPQueryExecutor -{ - NSURLSession *_session; - NSOperationQueue *_callbackQueue; -} - -@synthesize bridge = _bridge; -@synthesize sendIncrementalUpdates = _sendIncrementalUpdates; - -+ (instancetype)sharedInstance -{ - static RCTHTTPQueryExecutor *_sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - _sharedInstance = [[RCTHTTPQueryExecutor alloc] init]; - }); - return _sharedInstance; -} - -- (void)addQuery:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender -{ - [self makeRequest:query responseSender:responseSender]; -} - -- (void)makeRequest:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender -{ - // Build request - NSURL *URL = [RCTConvert NSURL:query[@"url"]]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; - request.HTTPMethod = [RCTConvert NSString:query[@"method"]] ?: @"GET"; - request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]]; - - NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]]; - - [[self class] processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) { - if (error != nil) { - RCTLogError(@"Error processing request body: %@", error); - // Ideally we'd circle back to JS here and notify an error/abort on the request. - return; - } - request.HTTPBody = result[@"body"]; - NSString *contentType = result[@"contentType"]; - if (contentType != nil) { - [request setValue:contentType forHTTPHeaderField:@"content-type"]; - } - [self sendRequest:request responseSender:responseSender]; - }]; -} - -+ (void)processURIDataForHTTPQuery:(NSString *)uri callback:(void (^)(NSError *error, NSDictionary *result))callback -{ - if ([RCTImageLoader isSystemImageURI:uri]) { - [RCTImageLoader loadImageWithTag:(NSString *)uri callback:^(NSError *error, UIImage *image) { - if (error) { - RCTDispatchCallbackOnMainQueue(callback, error, nil); - return; - } - NSData *imageData = UIImageJPEGRepresentation(image, 1.0); - RCTDispatchCallbackOnMainQueue(callback, nil, @{@"body": imageData, @"contentType": @"image/jpeg"}); - }]; - return; - } - NSString *errorText = [NSString stringWithFormat:@"Cannot resolve URI: %@", uri]; - NSError *error = RCTErrorWithMessage(errorText); - RCTDispatchCallbackOnMainQueue(callback, error, nil); -} - -+ (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError *error, NSDictionary *result))callback -{ - if (data == nil) { - RCTDispatchCallbackOnMainQueue(callback, nil, nil); - return; - } - - NSData *body = [RCTConvert NSData:data[@"string"]]; - if (body != nil) { - RCTDispatchCallbackOnMainQueue(callback, nil, @{@"body": body}); - return; - } - NSString *uri = [RCTConvert NSString:data[@"uri"]]; - if (uri != nil) { - [RCTHTTPQueryExecutor processURIDataForHTTPQuery:uri callback:callback]; - return; - } - NSDictionaryArray *formData = [RCTConvert NSDictionaryArray:data[@"formData"]]; - if (formData != nil) { - RCTHTTPFormDataHelper *formDataHelper = [[RCTHTTPFormDataHelper alloc] init]; - [formDataHelper process:formData callback:callback]; - return; - } - // Nothing in the data payload, at least nothing we could understand anyway. - // Ignore and treat it as if it were null. - RCTDispatchCallbackOnMainQueue(callback, nil, nil); -} - -- (void)sendRequest:(NSURLRequest *)request responseSender:(RCTResponseSenderBlock)responseSender -{ - // Create session if one doesn't already exist - if (!_session) { - _callbackQueue = [[NSOperationQueue alloc] init]; - NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - _session = [NSURLSession sessionWithConfiguration:configuration - delegate:self - delegateQueue:_callbackQueue]; - } - - __block NSURLSessionDataTask *task; - if (_sendIncrementalUpdates) { - task = [_session dataTaskWithRequest:request]; - } else { - task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - RCTSendResponseEvent(_bridge, task); - if (!error) { - RCTSendDataEvent(_bridge, task, data); - } - RCTSendCompletionEvent(_bridge, task, error); - }]; - } - - // Build data task - responseSender(@[@(task.taskIdentifier)]); - [task resume]; -} - -#pragma mark - URLSession delegate - -- (void)URLSession:(NSURLSession *)session - dataTask:(NSURLSessionDataTask *)task -didReceiveResponse:(NSURLResponse *)response - completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler -{ - RCTSendResponseEvent(_bridge, task); - completionHandler(NSURLSessionResponseAllow); -} - -- (void)URLSession:(NSURLSession *)session - dataTask:(NSURLSessionDataTask *)task - didReceiveData:(NSData *)data -{ - RCTSendDataEvent(_bridge, task, data); -} - -- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error -{ - RCTSendCompletionEvent(_bridge, task, error); -} - -#pragma mark - Build responses - -static void RCTSendResponseEvent(RCTBridge *bridge, NSURLSessionTask *task) -{ - NSURLResponse *response = task.response; - NSHTTPURLResponse *httpResponse = nil; - if ([response isKindOfClass:[NSHTTPURLResponse class]]) { - // Might be a local file request - httpResponse = (NSHTTPURLResponse *)response; - } - - NSArray *responseJSON = @[@(task.taskIdentifier), - @(httpResponse.statusCode ?: 200), - httpResponse.allHeaderFields ?: @{}, - ]; - - [bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkResponse" - body:responseJSON]; -} - -static void RCTSendDataEvent(RCTBridge *bridge, NSURLSessionDataTask *task, NSData *data) -{ - // Get text encoding - NSURLResponse *response = task.response; - NSStringEncoding encoding = NSUTF8StringEncoding; - if (response.textEncodingName) { - CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); - encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); - } - - NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding]; - if (!responseText && data.length) { - RCTLogError(@"Received data was invalid."); - return; - } - - NSArray *responseJSON = @[@(task.taskIdentifier), responseText ?: @""]; - [bridge.eventDispatcher sendDeviceEventWithName:@"didReceiveNetworkData" - body:responseJSON]; -} - -static void RCTSendCompletionEvent(RCTBridge *bridge, NSURLSessionTask *task, NSError *error) -{ - NSArray *responseJSON = @[@(task.taskIdentifier), - error.localizedDescription ?: [NSNull null], - ]; - - [bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" - body:responseJSON]; -} - -@end diff --git a/Libraries/Network/RCTHTTPRequestHandler.h b/Libraries/Network/RCTHTTPRequestHandler.h new file mode 100644 index 000000000..b8a7a3e26 --- /dev/null +++ b/Libraries/Network/RCTHTTPRequestHandler.h @@ -0,0 +1,15 @@ +/** + * 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 "RCTURLRequestHandler.h" +#import "RCTInvalidating.h" + +@interface RCTHTTPRequestHandler : NSObject + +@end diff --git a/Libraries/Network/RCTHTTPRequestHandler.m b/Libraries/Network/RCTHTTPRequestHandler.m new file mode 100644 index 000000000..7c1a5ac68 --- /dev/null +++ b/Libraries/Network/RCTHTTPRequestHandler.m @@ -0,0 +1,107 @@ +/** + * 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 "RCTHTTPRequestHandler.h" + +#import "RCTAssert.h" +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTImageLoader.h" +#import "RCTLog.h" +#import "RCTUtils.h" + +@interface RCTHTTPRequestHandler () + +@end + +@implementation RCTHTTPRequestHandler +{ + NSMapTable *_delegates; + NSURLSession *_session; +} + +RCT_EXPORT_MODULE() + +- (instancetype)init +{ + if ((self = [super init])) { + _delegates = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory + valueOptions:NSPointerFunctionsStrongMemory + capacity:0]; + } + return self; +} + +- (void)invalidate +{ + [_session invalidateAndCancel]; + _session = nil; + _delegates = nil; +} + +- (BOOL)isValid +{ + return _delegates != nil; +} + +#pragma mark - NSURLRequestHandler + +- (BOOL)canHandleRequest:(NSURLRequest *)request +{ + return [@[@"http", @"https", @"file"] containsObject:[request.URL.scheme lowercaseString]]; +} + +- (id)sendRequest:(NSURLRequest *)request + withDelegate:(id)delegate +{ + // Lazy setup + if (!_session && [self isValid]) { + NSOperationQueue *callbackQueue = [[NSOperationQueue alloc] init]; + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + _session = [NSURLSession sessionWithConfiguration:configuration + delegate:self + delegateQueue:callbackQueue]; + } + + NSURLSessionDataTask *task = [_session dataTaskWithRequest:request]; + [_delegates setObject:delegate forKey:task]; + [task resume]; + return task; +} + +- (void)cancelRequest:(NSURLSessionDataTask *)requestToken +{ + [requestToken cancel]; +} + +#pragma mark - NSURLSession delegate + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)task +didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler +{ + [[_delegates objectForKey:task] URLRequest:task didReceiveResponse:response]; + completionHandler(NSURLSessionResponseAllow); +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)task + didReceiveData:(NSData *)data +{ + [[_delegates objectForKey:task] URLRequest:task didReceiveData:data]; +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error +{ + [[_delegates objectForKey:task] URLRequest:task didCompleteWithError:error]; + [_delegates removeObjectForKey:task]; +} + +@end diff --git a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj index bc846292e..dba7c65fa 100644 --- a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj +++ b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTReachability.m */; }; - 352DA0BA1B17855800AA15A8 /* RCTHTTPQueryExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DA0B81B17855800AA15A8 /* RCTHTTPQueryExecutor.m */; }; + 352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */; }; 58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTDataManager.m */; }; /* End PBXBuildFile section */ @@ -27,9 +27,8 @@ /* Begin PBXFileReference section */ 1372B7351AB03E7B00659ED6 /* RCTReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTReachability.h; sourceTree = ""; }; 1372B7361AB03E7B00659ED6 /* RCTReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTReachability.m; sourceTree = ""; }; - 352DA0B51B17855800AA15A8 /* RCTDataQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataQuery.h; sourceTree = ""; }; - 352DA0B71B17855800AA15A8 /* RCTHTTPQueryExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTHTTPQueryExecutor.h; sourceTree = ""; }; - 352DA0B81B17855800AA15A8 /* RCTHTTPQueryExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTHTTPQueryExecutor.m; sourceTree = ""; }; + 352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTHTTPRequestHandler.h; sourceTree = ""; }; + 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTHTTPRequestHandler.m; sourceTree = ""; }; 58B511DB1A9E6C8500147676 /* libRCTNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTNetwork.a; sourceTree = BUILT_PRODUCTS_DIR; }; 58B512061A9E6CE300147676 /* RCTDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataManager.h; sourceTree = ""; }; 58B512071A9E6CE300147676 /* RCTDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDataManager.m; sourceTree = ""; }; @@ -51,9 +50,8 @@ children = ( 58B512061A9E6CE300147676 /* RCTDataManager.h */, 58B512071A9E6CE300147676 /* RCTDataManager.m */, - 352DA0B51B17855800AA15A8 /* RCTDataQuery.h */, - 352DA0B71B17855800AA15A8 /* RCTHTTPQueryExecutor.h */, - 352DA0B81B17855800AA15A8 /* RCTHTTPQueryExecutor.m */, + 352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */, + 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */, 1372B7351AB03E7B00659ED6 /* RCTReachability.h */, 1372B7361AB03E7B00659ED6 /* RCTReachability.m */, 58B511DC1A9E6C8500147676 /* Products */, @@ -128,7 +126,7 @@ files = ( 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */, 58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */, - 352DA0BA1B17855800AA15A8 /* RCTHTTPQueryExecutor.m in Sources */, + 352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index 5a5a29a9f..151781c91 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -89,28 +89,24 @@ class XMLHttpRequest extends XMLHttpRequestBase { if (data instanceof FormData) { data = {formData: data.getParts()}; } - RCTDataManager.queryData( - 'http', + RCTDataManager.sendRequest( { method, url, data, headers, + incrementalUpdates: this.onreadystatechange ? true : false, }, - this.onreadystatechange ? true : false, this._didCreateRequest.bind(this) ); } abortImpl(): void { if (this._requestId) { + RCTDataManager.cancelRequest(this._requestId); this._clearSubscriptions(); this._requestId = null; } - console.warn( - 'XMLHttpRequest: abort() cancels JS callbacks ' + - 'but not native HTTP request.' - ); } } diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 3bdf59753..0a08326c7 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -118,7 +118,8 @@ RCT_CONVERTER(NSString *, NSString, description) + (NSURLRequest *)NSURLRequest:(id)json { - return [NSURLRequest requestWithURL:[self NSURL:json]]; + NSURL *URL = [self NSURL:json]; + return URL ? [NSURLRequest requestWithURL:URL] : nil; } + (NSDate *)NSDate:(id)json diff --git a/React/Base/RCTDefines.h b/React/Base/RCTDefines.h index 0d985db8a..83a60d61a 100644 --- a/React/Base/RCTDefines.h +++ b/React/Base/RCTDefines.h @@ -18,6 +18,21 @@ #define RCT_EXTERN extern __attribute__((visibility("default"))) #endif +/** + * Nullability for Xcode 6.2 + */ +#if !__has_feature(nullability) +#define NS_ASSUME_NONNULL_BEGIN +#define NS_ASSUME_NONNULL_END +#define nullable +#define nonnull +#define null_unspecified +#define null_resettable +#define __nullable +#define __nonnull +#define __null_unspecified +#endif + /** * The RCT_DEBUG macro can be used to exclude error checking and logging code * from release builds to improve performance and reduce binary size. diff --git a/React/Base/RCTURLRequestDelegate.h b/React/Base/RCTURLRequestDelegate.h new file mode 100644 index 000000000..3ca5b0e01 --- /dev/null +++ b/React/Base/RCTURLRequestDelegate.h @@ -0,0 +1,36 @@ +/** + * 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 + +/** + * An abstract interface used by request handler modules to send + * data back over the bridge back to JS. + */ +@protocol RCTURLRequestDelegate + +/** + * Call this when you first receives a response from the server. This should + * include response headers, etc. + */ +- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response; + +/** + * Call this when you receive data from the server. This can be called multiple + * times with partial data chunks, or just once with the full data packet. + */ +- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data; + +/** + * Call this when the request is complete and/or if an error is encountered. + * For a successful request, the error parameter should be nil. + */ +- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error; + +@end diff --git a/React/Base/RCTURLRequestHandler.h b/React/Base/RCTURLRequestHandler.h new file mode 100644 index 000000000..d5959e5eb --- /dev/null +++ b/React/Base/RCTURLRequestHandler.h @@ -0,0 +1,54 @@ +/** + * 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 "RCTBridgeModule.h" +#import "RCTURLRequestDelegate.h" + +/** + * Provides the interface needed to register a request handler. Request handlers + * are also bridge modules, so should be registered using RCT_EXPORT_MODULE(). + */ +@protocol RCTURLRequestHandler + +/** + * Indicates whether this handler is capable of processing the specified + * request. Typically the handler would examine the scheme/protocol of the + * request URL (and possibly the HTTP method and/or headers) to determine this. + */ +- (BOOL)canHandleRequest:(NSURLRequest *)request; + +/** + * Send a network request and call the delegate with the response data. The + * method should return a token, which can be anything, including the request + * itself. This will be used later to refer to the request in callbacks. The + * `sendRequest:withDelegate:` method *must* return before calling any of the + * delegate methods, or the delegate won't recognize the token. + */ +- (id)sendRequest:(NSURLRequest *)request + withDelegate:(id)delegate; + +@optional + +/** + * Not all request types can be cancelled, but this method can be implemented + * for ones that can. It should be used to free up any resources on ongoing + * processes associated with the request. + */ +- (void)cancelRequest:(id)requestToken; + +/** + * If more than one RCTURLRequestHandler responds YES to `canHandleRequest:` + * then `handlerPriority` is used to determine which one to use. The handler + * with the highest priority will be selected. Default priority is zero. If + * two or more valid handlers have the same priority, the selection order is + * undefined. + */ +- (float)handlerPriority; + +@end diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index b620478fa..5150b5c97 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -61,10 +61,6 @@ RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); // Create an NSError in the NCTErrorDomain RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message); -// Dispatch an error + result callback on the main thread. -typedef void (^RCTResultOrErrorBlock)(NSError *error, id result); -RCT_EXTERN void RCTDispatchCallbackOnMainQueue(RCTResultOrErrorBlock callback, NSError *error, id result); - // Convert nil values to NSNull, and vice-versa RCT_EXTERN id RCTNullIfNil(id value); RCT_EXTERN id RCTNilIfNull(id value); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index fb79799a8..a9522ad8c 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -289,17 +289,6 @@ NSError *RCTErrorWithMessage(NSString *message) return error; } -void RCTDispatchCallbackOnMainQueue(RCTResultOrErrorBlock callback, NSError *error, id result) -{ - if ([NSThread isMainThread]) { - callback(error, result); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - callback(error, result); - }); - } -} - id RCTNullIfNil(id value) { return value ?: (id)kCFNull; diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 3c0cfe722..513773b6d 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -101,6 +101,8 @@ 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+CoreLocation.m"; sourceTree = ""; }; 13456E941ADAD482009F94A7 /* RCTConvert+MapKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+MapKit.h"; sourceTree = ""; }; 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+MapKit.m"; sourceTree = ""; }; + 1345A83A1B265A0E00583190 /* RCTURLRequestDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTURLRequestDelegate.h; sourceTree = ""; }; + 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTURLRequestHandler.h; sourceTree = ""; }; 134FCB391A6E7F0800051CC8 /* RCTContextExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTContextExecutor.h; sourceTree = ""; }; 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutor.m; sourceTree = ""; }; 134FCB3B1A6E7F0800051CC8 /* RCTWebViewExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebViewExecutor.h; sourceTree = ""; }; @@ -396,6 +398,8 @@ 83CBBA491A601E3B00E9B192 /* Base */ = { isa = PBXGroup; children = ( + 1345A83A1B265A0E00583190 /* RCTURLRequestDelegate.h */, + 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */, 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */, 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */, 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */,