Permalink
Browse files

[ReactNative] Allow uploading native files (e.g. photos) and FormData…

… via XMLHttpRequest
  • Loading branch information...
nicklockwood committed Jun 9, 2015
1 parent f590a8b commit f4bf80f3ea3b7ed6aee8b068ec1a289e0965eb5e
@@ -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);
}
});
};
@@ -23,4 +23,6 @@
+ (void)loadImageWithTag:(NSString *)tag
callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback;
+ (BOOL)isSystemImageURI:(NSString *)uri;
@end
@@ -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 @@ + (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error,
}
}
+ (BOOL)isSystemImageURI:(NSString *)uri
{
return uri != nil && (
[uri hasPrefix:@"assets-library"] ||
[uri hasPrefix:@"ph://"]);
}
@end
@@ -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;
@@ -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;
@@ -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;
@@ -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
@@ -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
@@ -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
Oops, something went wrong.

2 comments on commit f4bf80f

@lwansbrough

This comment has been minimized.

Show comment
Hide comment
@lwansbrough

lwansbrough Jun 13, 2015

Contributor

Pretty surprised to see this doesn't support the regular file: protocol. Any reason for that?

Contributor

lwansbrough replied Jun 13, 2015

Pretty surprised to see this doesn't support the regular file: protocol. Any reason for that?

@nicklockwood

This comment has been minimized.

Show comment
Hide comment
@nicklockwood

nicklockwood Jun 14, 2015

Contributor

I believe this commit rectifies that limitation (although I haven't tried uploading any non-image files): f88bc3e

Contributor

nicklockwood replied Jun 14, 2015

I believe this commit rectifies that limitation (although I haven't tried uploading any non-image files): f88bc3e

Please sign in to comment.