Skip to content

Commit

Permalink
[ReactNative] Allow uploading native files (e.g. photos) and FormData…
Browse files Browse the repository at this point in the history
… via XMLHttpRequest
  • Loading branch information
nicklockwood committed Jun 9, 2015
1 parent f590a8b commit f4bf80f
Show file tree
Hide file tree
Showing 13 changed files with 512 additions and 137 deletions.
4 changes: 2 additions & 2 deletions Libraries/Image/RCTImageDownloader.m
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ - (id)_downloadDataForURL:(NSURL *)url block:(RCTCachedDataDownloadBlock)block
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);
}
});
};
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Image/RCTImageLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@
+ (void)loadImageWithTag:(NSString *)tag
callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;

+ (BOOL)isSystemImageURI:(NSString *)uri;

@end
26 changes: 8 additions & 18 deletions Libraries/Image/RCTImageLoader.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#import "RCTGIFImage.h"
#import "RCTImageDownloader.h"
#import "RCTLog.h"
#import "RCTUtils.h"

static dispatch_queue_t RCTImageLoaderQueue(void)
{
Expand All @@ -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
Expand Down Expand Up @@ -154,4 +137,11 @@ + (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error,
}
}

+ (BOOL)isSystemImageURI:(NSString *)uri
{
return uri != nil && (
[uri hasPrefix:@"assets-library"] ||
[uri hasPrefix:@"ph://"]);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
67 changes: 67 additions & 0 deletions Libraries/Network/FormData.js
Original file line number Diff line number Diff line change
@@ -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<FormDataPart>;
_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<FormDataValue> {
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;
124 changes: 11 additions & 113 deletions Libraries/Network/RCTDataManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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 () <NSURLSessionDataDelegate>

@end

@implementation RCTDataManager
{
NSURLSession *_session;
NSOperationQueue *_callbackQueue;
}

@synthesize bridge = _bridge;

Expand All @@ -38,119 +32,23 @@ @implementation RCTDataManager
sendIncrementalUpdates:(BOOL)incrementalUpdates
responseSender:(RCTResponseSenderBlock)responseSender)
{
id<RCTDataQueryExecutor> 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);
return;
}
}

#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];
}
RCTAssert(executor != nil, @"executor must be defined");

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);
if ([executor respondsToSelector:@selector(setBridge:)]) {
executor.bridge = _bridge;
}

NSString *responseText = [[NSString alloc] initWithData:data encoding:encoding];
if (!responseText && data.length) {
RCTLogError(@"Received data was invalid.");
return;
if ([executor respondsToSelector:@selector(setSendIncrementalUpdates:)]) {
executor.sendIncrementalUpdates = incrementalUpdates;
}

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];
[executor addQuery:query responseSender:responseSender];
}

@end
21 changes: 21 additions & 0 deletions Libraries/Network/RCTDataQuery.h
Original file line number Diff line number Diff line change
@@ -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 <NSObject>

- (void)addQuery:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender;

@optional

@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, assign) BOOL sendIncrementalUpdates;

@end
39 changes: 39 additions & 0 deletions Libraries/Network/RCTHTTPQueryExecutor.h
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>

#import "RCTDataQuery.h"

@interface RCTHTTPQueryExecutor : NSObject <RCTDataQueryExecutor>

+ (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
Loading

0 comments on commit f4bf80f

Please sign in to comment.