diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index 7ff8c6379..760fce614 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -82,8 +82,8 @@ static NSString *RCTCacheKeyForURL(NSURL *url) RCTImageDownloader *strongSelf = weakSelf; NSArray *blocks = strongSelf->_pendingBlocks[cacheKey]; [strongSelf->_pendingBlocks removeObjectForKey:cacheKey]; - for (RCTCachedDataDownloadBlock cacheDownloadBlock in blocks) { - cacheDownloadBlock(cached, data, error); + for (RCTCachedDataDownloadBlock downloadBlock in blocks) { + downloadBlock(cached, data, error); } }); }; diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index 186a53cd1..4d23a628e 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -23,4 +23,6 @@ + (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 7525b37d0..1fda32191 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -19,6 +19,7 @@ #import "RCTGIFImage.h" #import "RCTImageDownloader.h" #import "RCTLog.h" +#import "RCTUtils.h" static dispatch_queue_t RCTImageLoaderQueue(void) { @@ -31,24 +32,6 @@ static dispatch_queue_t RCTImageLoaderQueue(void) return queue; } -static NSError *RCTErrorWithMessage(NSString *message) -{ - NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message}; - NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; - return error; -} - -static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSError *error, UIImage *image) -{ - if ([NSThread isMainThread]) { - callback(error, image); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - callback(error, image); - }); - } -} - @implementation RCTImageLoader + (ALAssetsLibrary *)assetsLibrary @@ -154,4 +137,11 @@ static void RCTDispatchCallbackOnMainQueue(void (^callback)(NSError *, id), NSEr } } ++ (BOOL)isSystemImageURI:(NSString *)uri +{ + return uri != nil && ( + [uri hasPrefix:@"assets-library"] || + [uri hasPrefix:@"ph://"]); +} + @end diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index f9fe1523d..e87f44fca 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -102,6 +102,7 @@ function setUpXHR() { // The native XMLHttpRequest in Chrome dev tools is CORS aware and won't // let you fetch anything from the internet GLOBAL.XMLHttpRequest = require('XMLHttpRequest'); + GLOBAL.FormData = require('FormData'); var fetchPolyfill = require('fetch'); GLOBAL.fetch = fetchPolyfill.fetch; diff --git a/Libraries/Network/FormData.js b/Libraries/Network/FormData.js new file mode 100644 index 000000000..6e44f6193 --- /dev/null +++ b/Libraries/Network/FormData.js @@ -0,0 +1,67 @@ +/** + * 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. + * + * @providesModule FormData + * @flow + */ +'use strict'; + +type FormDataValue = any; +type FormDataPart = [string, FormDataValue]; + +/** + * Polyfill for XMLHttpRequest2 FormData API, allowing multipart POST requests + * with mixed data (string, native files) to be submitted via XMLHttpRequest. + */ +class FormData { + _parts: Array; + _partsByKey: {[key: string]: FormDataPart}; + + constructor() { + this._parts = []; + this._partsByKey = {}; + } + + append(key: string, value: FormDataValue) { + var parts = this._partsByKey[key]; + if (parts) { + // It's a bit unclear what the behaviour should be in this case. + // The XMLHttpRequest spec doesn't specify it, while MDN says that + // the any new values should appended to existing values. We're not + // doing that for now -- it's tedious and doesn't seem worth the effort. + parts[1] = value; + return; + } + parts = [key, value]; + this._parts.push(parts); + this._partsByKey[key] = parts; + } + + getParts(): Array { + return this._parts.map(([name, value]) => { + if (typeof value === 'string') { + return { + string: value, + headers: { + 'content-disposition': 'form-data; name="' + name + '"', + }, + }; + } + var contentDisposition = 'form-data; name="' + name + '"'; + if (typeof value.name === 'string') { + contentDisposition += '; filename="' + value.name + '"'; + } + return { + ...value, + headers: {'content-disposition': contentDisposition}, + }; + }); + } +} + +module.exports = FormData; diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m index 35400955b..135214106 100644 --- a/Libraries/Network/RCTDataManager.m +++ b/Libraries/Network/RCTDataManager.m @@ -11,19 +11,13 @@ #import "RCTAssert.h" #import "RCTConvert.h" +#import "RCTDataQuery.h" #import "RCTEventDispatcher.h" +#import "RCTHTTPQueryExecutor.h" #import "RCTLog.h" #import "RCTUtils.h" -@interface RCTDataManager () - -@end - @implementation RCTDataManager -{ - NSURLSession *_session; - NSOperationQueue *_callbackQueue; -} @synthesize bridge = _bridge; @@ -38,119 +32,23 @@ RCT_EXPORT_METHOD(queryData:(NSString *)queryType sendIncrementalUpdates:(BOOL)incrementalUpdates responseSender:(RCTResponseSenderBlock)responseSender) { + id executor = nil; if ([queryType isEqualToString:@"http"]) { - - // 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"]]; - request.HTTPBody = [RCTConvert NSData:query[@"data"]]; - - // 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 (incrementalUpdates) { - 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]; - + executor = [RCTHTTPQueryExecutor sharedInstance]; } else { - RCTLogError(@"unsupported query type %@", queryType); - } -} - -#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]; -} + RCTAssert(executor != nil, @"executor must be defined"); -static void RCTSendCompletionEvent(RCTBridge *bridge, NSURLSessionTask *task, NSError *error) -{ - NSArray *responseJSON = @[@(task.taskIdentifier), - error.localizedDescription ?: [NSNull null], - ]; - - [bridge.eventDispatcher sendDeviceEventWithName:@"didCompleteNetworkResponse" - body:responseJSON]; + if ([executor respondsToSelector:@selector(setBridge:)]) { + executor.bridge = _bridge; + } + if ([executor respondsToSelector:@selector(setSendIncrementalUpdates:)]) { + executor.sendIncrementalUpdates = incrementalUpdates; + } + [executor addQuery:query responseSender:responseSender]; } @end diff --git a/Libraries/Network/RCTDataQuery.h b/Libraries/Network/RCTDataQuery.h new file mode 100644 index 000000000..4588cbbe4 --- /dev/null +++ b/Libraries/Network/RCTDataQuery.h @@ -0,0 +1,21 @@ +/** + * 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" + +@protocol RCTDataQueryExecutor + +- (void)addQuery:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender; + +@optional + +@property (nonatomic, weak) RCTBridge *bridge; +@property (nonatomic, assign) BOOL sendIncrementalUpdates; + +@end diff --git a/Libraries/Network/RCTHTTPQueryExecutor.h b/Libraries/Network/RCTHTTPQueryExecutor.h new file mode 100644 index 000000000..196a4da5f --- /dev/null +++ b/Libraries/Network/RCTHTTPQueryExecutor.h @@ -0,0 +1,39 @@ +/** + * 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 new file mode 100644 index 000000000..7eb858f18 --- /dev/null +++ b/Libraries/Network/RCTHTTPQueryExecutor.m @@ -0,0 +1,314 @@ +/** + * 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/RCTNetwork.xcodeproj/project.pbxproj b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj index 1dca7fe6d..bc846292e 100644 --- a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj +++ b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj @@ -8,6 +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 */; }; 58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTDataManager.m */; }; /* End PBXBuildFile section */ @@ -26,6 +27,9 @@ /* 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 = ""; }; 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 = ""; }; @@ -47,6 +51,9 @@ children = ( 58B512061A9E6CE300147676 /* RCTDataManager.h */, 58B512071A9E6CE300147676 /* RCTDataManager.m */, + 352DA0B51B17855800AA15A8 /* RCTDataQuery.h */, + 352DA0B71B17855800AA15A8 /* RCTHTTPQueryExecutor.h */, + 352DA0B81B17855800AA15A8 /* RCTHTTPQueryExecutor.m */, 1372B7351AB03E7B00659ED6 /* RCTReachability.h */, 1372B7361AB03E7B00659ED6 /* RCTReachability.m */, 58B511DC1A9E6C8500147676 /* Products */, @@ -121,6 +128,7 @@ files = ( 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */, 58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */, + 352DA0BA1B17855800AA15A8 /* RCTHTTPQueryExecutor.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -209,6 +217,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../Image/**", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; @@ -226,6 +235,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", + "$(SRCROOT)/../Image/**", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index 9344d43cc..5a5a29a9f 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -11,6 +11,7 @@ */ 'use strict'; +var FormData = require('FormData'); var RCTDataManager = require('NativeModules').DataManager; var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); @@ -82,13 +83,19 @@ class XMLHttpRequest extends XMLHttpRequestBase { } sendImpl(method: ?string, url: ?string, headers: Object, data: any): void { + if (typeof data === 'string') { + data = {string: data}; + } + if (data instanceof FormData) { + data = {formData: data.getParts()}; + } RCTDataManager.queryData( 'http', { - method: method, - url: url, - data: data, - headers: headers, + method, + url, + data, + headers, }, this.onreadystatechange ? true : false, this._didCreateRequest.bind(this) diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 366e8aa55..b620478fa 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -58,5 +58,13 @@ RCT_EXTERN BOOL RCTRunningInTestEnvironment(void); // Return YES if image has an alpha component 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 7a31cf768..fb79799a8 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -282,6 +282,24 @@ BOOL RCTImageHasAlpha(CGImageRef image) } } +NSError *RCTErrorWithMessage(NSString *message) +{ + NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message}; + NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; + 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;