diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index b7ea1b3..253de3b 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -63,8 +63,8 @@ public class CordovaHttpPlugin extends CordovaPlugin { return this.executeHttpRequestWithData(action, args, callbackContext); } else if ("patch".equals(action)) { return this.executeHttpRequestWithData(action, args, callbackContext); - } else if ("uploadFile".equals(action)) { - return this.uploadFile(args, callbackContext); + } else if ("uploadFiles".equals(action)) { + return this.uploadFiles(args, callbackContext); } else if ("downloadFile".equals(action)) { return this.downloadFile(args, callbackContext); } else if ("setServerTrustMode".equals(action)) { @@ -112,17 +112,17 @@ public class CordovaHttpPlugin extends CordovaPlugin { return true; } - private boolean uploadFile(final JSONArray args, final CallbackContext callbackContext) throws JSONException { + private boolean uploadFiles(final JSONArray args, final CallbackContext callbackContext) throws JSONException { String url = args.getString(0); JSONObject headers = args.getJSONObject(1); - String filePath = args.getString(2); - String uploadName = args.getString(3); + JSONArray filePaths = args.getJSONArray(2); + JSONArray uploadNames = args.getJSONArray(3); int timeout = args.getInt(4) * 1000; boolean followRedirect = args.getBoolean(5); String responseType = args.getString(6); - CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePath, uploadName, timeout, followRedirect, - responseType, this.tlsConfiguration, callbackContext); + CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect, + responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), callbackContext); cordova.getThreadPool().execute(upload); diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java index 9d74736..dcbcd06 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java @@ -1,44 +1,92 @@ package com.silkimen.cordovahttp; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.OpenableColumns; import android.webkit.MimeTypeMap; import com.silkimen.http.HttpRequest; +import com.silkimen.http.TLSConfiguration; import java.io.File; +import java.io.InputStream; import java.net.URI; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; -import com.silkimen.http.TLSConfiguration; - import org.apache.cordova.CallbackContext; +import org.json.JSONArray; import org.json.JSONObject; class CordovaHttpUpload extends CordovaHttpBase { - private String filePath; - private String uploadName; + private JSONArray filePaths; + private JSONArray uploadNames; + private Context applicationContext; - public CordovaHttpUpload(String url, JSONObject headers, String filePath, String uploadName, int timeout, + public CordovaHttpUpload(String url, JSONObject headers, JSONArray filePaths, JSONArray uploadNames, int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, - CallbackContext callbackContext) { + Context applicationContext, CallbackContext callbackContext) { super("POST", url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); - this.filePath = filePath; - this.uploadName = uploadName; + this.filePaths = filePaths; + this.uploadNames = uploadNames; + this.applicationContext = applicationContext; } @Override protected void sendBody(HttpRequest request) throws Exception { - int filenameIndex = this.filePath.lastIndexOf('/'); - String filename = this.filePath.substring(filenameIndex + 1); + for (int i = 0; i < this.filePaths.length(); ++i) { + String uploadName = this.uploadNames.getString(i); + String filePath = this.filePaths.getString(i); - int extIndex = this.filePath.lastIndexOf('.'); - String ext = this.filePath.substring(extIndex + 1); + Uri fileUri = Uri.parse(filePath); + + // File Scheme + if (ContentResolver.SCHEME_FILE.equals(fileUri.getScheme())) { + File file = new File(new URI(filePath)); + String fileName = file.getName().trim(); + String mimeType = this.getMimeTypeFromFileName(fileName); + + request.part(uploadName, fileName, mimeType, file); + } + + // Content Scheme + if (ContentResolver.SCHEME_CONTENT.equals(fileUri.getScheme())) { + InputStream inputStream = this.applicationContext.getContentResolver().openInputStream(fileUri); + String fileName = this.getFileNameFromContentScheme(fileUri, this.applicationContext).trim(); + String mimeType = this.getMimeTypeFromFileName(fileName); + + request.part(uploadName, fileName, mimeType, inputStream); + } + } + } + + private String getFileNameFromContentScheme(Uri contentSchemeUri, Context applicationContext) { + Cursor returnCursor = applicationContext.getContentResolver().query(contentSchemeUri, null, null, null, null); + + if (returnCursor == null || !returnCursor.moveToFirst()) { + return null; + } + + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + String fileName = returnCursor.getString(nameIndex); + returnCursor.close(); + + return fileName; + } + + private String getMimeTypeFromFileName(String fileName) { + if (fileName == null || !fileName.contains(".")) { + return null; + } MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); - String mimeType = mimeTypeMap.getMimeTypeFromExtension(ext); + int extIndex = fileName.lastIndexOf('.') + 1; + String extension = fileName.substring(extIndex).toLowerCase(); - request.part(this.uploadName, filename, mimeType, new File(new URI(this.filePath))); + return mimeTypeMap.getMimeTypeFromExtension(extension); } } diff --git a/src/browser/cordova-http-plugin.js b/src/browser/cordova-http-plugin.js index aecc81b..721a1b9 100644 --- a/src/browser/cordova-http-plugin.js +++ b/src/browser/cordova-http-plugin.js @@ -205,6 +205,9 @@ var browserInterface = { uploadFile: function (success, failure, opts) { return failure('advanced-http: function "uploadFile" not supported on browser platform'); }, + uploadFiles: function (success, failure, opts) { + return failure('advanced-http: function "uploadFiles" not supported on browser platform'); + }, downloadFile: function (success, failure, opts) { return failure('advanced-http: function "downloadFile" not supported on browser platform'); }, diff --git a/test/e2e-specs.js b/test/e2e-specs.js index 6be505a..082a2b4 100644 --- a/test/e2e-specs.js +++ b/test/e2e-specs.js @@ -349,6 +349,43 @@ const tests = [ .should.be.equal(fileContent); } }, + { + description: 'should upload multiple files from given paths in local filesystem to given URL #127', + 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 fileName2 = 'test-file2.txt'; + + var fileContent = 'I am a dummy file. I am used for testing purposes!'; + var fileContent2 = 'I am the second dummy file. I am used for testing purposes!'; + + var sourcePath = cordova.file.cacheDirectory + fileName; + var sourcePath2 = cordova.file.cacheDirectory + fileName2; + + var targetUrl = 'http://httpbin.org/post'; + + helpers.writeToFile(function () { + helpers.writeToFile(function() { + cordova.plugin.http.uploadFile(targetUrl, {}, {}, [sourcePath, sourcePath2], [fileName, fileName2], resolve, reject); + }, fileName2, fileContent2); + }, fileName, fileContent); + }, + validationFunc: function (driver, result) { + var fileName = 'test-file.txt'; + var fileName2 = 'test-file2.txt'; + + var fileContent = 'I am a dummy file. I am used for testing purposes!'; + var fileContent2 = 'I am the second dummy file. I am used for testing purposes!'; + + result.type.should.be.equal('resolved'); + result.data.data.should.be.a('string'); + + var parsed = JSON.parse(result.data.data); + + parsed.files[fileName].should.be.equal(fileContent); + parsed.files[fileName2].should.be.equal(fileContent2); + } + }, { description: 'should encode HTTP array params correctly (GET) #45', expected: 'resolved: {"status": 200, "data": "{\\"url\\":\\"http://httpbin.org/get?myArray[]=val1&myArray[]=val2&myArray[]=val3\\"}\" ...', diff --git a/test/js-specs.js b/test/js-specs.js index 2afe7d1..823c944 100644 --- a/test/js-specs.js +++ b/test/js-specs.js @@ -476,7 +476,7 @@ describe('Common helpers', function () { }); }); - describe('checkFileOptions()', function() { + describe('checkUploadFileOptions()', function() { const jsUtil = require('../www/js-util'); const messages = require('../www/messages'); const helpers = require('../www/helpers')(jsUtil, null, messages, null, null); @@ -487,22 +487,25 @@ describe('Common helpers', function () { names: ['ScreenCapture'] }; - helpers.checkFileOptions(opts.filePaths, opts.names).should.be.eql(opts); + // string values + helpers.checkUploadFileOptions(opts.filePaths[0], opts.names[0]).should.be.eql(opts); + // string array values + helpers.checkUploadFileOptions(opts.filePaths, opts.names).should.be.eql(opts); }); it('throws an error when file options are missing', () => { - (() => helpers.checkFileOptions(undefined, ['ScreenCapture'])).should.throw(messages.FILE_PATHS_TYPE_MISMATCH); - (() => helpers.checkFileOptions(['file://path/to/file.png'], undefined)).should.throw(messages.NAMES_TYPE_MISMATCH); + (() => helpers.checkUploadFileOptions(undefined, ['ScreenCapture'])).should.throw(messages.FILE_PATHS_TYPE_MISMATCH); + (() => helpers.checkUploadFileOptions(['file://path/to/file.png'], undefined)).should.throw(messages.NAMES_TYPE_MISMATCH); }); it('throws an error when file options contains empty arrays', () => { - (() => helpers.checkFileOptions([], ['ScreenCapture'])).should.throw(messages.EMPTY_FILE_PATHS); - (() => helpers.checkFileOptions(['file://path/to/file.png'], [])).should.throw(messages.EMPTY_NAMES); + (() => helpers.checkUploadFileOptions([], ['ScreenCapture'])).should.throw(messages.EMPTY_FILE_PATHS); + (() => helpers.checkUploadFileOptions(['file://path/to/file.png'], [])).should.throw(messages.EMPTY_NAMES); }); it('throws an error when file options contains invalid values', () => { - (() => helpers.checkFileOptions([1], ['ScreenCapture'])).should.throw(messages.FILE_PATHS_TYPE_MISMATCH); - (() => helpers.checkFileOptions(['file://path/to/file.png'], [1])).should.throw(messages.NAMES_TYPE_MISMATCH); + (() => helpers.checkUploadFileOptions([1], ['ScreenCapture'])).should.throw(messages.FILE_PATHS_TYPE_MISMATCH); + (() => helpers.checkUploadFileOptions(['file://path/to/file.png'], [1])).should.throw(messages.NAMES_TYPE_MISMATCH); }); }); }) diff --git a/www/helpers.js b/www/helpers.js index 58038c2..1562357 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -9,13 +9,14 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod b64EncodeUnicode: b64EncodeUnicode, checkClientAuthMode: checkClientAuthMode, checkClientAuthOptions: checkClientAuthOptions, - checkUploadFileOptions: checkUploadFileOptions, + checkDownloadFilePath: checkDownloadFilePath, checkFollowRedirectValue: checkFollowRedirectValue, checkForBlacklistedHeaderKey: checkForBlacklistedHeaderKey, checkForInvalidHeaderValue: checkForInvalidHeaderValue, checkSerializer: checkSerializer, checkSSLCertMode: checkSSLCertMode, checkTimeoutValue: checkTimeoutValue, + checkUploadFileOptions: checkUploadFileOptions, getMergedHeaders: getMergedHeaders, getProcessedData: getProcessedData, handleMissingCallbacks: handleMissingCallbacks, @@ -217,7 +218,23 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod return checkKeyValuePairObject(params, ['String', 'Array'], messages.TYPE_MISMATCH_PARAMS); } + function checkDownloadFilePath(filePath) { + if (!filePath || jsUtil.getTypeOf(filePath) !== 'String') { + throw new Error(messages.INVALID_DOWNLOAD_FILE_PATH); + } + + return filePath; + } + function checkUploadFileOptions(filePaths, names) { + if (jsUtil.getTypeOf(filePaths) === 'String') { + filePaths = [filePaths]; + } + + if (jsUtil.getTypeOf(names) === 'String') { + names = [names]; + } + var opts = { filePaths: checkArray(filePaths, ['String'], messages.TYPE_MISMATCH_FILE_PATHS), names: checkArray(names, ['String'], messages.TYPE_MISMATCH_NAMES) @@ -381,13 +398,11 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod return { data: jsUtil.getTypeOf(options.data) === 'Undefined' ? null : options.data, - filePath: options.filePath || '', - filePaths: options.filePaths || [], + filePath: options.filePath, followRedirect: checkFollowRedirectValue(options.followRedirect || globals.followRedirect), headers: checkHeadersObject(options.headers || {}), method: checkHttpMethod(options.method || validHttpMethods[0]), - name: options.name || '', - names: options.names || [], + name: options.name, params: checkParamsObject(options.params || {}), responseType: checkResponseType(options.responseType || validResponseTypes[0]), serializer: checkSerializer(options.serializer || globals.serializer), diff --git a/www/messages.js b/www/messages.js index 843327f..55489c3 100644 --- a/www/messages.js +++ b/www/messages.js @@ -8,6 +8,7 @@ module.exports = { INVALID_CLIENT_AUTH_PKCS_PASSWORD: 'advanced-http: invalid PKCS12 container password, needs to be a string, ', INVALID_CLIENT_AUTH_RAW_PKCS: 'advanced-http: invalid PKCS12 container, needs to be an array buffer, ', INVALID_DATA_SERIALIZER: 'advanced-http: invalid serializer, supported serializers are:', + INVALID_DOWNLOAD_FILE_PATH: 'advanced-http: invalid "filePath" value, needs to be a string, ', INVALID_FOLLOW_REDIRECT_VALUE: 'advanced-http: invalid follow redirect value, needs to be a boolean value, ', INVALID_HEADER_VALUE: 'advanced-http: invalid header value, needs to be a string, ', INVALID_HTTP_METHOD: 'advanced-http: invalid HTTP method, supported methods are:', diff --git a/www/public-interface.js b/www/public-interface.js index 20d39fd..1018504 100644 --- a/www/public-interface.js +++ b/www/public-interface.js @@ -28,7 +28,6 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf delete: del, head: head, uploadFile: uploadFile, - uploadFiles: uploadFiles, downloadFile: downloadFile, ErrorCode: errorCodes }; @@ -156,11 +155,12 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf var data = helpers.getProcessedData(options.data, options.serializer); return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout, options.followRedirect, options.responseType]); case 'upload': - var fileOptions = helpers.checkUploadFileOptions(options.filePaths || [options.filePath], options.names || [options.name]); - return exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFile', [url, headers, fileOptions.filePaths, fileOptions.names, options.timeout, options.followRedirect, options.responseType]); + var fileOptions = helpers.checkUploadFileOptions(options.filePath, options.name); + return exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.timeout, options.followRedirect, options.responseType]); case 'download': + var filePath = helpers.checkDownloadFilePath(options.filePath); var onDownloadSuccess = helpers.injectCookieHandler(url, helpers.injectFileEntryHandler(success)); - return exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, options.filePath, options.timeout, options.followRedirect]); + return exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, filePath, options.timeout, options.followRedirect]); default: return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect, options.responseType]); } @@ -194,10 +194,6 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf return publicInterface.sendRequest(url, { method: 'upload', params: params, headers: headers, filePath: filePath, name: name }, success, failure); } - function uploadFiles(url, params, headers, filePaths, names, success, failure) { - return publicInterface.sendRequest(url, { method: 'upload', params: params, headers: headers, filePaths: filePaths, names: names }, success, failure); - } - function downloadFile(url, params, headers, filePath, success, failure) { return publicInterface.sendRequest(url, { method: 'download', params: params, headers: headers, filePath: filePath }, success, failure); }