From 767f638635975822633461552703b7b434fa7a13 Mon Sep 17 00:00:00 2001 From: Tobias Becht Date: Wed, 8 Feb 2023 17:25:57 +0100 Subject: [PATCH 1/4] feat: upload file without formdata --- .../cordovahttp/CordovaHttpPlugin.java | 9 +- .../cordovahttp/CordovaHttpUpload.java | 24 +++- src/ios/CordovaHttpPlugin.m | 135 +++++++++++++----- www/public-interface.js | 8 +- 4 files changed, 137 insertions(+), 39 deletions(-) diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index 9a7d34ad..52716b54 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -163,12 +163,17 @@ private boolean uploadFiles(final JSONArray args, final CallbackContext callback int readTimeout = args.getInt(5) * 1000; boolean followRedirect = args.getBoolean(6); String responseType = args.getString(7); - Integer reqId = args.getInt(8); + JSONObject transmitOptions = args.getJSONObject(8); + Integer reqId = args.getInt(9); + + // new file transmission options + String transmitFileType = transmitOptions.getString("transmitFileAs"); + boolean submitRaw = transmitFileType.equalsIgnoreCase("BINARY"); CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, connectTimeout, readTimeout, followRedirect, - responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), observableCallbackContext); + responseType, this.tlsConfiguration, submitRaw, this.cordova.getActivity().getApplicationContext(), observableCallbackContext); startRequest(reqId, observableCallbackContext, upload); diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java index d37464d2..ef74c1a1 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java @@ -25,18 +25,40 @@ class CordovaHttpUpload extends CordovaHttpBase { private JSONArray uploadNames; private Context applicationContext; + private boolean submitRaw = false; + public CordovaHttpUpload(String url, JSONObject headers, JSONArray filePaths, JSONArray uploadNames, int connectTimeout, int readTimeout, - boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, + boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, boolean submitRaw, Context applicationContext, CordovaObservableCallbackContext callbackContext) { super("POST", url, headers, connectTimeout, readTimeout, followRedirects, responseType, tlsConfiguration, callbackContext); this.filePaths = filePaths; this.uploadNames = uploadNames; this.applicationContext = applicationContext; + this.submitRaw = submitRaw; } @Override protected void sendBody(HttpRequest request) throws Exception { + if (this.submitRaw) { + if (this.filePaths.length() != 1) { + throw new IllegalArgumentException("Can only transmit a single file. Multiple files are not supported in this mode."); + } + + String filePath = this.filePaths.getString(0); + Uri fileURI = Uri.parse(filePath); + + if (ContentResolver.SCHEME_FILE.equals((fileURI.getScheme()))) { + File file = new File(new URI(filePath)); + request.send(file); + } else if (ContentResolver.SCHEME_CONTENT.equals(fileURI.getScheme())) { + InputStream inputStream = this.applicationContext.getContentResolver().openInputStream(fileURI); + request.send(inputStream); + } + + return; + } + for (int i = 0; i < this.filePaths.length(); ++i) { String uploadName = this.uploadNames.getString(i); String filePath = this.filePaths.getString(i); diff --git a/src/ios/CordovaHttpPlugin.m b/src/ios/CordovaHttpPlugin.m index 97c98e01..58699d4a 100644 --- a/src/ios/CordovaHttpPlugin.m +++ b/src/ios/CordovaHttpPlugin.m @@ -460,62 +460,127 @@ - (void)uploadFiles:(CDVInvokedUrlCommand*)command { NSDictionary *headers = [command.arguments objectAtIndex:1]; NSArray *filePaths = [command.arguments objectAtIndex: 2]; NSArray *names = [command.arguments objectAtIndex: 3]; - NSTimeInterval connectTimeout = [[command.arguments objectAtIndex:4] doubleValue]; + NSTimeInterval _connectTimeout = [[command.arguments objectAtIndex:4] doubleValue]; NSTimeInterval readTimeout = [[command.arguments objectAtIndex:5] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:6] boolValue]; NSString *responseType = [command.arguments objectAtIndex:7]; - NSNumber *reqId = [command.arguments objectAtIndex:8]; - - [self setRequestHeaders: headers forManager: manager]; - [self setTimeout:readTimeout forManager:manager]; - [self setRedirect:followRedirect forManager:manager]; - [self setResponseSerializer:responseType forManager:manager]; + NSDictionary *transmitOptions = [command.arguments objectAtIndex:8]; + NSNumber *reqId = [command.arguments objectAtIndex:9]; + + NSString *transmitFileType = [transmitOptions objectForKey:@"transmitFileAs"]; CordovaHttpPlugin* __weak weakSelf = self; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; - @try { - NSURLSessionDataTask *task = [manager POST:url parameters:nil constructingBodyWithBlock:^(id formData) { - NSError *error; - for (int i = 0; i < [filePaths count]; i++) { - NSString *filePath = (NSString *) [filePaths objectAtIndex:i]; - NSString *uploadName = (NSString *) [names objectAtIndex:i]; - NSURL *fileURL = [NSURL URLWithString: filePath]; - [formData appendPartWithFileURL:fileURL name:uploadName error:&error]; - } - if (error) { - [weakSelf removeRequest:reqId]; - - NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; - [dictionary setObject:[NSNumber numberWithInt:500] forKey:@"status"]; - [dictionary setObject:@"Could not add file to post body." forKey:@"error"]; - CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; - [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; - [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; - return; - } - } progress:nil success:^(NSURLSessionTask *task, id responseObject) { + @try + { + NSURLSessionDataTask* task; + + void (^setupManager)(void) = ^() { + [self setRequestHeaders: headers forManager: manager]; + [self setTimeout:readTimeout forManager:manager]; + [self setRedirect:followRedirect forManager:manager]; + [self setResponseSerializer:responseType forManager:manager]; + }; + + void (^onFailure)(NSURLSessionTask *, NSError *) = ^(NSURLSessionTask *task, NSError *error) { [weakSelf removeRequest:reqId]; NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; - [self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject]; + [self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error]; - CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; - } failure:^(NSURLSessionTask *task, NSError *error) { + [manager invalidateSessionCancelingTasks:YES]; + }; + + void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) { [weakSelf removeRequest:reqId]; NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; - [self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error]; + [self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject]; - CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary]; [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; - }]; + [manager invalidateSessionCancelingTasks:YES]; + }; + + bool submitRaw = [transmitFileType isEqualToString:@"BINARY"]; + + // RAW + if (submitRaw) + { + if ([filePaths count] == 1) + { + // setup the request serializer to submit the raw file content + manager.requestSerializer = [BinaryRequestSerializer serializer]; + setupManager(); + + NSURL *fileURL = [NSURL URLWithString:[filePaths objectAtIndex:0]]; + NSError *error; + NSData *fileData = [NSData dataWithContentsOfURL:fileURL options:0 error:&error]; + + if (error) + { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + [self handleError:dictionary withResponse:nil error:error]; + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; + [manager invalidateSessionCancelingTasks:YES]; + return; + } + + task = [manager uploadTaskWithHTTPMethod:@"POST" URLString:url parameters:fileData progress:nil success:onSuccess failure:onFailure]; + } + else + { + [NSException raise:@"ArgumentException" format:@"Can only transmit a single file. Multiple files are not supported in this mode."]; + } + } + else // FALLBACK: Multipart / FormData + { + setupManager(); + task = [manager + POST:url + parameters:nil + constructingBodyWithBlock:^(id formData) + { + NSError *error; + + for (int i = 0; i < [filePaths count]; i++) + { + NSString *filePath = (NSString *) [filePaths objectAtIndex:i]; + NSString *uploadName = (NSString *) [names objectAtIndex:i]; + NSURL *fileURL = [NSURL URLWithString: filePath]; + [formData appendPartWithFileURL:fileURL name:uploadName error:&error]; + } + + if (error) + { + [weakSelf removeRequest:reqId]; + + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + [dictionary setObject:[NSNumber numberWithInt:500] forKey:@"status"]; + [dictionary setObject:@"Could not add file to post body." forKey:@"error"]; + CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary]; + [weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; + return; + } + } + progress:nil + success:onSuccess + failure:onFailure + ]; + } + [self addRequest:reqId forTask:task]; } - @catch (NSException *exception) { + @catch (NSException *exception) + { [[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity]; [self handleException:exception withCommand:command]; } diff --git a/www/public-interface.js b/www/public-interface.js index 66b52612..15fd51b7 100644 --- a/www/public-interface.js +++ b/www/public-interface.js @@ -180,7 +180,13 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf break; case 'upload': var fileOptions = helpers.checkUploadFileOptions(options.filePath, options.name); - exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.connectTimeout, options.readTimeout, options.followRedirect, options.responseType, reqId]); + + // support uploading files as octet-stream / encoded string instead of form-data + var transmitOptions = {}; + transmitOptions.transmitFileAs = options.transmitFileAs || 'FORMDATA'; + // transmitOptions.transmitMethod = options.transmitMethod || 'POST'; + + exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.connectTimeout, options.readTimeout, options.followRedirect, options.responseType, transmitOptions, reqId]); break; case 'download': var filePath = helpers.checkDownloadFilePath(options.filePath); From 4ef17d1af528a7ceefa4b1ca5caf239a421a9e35 Mon Sep 17 00:00:00 2001 From: Tobias Becht Date: Thu, 9 Feb 2023 11:48:37 +0100 Subject: [PATCH 2/4] fix: handle 'missing' options --- www/helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/helpers.js b/www/helpers.js index ed070fe6..a3793947 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -498,7 +498,7 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64, function handleMissingOptions(options, globals) { options = options || {}; - return { + return Object.assign({}, options, { data: jsUtil.getTypeOf(options.data) === 'Undefined' ? null : options.data, filePath: options.filePath, followRedirect: checkFollowRedirectValue(options.followRedirect || globals.followRedirect), @@ -511,6 +511,6 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64, connectTimeout: checkTimeoutValue(options.connectTimeout || globals.connectTimeout), readTimeout: checkTimeoutValue(options.readTimeout || globals.readTimeout), timeout: checkTimeoutValue(options.timeout || globals.timeout) - }; + }); } }; From 52e754b112eeb4e9442c08aab0718ee849174917 Mon Sep 17 00:00:00 2001 From: Sefa Ilkimen Date: Mon, 20 Feb 2023 03:44:27 +0100 Subject: [PATCH 3/4] test: implement tests for #495 --- test/e2e-specs.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/e2e-specs.js b/test/e2e-specs.js index dbe6a679..7867ce2c 100644 --- a/test/e2e-specs.js +++ b/test/e2e-specs.js @@ -1178,6 +1178,34 @@ const tests = [ result.data.should.be.eql({ status: -2, error: messageFactory.handshakeFailed() }); } }, + { + description: 'should upload a file in binary mode from given path in local filesystem to given URL #495', + expected: 'resolved: {"status": 200, "data": "files": {"test-file.txt": "I am a dummy file. I am used ...', + func: function (resolve, reject) { + var fileName = 'test-file.txt'; + var fileContent = 'I am a dummy file. I am used for testing purposes!'; + var sourcePath = cordova.file.cacheDirectory + fileName; + var targetUrl = 'http://httpbin.org/post'; + + var options = { method: 'upload', transmitFileAs: 'BINARY', filePath: sourcePath, name: fileName }; + + helpers.writeToFile(function () { + cordova.plugin.http.sendRequest(targetUrl, options, resolve, reject); + }, fileName, fileContent); + }, + validationFunc: function (driver, result) { + var fileName = 'test-file.txt'; + var fileContent = 'I am a dummy file. I am used for testing purposes!'; + + result.type.should.be.equal('resolved'); + result.data.data.should.be.a('string'); + + JSON + .parse(result.data.data) + .data + .should.be.equal(fileContent); + } + }, ]; if (typeof module !== 'undefined' && module.exports) { From a352fb1cf78b3b457d94439e5761a6551fc1f3c4 Mon Sep 17 00:00:00 2001 From: Sefa Ilkimen Date: Mon, 20 Feb 2023 03:47:31 +0100 Subject: [PATCH 4/4] chore: some cleanup --- www/public-interface.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/www/public-interface.js b/www/public-interface.js index 15fd51b7..84bfb333 100644 --- a/www/public-interface.js +++ b/www/public-interface.js @@ -180,12 +180,11 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf break; case 'upload': var fileOptions = helpers.checkUploadFileOptions(options.filePath, options.name); - + // support uploading files as octet-stream / encoded string instead of form-data var transmitOptions = {}; transmitOptions.transmitFileAs = options.transmitFileAs || 'FORMDATA'; - // transmitOptions.transmitMethod = options.transmitMethod || 'POST'; - + exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.connectTimeout, options.readTimeout, options.followRedirect, options.responseType, transmitOptions, reqId]); break; case 'download':