From 230de366189aab413c399ba470f20585fa5aca93 Mon Sep 17 00:00:00 2001 From: Sefa Ilkimen Date: Tue, 2 Apr 2019 12:04:49 +0200 Subject: [PATCH] WIP: - some refactoring for future features - move params serializer into JS code --- plugin.xml | 11 +- .../silkimen/cordovahttp/CordovaHttpBase.java | 9 +- .../cordovahttp/CordovaHttpDownload.java | 4 +- .../cordovahttp/CordovaHttpOperation.java | 6 +- .../cordovahttp/CordovaHttpPlugin.java | 35 +- .../cordovahttp/CordovaHttpUpload.java | 4 +- test/app-test-definitions.js | 142 +++-- test/js-mocha-specs.js | 181 +++++-- www/advanced-http.js | 125 +---- www/cookie-handler.js | 75 ++- www/global-configs.js | 7 + www/helpers.js | 498 +++++++++--------- www/local-storage-store.js | 138 +++-- www/public-interface.js | 158 ++++++ www/url-util.js | 71 +++ 15 files changed, 864 insertions(+), 600 deletions(-) create mode 100644 www/global-configs.js create mode 100644 www/public-interface.js create mode 100644 www/url-util.js diff --git a/plugin.xml b/plugin.xml index 0ff984d..cc720aa 100644 --- a/plugin.xml +++ b/plugin.xml @@ -8,12 +8,15 @@ - - - - + + + + + + + diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java index 4ecbca5..af2bcfa 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java @@ -31,7 +31,6 @@ abstract class CordovaHttpBase implements Runnable { protected String url; protected String serializer = "none"; protected Object data; - protected JSONObject params; protected JSONObject headers; protected int timeout; protected boolean followRedirects; @@ -55,13 +54,12 @@ abstract class CordovaHttpBase implements Runnable { this.callbackContext = callbackContext; } - public CordovaHttpBase(String method, String url, JSONObject params, JSONObject headers, int timeout, + public CordovaHttpBase(String method, String url, JSONObject headers, int timeout, boolean followRedirects, SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, CallbackContext callbackContext) { this.method = method; this.url = url; - this.params = params; this.headers = headers; this.timeout = timeout; this.followRedirects = followRedirects; @@ -115,10 +113,7 @@ abstract class CordovaHttpBase implements Runnable { } protected HttpRequest createRequest() throws JSONException { - String processedUrl = HttpRequest.encode(HttpRequest.append(this.url, JsonUtils.getObjectMap(this.params))); - HttpRequest request = new HttpRequest(processedUrl, this.method); - - return request; + return new HttpRequest(this.url, this.method); } protected void prepareRequest(HttpRequest request) throws JSONException { diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java index 478a1e0..425faa9 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java @@ -15,11 +15,11 @@ import org.json.JSONObject; class CordovaHttpDownload extends CordovaHttpBase { private String filePath; - public CordovaHttpDownload(String url, JSONObject params, JSONObject headers, String filePath, int timeout, + public CordovaHttpDownload(String url, JSONObject headers, String filePath, int timeout, boolean followRedirects, SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, CallbackContext callbackContext) { - super("GET", url, params, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, + super("GET", url, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, callbackContext); this.filePath = filePath; } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java index fe13578..98c3569 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java @@ -15,11 +15,11 @@ class CordovaHttpOperation extends CordovaHttpBase { customHostnameVerifier, callbackContext); } - public CordovaHttpOperation(String method, String url, JSONObject params, JSONObject headers, int timeout, - boolean followRedirects, SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, + public CordovaHttpOperation(String method, String url, JSONObject headers, int timeout, boolean followRedirects, + SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, CallbackContext callbackContext) { - super(method, url, params, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, + super(method, url, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, callbackContext); } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index 3567693..ccf5745 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -66,7 +66,7 @@ public class CordovaHttpPlugin extends CordovaPlugin { switch (action) { case "get": - return this.executeHttpRequestWithParams(action, args, callbackContext); + return this.executeHttpRequestWithoutData(action, args, callbackContext); case "post": return this.executeHttpRequestWithData(action, args, callbackContext); case "put": @@ -74,9 +74,9 @@ public class CordovaHttpPlugin extends CordovaPlugin { case "patch": return this.executeHttpRequestWithData(action, args, callbackContext); case "head": - return this.executeHttpRequestWithParams(action, args, callbackContext); + return this.executeHttpRequestWithoutData(action, args, callbackContext); case "delete": - return this.executeHttpRequestWithParams(action, args, callbackContext); + return this.executeHttpRequestWithoutData(action, args, callbackContext); case "uploadFile": return this.uploadFile(args, callbackContext); case "downloadFile": @@ -90,15 +90,14 @@ public class CordovaHttpPlugin extends CordovaPlugin { } } - private boolean executeHttpRequestWithParams(final String method, final JSONArray args, + private boolean executeHttpRequestWithoutData(final String method, final JSONArray args, final CallbackContext callbackContext) throws JSONException { String url = args.getString(0); - JSONObject params = args.getJSONObject(1); - JSONObject headers = args.getJSONObject(2); - int timeout = args.getInt(3) * 1000; + JSONObject headers = args.getJSONObject(1); + int timeout = args.getInt(2) * 1000; - CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, params, headers, timeout, + CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, this.followRedirects, this.customSSLSocketFactory, this.customHostnameVerifier, callbackContext); cordova.getThreadPool().execute(request); @@ -125,13 +124,12 @@ public class CordovaHttpPlugin extends CordovaPlugin { private boolean uploadFile(final JSONArray args, final CallbackContext callbackContext) throws JSONException { String url = args.getString(0); - JSONObject params = args.getJSONObject(1); - JSONObject headers = args.getJSONObject(2); - String filePath = args.getString(3); - String uploadName = args.getString(4); - int timeout = args.getInt(5) * 1000; + JSONObject headers = args.getJSONObject(1); + String filePath = args.getString(2); + String uploadName = args.getString(3); + int timeout = args.getInt(4) * 1000; - CordovaHttpUpload upload = new CordovaHttpUpload(url, params, headers, filePath, uploadName, timeout, + CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePath, uploadName, timeout, this.followRedirects, this.customSSLSocketFactory, this.customHostnameVerifier, callbackContext); cordova.getThreadPool().execute(upload); @@ -141,12 +139,11 @@ public class CordovaHttpPlugin extends CordovaPlugin { private boolean downloadFile(final JSONArray args, final CallbackContext callbackContext) throws JSONException { String url = args.getString(0); - JSONObject params = args.getJSONObject(1); - JSONObject headers = args.getJSONObject(2); - String filePath = args.getString(3); - int timeout = args.getInt(4) * 1000; + JSONObject headers = args.getJSONObject(1); + String filePath = args.getString(2); + int timeout = args.getInt(3) * 1000; - CordovaHttpDownload download = new CordovaHttpDownload(url, params, headers, filePath, timeout, + CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, this.followRedirects, this.customSSLSocketFactory, this.customHostnameVerifier, callbackContext); cordova.getThreadPool().execute(download); diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java index a28e646..77449dc 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java @@ -17,11 +17,11 @@ class CordovaHttpUpload extends CordovaHttpBase { private String filePath; private String uploadName; - public CordovaHttpUpload(String url, JSONObject params, JSONObject headers, String filePath, String uploadName, + public CordovaHttpUpload(String url, JSONObject headers, String filePath, String uploadName, int timeout, boolean followRedirects, SSLSocketFactory customSSLSocketFactory, HostnameVerifier customHostnameVerifier, CallbackContext callbackContext) { - super("POST", url, params, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, + super("POST", url, headers, timeout, followRedirects, customSSLSocketFactory, customHostnameVerifier, callbackContext); this.filePath = filePath; this.uploadName = uploadName; diff --git a/test/app-test-definitions.js b/test/app-test-definitions.js index 7a14111..10f2e01 100644 --- a/test/app-test-definitions.js +++ b/test/app-test-definitions.js @@ -51,7 +51,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } - },{ + }, + { description: 'should reject self signed cert (PUT)', expected: 'rejected: {"status":-2, ...', func: function(resolve, reject) { cordova.plugin.http.put('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, @@ -59,7 +60,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } - },{ + }, + { description: 'should reject self signed cert (POST)', expected: 'rejected: {"status":-2, ...', func: function(resolve, reject) { cordova.plugin.http.post('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, @@ -67,7 +69,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } - },{ + }, + { description: 'should reject self signed cert (PATCH)', expected: 'rejected: {"status":-2, ...', func: function(resolve, reject) { cordova.plugin.http.patch('https://self-signed.badssl.com/', { test: 'testString' }, {}, resolve, reject); }, @@ -75,7 +78,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } - },{ + }, + { description: 'should reject self signed cert (DELETE)', expected: 'rejected: {"status":-2, ...', func: function(resolve, reject) { cordova.plugin.http.delete('https://self-signed.badssl.com/', {}, {}, resolve, reject); }, @@ -83,7 +87,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('self-signed.badssl.com') }); } - },{ + }, + { description: 'should accept bad cert (GET)', expected: 'resolved: {"status":200, ...', before: helpers.setNoCheckCertMode, @@ -92,7 +97,8 @@ const tests = [ result.type.should.be.equal('resolved'); result.data.should.include({ status: 200 }); } - },{ + }, + { description: 'should accept bad cert (PUT)', expected: 'rejected: {"status":405, ... // will be rejected because PUT is not allowed', before: helpers.setNoCheckCertMode, @@ -101,7 +107,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.include({ status: 405 }); } - },{ + }, + { description: 'should accept bad cert (POST)', expected: 'rejected: {"status":405, ... // will be rejected because POST is not allowed', before: helpers.setNoCheckCertMode, @@ -110,7 +117,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.include({ status: 405 }); } - },{ + }, + { description: 'should accept bad cert (PATCH)', expected: 'rejected: {"status":405, ... // will be rejected because PATCH is not allowed', before: helpers.setNoCheckCertMode, @@ -119,7 +127,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.include({ status: 405 }); } - },{ + }, + { description: 'should accept bad cert (DELETE)', expected: 'rejected: {"status":405, ... // will be rejected because DELETE is not allowed', before: helpers.setNoCheckCertMode, @@ -128,7 +137,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.include({ status: 405 }); } - },{ + }, + { description: 'should fetch data from http://httpbin.org/ (GET)', expected: 'resolved: {"status":200, ...', before: helpers.setNoCheckCertMode, @@ -137,7 +147,8 @@ const tests = [ result.type.should.be.equal('resolved'); result.data.should.include({ status: 200 }); } - },{ + }, + { description: 'should send JSON object correctly (POST)', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setJsonSerializer, @@ -146,7 +157,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql({ test: 'testString' }); } - },{ + }, + { description: 'should send JSON object correctly (PUT)', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setJsonSerializer, @@ -155,7 +167,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql({ test: 'testString' }); } - },{ + }, + { description: 'should send JSON object correctly (PATCH)', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setJsonSerializer, @@ -164,7 +177,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql({ test: 'testString' }); } - },{ + }, + { description: 'should send JSON array correctly (POST) #26', expected: 'resolved: {"status": 200, "data": "[ 1, 2, 3 ]\" ...', before: helpers.setJsonSerializer, @@ -173,7 +187,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql([ 1, 2, 3 ]); } - },{ + }, + { description: 'should send JSON array correctly (PUT) #26', expected: 'resolved: {"status": 200, "data": "[ 1, 2, 3 ]\" ...', before: helpers.setJsonSerializer, @@ -182,7 +197,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql([ 1, 2, 3 ]); } - },{ + }, + { description: 'should send JSON array correctly (PATCH) #26', expected: 'resolved: {"status": 200, "data": "[ 1, 2, 3 ]\" ...', before: helpers.setJsonSerializer, @@ -192,7 +208,8 @@ const tests = [ result.data.data.should.be.a('string'); JSON.parse(result.data.data).json.should.eql([ 1, 2, 3 ]); } - },{ + }, + { description: 'should send url encoded data correctly (POST) #41', expected: 'resolved: {"status": 200, "data": "{\\"form\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setUrlEncodedSerializer, @@ -201,7 +218,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).form.should.eql({ test: 'testString' }); } - },{ + }, + { description: 'should send url encoded data correctly (PUT)', expected: 'resolved: {"status": 200, "data": "{\\"form\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setUrlEncodedSerializer, @@ -210,7 +228,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).form.should.eql({ test: 'testString' }); } - },{ + }, + { description: 'should send url encoded data correctly (PATCH)', expected: 'resolved: {"status": 200, "data": "{\\"form\\":\\"test\\": \\"testString\\"}\" ...', before: helpers.setUrlEncodedSerializer, @@ -219,7 +238,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).form.should.eql({ test: 'testString' }); } - },{ + }, + { description: 'should resolve correct URL after redirect (GET) #33', expected: 'resolved: {"status": 200, url: "http://httpbin.org/anything", ...', func: function(resolve, reject) { cordova.plugin.http.get('http://httpbin.org/redirect-to?url=http://httpbin.org/anything', {}, {}, resolve, reject); }, @@ -227,7 +247,8 @@ const tests = [ result.type.should.be.equal('resolved'); result.data.url.should.be.equal('http://httpbin.org/anything'); } - },{ + }, + { description: 'should download a file from given URL to given path in local filesystem', expected: 'resolved: {"content": "\\n\\n" ...', func: function(resolve, reject) { @@ -251,7 +272,8 @@ const tests = [ result.data.name.should.be.equal('test.xml'); result.data.content.should.be.equal("\n\n\n\n\n\n \n \n Wake up to WonderWidgets!\n \n\n \n \n Overview\n Why WonderWidgets are great\n \n Who buys WonderWidgets\n \n\n"); } - },{ + }, + { description: 'should upload a file from given path in local filesystem to given URL #27', expected: 'resolved: {"status": 200, "data": "files": {"test-file.txt": "I am a dummy file. I am used ...', func: function(resolve, reject) { @@ -276,7 +298,8 @@ const tests = [ .files[fileName] .should.be.equal(fileContent); } - },{ + }, + { 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\\"}\" ...', func: function(resolve, reject) { @@ -291,7 +314,8 @@ const tests = [ .url .should.include('httpbin.org/get?myArray[]=val1&myArray[]=val2&myArray[]=val3&myString=testString'); } - },{ + }, + { description: 'should throw on non-string values in local header object #54', expected: 'throwed: {"message": "advanced-http: header values must be strings"}', func: function(resolve, reject) { @@ -301,7 +325,8 @@ const tests = [ result.type.should.be.equal('throwed'); result.message.should.be.equal('advanced-http: header values must be strings'); } - },{ + }, + { description: 'should throw an error while setting non-string value as global header #54', expected: 'throwed: "advanced-http: header values must be strings"', func: function(resolve, reject) { @@ -311,7 +336,8 @@ const tests = [ result.type.should.be.equal('throwed'); result.message.should.be.equal('advanced-http: header values must be strings'); } - },{ + }, + { description: 'should accept content-type "application/xml" #58', expected: 'resolved: {"status": 200, ...', func: function(resolve, reject) { @@ -321,7 +347,8 @@ const tests = [ result.type.should.be.equal('resolved'); result.data.status.should.be.equal(200); } - },{ + }, + { description: 'should send programmatically set cookies correctly (GET)', expected: 'resolved: {"status": 200, ...', func: function(resolve, reject) { @@ -339,7 +366,8 @@ const tests = [ .Cookie .should.be.equal('myCookie=myValue; mySecondCookie=mySecondValue'); } - },{ + }, + { description: 'should not send any cookies after running "clearCookies" (GET) #59', expected: 'resolved: {"status": 200, "data": "{\"headers\": {\"Cookie\": \"\"...', func: function(resolve, reject) { @@ -358,7 +386,8 @@ const tests = [ .Cookie .should.be.equal(''); } - },{ + }, + { description: 'should send programmatically set cookies correctly (DOWNLOAD) #57', expected: 'resolved: {"content":{"cookies":{"myCookie":"myValue ...', func: function(resolve, reject) { @@ -390,7 +419,8 @@ const tests = [ cookies.myCookie.should.be.equal('myValue'); cookies.mySecondCookie.should.be.equal('mySecondValue'); } - },{ + }, + { description: 'should send UTF-8 encoded raw string correctly (POST) #34', expected: 'resolved: {"status": 200, "data": "{\\"data\\": \\"this is a test string\\"...', before: helpers.setUtf8StringSerializer, @@ -401,7 +431,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).data.should.be.equal('this is a test string'); } - },{ + }, + { description: 'should encode spaces in query string (params object) correctly (GET) #71', expected: 'resolved: {"status": 200, "data": "{\\"args\\": \\"query param\\": \\"and value with spaces\\"...', func: function(resolve, reject) { @@ -411,7 +442,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).args['query param'].should.be.equal('and value with spaces'); } - },{ + }, + { description: 'should decode latin1 (iso-8859-1) encoded body correctly (GET) #72', expected: 'resolved: {"status": 200, "data": " ...', func: function(resolve, reject) { @@ -421,7 +453,8 @@ const tests = [ result.type.should.be.equal('resolved'); result.data.data.should.include('[¡] 161 10/01 241 A1 INVERTED EXCLAMATION MARK\n[¢] 162 10/02 242 A2 CENT SIGN'); } - },{ + }, + { description: 'should return empty body string correctly (GET)', expected: 'resolved: {"status": 200, "data": "" ...', func: function(resolve, reject) { @@ -431,7 +464,8 @@ const tests = [ result.type.should.be.equal('resolved'); result.data.data.should.be.equal(''); } - },{ + }, + { description: 'should pin SSL cert correctly (GET)', expected: 'resolved: {"status": 200 ...', before: helpers.setPinnedCertMode, @@ -442,7 +476,8 @@ const tests = [ result.type.should.be.equal('resolved'); result.data.status.should.be.equal(200); } - },{ + }, + { description: 'should reject when pinned cert does not match received server cert (GET)', expected: 'rejected: {"status": -2 ...', before: helpers.setPinnedCertMode, @@ -453,7 +488,8 @@ const tests = [ result.type.should.be.equal('rejected'); result.data.should.be.eql({ status: -2, error: targetInfo.isAndroid ? messageFactory.sslTrustAnchor() : messageFactory.invalidCertificate('sha512.badssl.com') }); } - },{ + }, + { description: 'should send deeply structured JSON object correctly (POST) #65', expected: 'resolved: {"status": 200, "data": "{\\"data\\": \\"{\\\\"outerObj\\\\":{\\\\"innerStr\\\\":\\\\"testString\\\\",\\\\"innerArr\\\\":[1,2,3]}}\\" ...', before: helpers.setJsonSerializer, @@ -462,7 +498,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).json.should.eql({ outerObj: { innerStr: 'testString', innerArr: [1, 2, 3] }}); } - },{ + }, + { description: 'should override header "content-type" correctly (POST) #78', expected: 'resolved: {"status": 200, "headers": "{\\"Content-Type\\": \\"text/plain\\" ...', before: helpers.setJsonSerializer, @@ -471,7 +508,8 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).headers['Content-Type'].should.be.equal('text/plain'); } - },{ + }, + { description: 'should handle error during file download correctly (DOWNLOAD) #83', expected: 'rejected: {"status": 403, "error": "There was an error downloading the file" ...', func: function(resolve, reject) { cordova.plugin.http.downloadFile('http://httpbin.org/status/403', {}, {}, cordova.file.tempDirectory + 'testfile.txt', resolve, reject); }, @@ -480,7 +518,8 @@ const tests = [ result.data.status.should.be.equal(403); result.data.error.should.be.equal('There was an error downloading the file'); } - },{ + }, + { description: 'should handle gzip encoded response correctly', expected: 'resolved: {"status": 200, "headers": "{\\"Content-Encoding\\": \\"gzip\\" ...', func: function(resolve, reject) { cordova.plugin.http.get('http://httpbin.org/gzip', {}, {}, resolve, reject); }, @@ -489,7 +528,8 @@ const tests = [ result.data.status.should.be.equal(200); JSON.parse(result.data.data).gzipped.should.be.equal(true); } - },{ + }, + { description: 'should send empty string correctly', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"\\" ...', before: helpers.setUtf8StringSerializer, @@ -498,16 +538,34 @@ const tests = [ result.type.should.be.equal('resolved'); JSON.parse(result.data.data).data.should.be.equal(''); } - },{ + }, + { description: 'shouldn\'t escape forward slashes #184', expected: 'resolved: {"status": 200, "data": "{\\"json\\":\\"/\\" ...', before: helpers.setJsonSerializer, func: function(resolve, reject) { cordova.plugin.http.post('http://httpbin.org/anything', { testString: '/' }, {}, resolve, reject); }, validationFunc: function(driver, result) { result.type.should.be.equal('resolved'); - console.log(result.data.data); JSON.parse(result.data.data).json.testString.should.be.equal('/'); } + }, + { + description: 'should not double encode spaces in url path #195', + expected: '', + func: function(resolve, reject) { cordova.plugin.http.get('https://httpbin.org/anything/containing%20spaces%20in%20url', {}, {}, resolve, reject); }, + validationFunc: function(driver, result) { + result.type.should.be.equal('resolved'); + JSON.parse(result.data.data).url.should.be.equal('https://httpbin.org/anything/containing spaces in url'); + } + }, + { + description: 'should encode spaces in url query correctly', + expected: '', + func: function(resolve, reject) { cordova.plugin.http.get('https://httpbin.org/anything', { 'query key': 'very long query value with spaces' }, {}, resolve, reject); }, + validationFunc: function(driver, result) { + result.type.should.be.equal('resolved'); + JSON.parse(result.data.data).url.should.be.equal('https://httpbin.org/anything?query key=very long query value with spaces'); + } } ]; diff --git a/test/js-mocha-specs.js b/test/js-mocha-specs.js index 11ee709..210e7eb 100644 --- a/test/js-mocha-specs.js +++ b/test/js-mocha-specs.js @@ -1,21 +1,27 @@ const chai = require('chai'); const mock = require('mock-require'); -const path = require('path'); - const should = chai.should(); -const HELPERS_ID = path.resolve(__dirname, '..', 'www', 'helpers'); -const PLUGIN_ID = path.resolve(__dirname, '..', 'www', 'advanced-http'); - -describe('Advanced HTTP www interface', function() { +describe('Advanced HTTP public interface', function () { let http = {}; - let helpers = {}; const noop = () => { /* intentionally doing nothing */ }; - const loadHttp = () => { - mock(`${PLUGIN_ID}.helpers`, mock.reRequire('../www/helpers')); - http = mock.reRequire('../www/advanced-http'); + const getDependenciesBlueprint = () => { + const messages = require('../www/messages'); + const globalConfigs = require('../www/global-configs'); + const ToughCookie = require('../www/umd-tough-cookie'); + const lodash = require('../www/lodash'); + const WebStorageCookieStore = require('../www/local-storage-store')(ToughCookie, lodash); + const cookieHandler = require('../www/cookie-handler')(null, ToughCookie, WebStorageCookieStore); + const helpers = require('../www/helpers')(cookieHandler, messages); + const urlUtil = require('../www/url-util')(helpers); + + return { exec: noop, cookieHandler, urlUtil: urlUtil, helpers, globalConfigs }; + }; + + const loadHttp = (deps) => { + http = require('../www/public-interface')(deps.exec, deps.cookieHandler, deps.urlUtil, deps.helpers, deps.globalConfigs); }; this.timeout(900000); @@ -23,13 +29,7 @@ describe('Advanced HTTP www interface', function() { beforeEach(() => { // mocked btoa function (base 64 encoding strings) global.btoa = decoded => new Buffer(decoded).toString('base64'); - - mock('cordova/exec', noop); - mock(`${PLUGIN_ID}.cookie-handler`, {}); - mock(`${HELPERS_ID}.cookie-handler`, {}); - mock(`${HELPERS_ID}.messages`, require('../www/messages')); - - loadHttp(); + loadHttp(getDependenciesBlueprint()); }); it('sets global headers correctly with two args (old interface)', () => { @@ -48,76 +48,76 @@ describe('Advanced HTTP www interface', function() { }); it('resolves global headers correctly #24', () => { - mock(`${HELPERS_ID}.cookie-handler`, { - getCookieString: () => 'fakeCookieString' - }); + const deps = getDependenciesBlueprint(); - mock('cordova/exec', (onSuccess, onFail, namespace, method, params) => { - const headers = params[2]; + deps.cookieHandler.getCookieString = () => 'fakeCookieString'; + + deps.exec = (onSuccess, onFail, namespace, method, params) => { + const headers = params[1]; headers.should.eql({ Cookie: 'fakeCookieString', myKey: 'myValue' }); - }); + }; - loadHttp(); + loadHttp(deps); http.setHeader('*', 'myKey', 'myValue'); http.get('url', {}, {}, noop, noop); }); it('resolves host headers correctly (set without port number) #37', () => { - mock(`${HELPERS_ID}.cookie-handler`, { - getCookieString: () => 'fakeCookieString' - }); + const deps = getDependenciesBlueprint(); - mock('cordova/exec', (onSuccess, onFail, namespace, method, params) => { - const headers = params[2]; + deps.cookieHandler.getCookieString = () => 'fakeCookieString'; + + deps.exec = (onSuccess, onFail, namespace, method, params) => { + const headers = params[1]; headers.should.eql({ Cookie: 'fakeCookieString', myKey: 'myValue' }); - }); + }; - loadHttp(); + loadHttp(deps); http.setHeader('www.google.de', 'myKey', 'myValue'); http.get('https://www.google.de/?gws_rd=ssl', {}, {}, noop, noop); }); it('resolves host headers correctly (set with port number) #37', () => { - mock(`${HELPERS_ID}.cookie-handler`, { - getCookieString: () => 'fakeCookieString' - }); + const deps = getDependenciesBlueprint(); - mock('cordova/exec', (onSuccess, onFail, namespace, method, params) => { - const headers = params[2]; + deps.cookieHandler.getCookieString = () => 'fakeCookieString'; + + deps.exec = (onSuccess, onFail, namespace, method, params) => { + const headers = params[1]; headers.should.eql({ Cookie: 'fakeCookieString', myKey: 'myValue' }); - }); + }; - loadHttp(); + loadHttp(deps); http.setHeader('www.google.de:8080', 'myKey', 'myValue'); http.get('https://www.google.de:8080/?gws_rd=ssl', {}, {}, noop, noop); }); it('resolves request headers correctly', () => { - mock(`${HELPERS_ID}.cookie-handler`, { - getCookieString: () => 'fakeCookieString' - }); + const deps = getDependenciesBlueprint(); - mock('cordova/exec', (onSuccess, onFail, namespace, method, params) => { - const headers = params[2]; + deps.cookieHandler.getCookieString = () => 'fakeCookieString'; + + deps.exec = (onSuccess, onFail, namespace, method, params) => { + const headers = params[1]; headers.should.eql({ Cookie: 'fakeCookieString', myKey: 'myValue' }); - }); + }; - loadHttp(); + loadHttp(deps); http.get('https://www.google.de/?gws_rd=ssl', {}, { myKey: 'myValue' }, noop, noop); }); @@ -128,6 +128,95 @@ describe('Advanced HTTP www interface', function() { }); it('throws an Error when you try to add a cookie by using "setHeader" #46', () => { - (function() { http.setHeader('*', 'cookie', 'value') }).should.throw(); + (function () { http.setHeader('*', 'cookie', 'value') }).should.throw(); + }); +}); + +describe('URL handler', function () { + const helpers = require('../www/helpers')(null, null); + const handler = require('../www/url-util')(helpers); + + it('parses URL with protocol, hostname and path correctly', () => { + handler.parseUrl('http://ilkimen.net/test').should.include({ + protocol: 'http:', + host: 'ilkimen.net', + hostname: 'ilkimen.net', + pathname: '/test', + port: '', + search: '', + hash: '' + }); + }); + + it('parses URL with protocol, hostname, port and path correctly', () => { + handler.parseUrl('http://ilkimen.net:8080/test').should.include({ + protocol: 'http:', + host: 'ilkimen.net:8080', + hostname: 'ilkimen.net', + pathname: '/test', + port: '8080', + search: '', + hash: '' + }); + }); + + it('parses URL with protocol, hostname, port, path and query string correctly', () => { + handler.parseUrl('http://ilkimen.net:8080/test?param=value').should.include({ + protocol: 'http:', + host: 'ilkimen.net:8080', + hostname: 'ilkimen.net', + pathname: '/test', + port: '8080', + search: '?param=value', + hash: '' + }); + }); + + it('parses URL with protocol, hostname, port, path, query string and hash param correctly', () => { + handler.parseUrl('http://ilkimen.net:8080/test?param=value#myHash').should.include({ + protocol: 'http:', + host: 'ilkimen.net:8080', + hostname: 'ilkimen.net', + pathname: '/test', + port: '8080', + search: '?param=value', + hash: '#myHash' + }); + }); + + it('serializes query params correctly without URL encoding', () => { + handler.serializeQueryParams({ + param1: 'value with spaces', + param2: 'value with special character äöü%' + }, false).should.equal('param1=value with spaces¶m2=value with special character äöü%'); + }); + + it('serializes array of query params correctly without URL encoding', () => { + handler.serializeQueryParams({ + myArray: ['val1', 'val2', 'val3'], + myString: 'testString' + }, false).should.equal('myArray[]=val1&myArray[]=val2&myArray[]=val3&myString=testString'); + }); + + it('serializes query params correctly with URL encoding enabled', () => { + handler.serializeQueryParams({ + param1: 'value with spaces', + param2: 'value with special character äöü%&' + }, true).should.equal('param1=value%20with%20spaces¶m2=value%20with%20special%20character%20%C3%A4%C3%B6%C3%BC%25%26'); + }); + + it('appends query params string correctly to given URL without query parameters', () => { + handler.appendQueryParamsString('http://ilkimen.net/', 'param1=value1') + .should.equal('http://ilkimen.net/?param1=value1'); + }); + + it('appends query params string correctly to given URL with existing query parameters', () => { + handler.appendQueryParamsString('http://ilkimen.net/?myParam=myValue', 'param1=value1') + .should.equal('http://ilkimen.net/?myParam=myValue¶m1=value1'); + }); + + it('appends query params string correctly to given URL with existing query parameters and hash value', () => { + handler.appendQueryParamsString('http://ilkimen.net/?myParam=myValue#myHash', 'param1=value1') + .should.equal('http://ilkimen.net/?myParam=myValue¶m1=value1#myHash'); }); }); diff --git a/www/advanced-http.js b/www/advanced-http.js index 74173c8..a60aa3c 100644 --- a/www/advanced-http.js +++ b/www/advanced-http.js @@ -5,121 +5,14 @@ var pluginId = module.id.slice(0, module.id.lastIndexOf('.')); var exec = require('cordova/exec'); -var cookieHandler = require(pluginId + '.cookie-handler'); -var helpers = require(pluginId + '.helpers'); - -var globalConfigs = { - headers: {}, - serializer: 'urlencoded', - timeout: 60.0, -}; - -var publicInterface = { - getBasicAuthHeader: function (username, password) { - return {'Authorization': 'Basic ' + helpers.b64EncodeUnicode(username + ':' + password)}; - }, - useBasicAuth: function (username, password) { - this.setHeader('*', 'Authorization', 'Basic ' + helpers.b64EncodeUnicode(username + ':' + password)); - }, - getHeaders: function (host) { - return globalConfigs.headers[host || '*'] || null; - }, - setHeader: function () { - // this one is for being backward compatible - var host = '*'; - var header = arguments[0]; - var value = arguments[1]; - - if (arguments.length === 3) { - host = arguments[0]; - header = arguments[1]; - value = arguments[2]; - } - - helpers.checkForBlacklistedHeaderKey(header); - helpers.checkForInvalidHeaderValue(value); - - globalConfigs.headers[host] = globalConfigs.headers[host] || {}; - globalConfigs.headers[host][header] = value; - }, - getDataSerializer: function () { - return globalConfigs.serializer; - }, - setDataSerializer: function (serializer) { - globalConfigs.serializer = helpers.checkSerializer(serializer); - }, - setCookie: function (url, cookie, options) { - cookieHandler.setCookie(url, cookie, options); - }, - clearCookies: function () { - cookieHandler.clearCookies(); - }, - removeCookies: function (url, callback) { - cookieHandler.removeCookies(url, callback); - }, - getCookieString: function (url) { - return cookieHandler.getCookieString(url); - }, - getRequestTimeout: function () { - return globalConfigs.timeout; - }, - setRequestTimeout: function (timeout) { - globalConfigs.timeout = timeout; - }, - setSSLCertMode: function (mode, success, failure) { - return exec(success, failure, 'CordovaHttpPlugin', 'setSSLCertMode', [ helpers.checkSSLCertMode(mode) ]); - }, - disableRedirect: function (disable, success, failure) { - return exec(success, failure, 'CordovaHttpPlugin', 'disableRedirect', [ !!disable ]); - }, - sendRequest: function (url, options, success, failure) { - helpers.handleMissingCallbacks(success, failure); - - options = helpers.handleMissingOptions(options, globalConfigs); - - var headers = helpers.getMergedHeaders(url, options.headers, globalConfigs.headers); - var onSuccess = helpers.injectCookieHandler(url, success); - var onFail = helpers.injectCookieHandler(url, failure); - - switch(options.method) { - case 'post': - case 'put': - case 'patch': - var data = helpers.getProcessedData(options.data, options.serializer); - return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [ url, data, options.serializer, headers, options.timeout ]); - case 'upload': - return exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFile', [ url, options.params, headers, options.filePath, options.name, options.timeout ]); - case 'download': - var onDownloadSuccess = helpers.injectCookieHandler(url, helpers.injectFileEntryHandler(success)); - return exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [ url, options.params, headers, options.filePath, options.timeout ]); - default: - return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [ url, options.params, headers, options.timeout ]); - } - }, - post: function (url, data, headers, success, failure) { - return publicInterface.sendRequest(url, { method: 'post', data: data, headers: headers }, success, failure); - }, - get: function (url, params, headers, success, failure) { - return publicInterface.sendRequest(url, { method: 'get', params: params, headers: headers }, success, failure); - }, - put: function (url, data, headers, success, failure) { - return publicInterface.sendRequest(url, { method: 'put', data: data, headers: headers }, success, failure); - }, - patch: function (url, data, headers, success, failure) { - return publicInterface.sendRequest(url, { method: 'patch', data: data, headers: headers }, success, failure); - }, - delete: function (url, params, headers, success, failure) { - return publicInterface.sendRequest(url, { method: 'delete', params: params, headers: headers }, success, failure); - }, - head: function (url, params, headers, success, failure) { - return publicInterface.sendRequest(url, { method: 'head', params: params, headers: headers }, success, failure); - }, - uploadFile: function (url, params, headers, filePath, name, success, failure) { - return publicInterface.sendRequest(url, { method: 'upload', params: params, headers: headers, filePath: filePath, name: name }, success, failure); - }, - downloadFile: function (url, params, headers, filePath, success, failure) { - return publicInterface.sendRequest(url, { method: 'download', params: params, headers: headers, filePath: filePath }, success, failure); - } -}; +var messages = require(pluginId + '.messages'); +var globalConfigs = require(pluginId + '.global-configs'); +var ToughCookie = require(pluginId + '.tough-cookie'); +var lodash = require(pluginId + '.lodash'); +var WebStorageCookieStore = require(pluginId + '.local-storage-store')(ToughCookie, lodash); +var cookieHandler = require(pluginId + '.cookie-handler')(window.localStorage, ToughCookie, WebStorageCookieStore); +var helpers = require(pluginId + '.helpers')(cookieHandler, messages); +var urlUtil = require(pluginId + '.url-util')(helpers); +var publicInterface = require(pluginId + '.public-interface')(exec, cookieHandler, urlUtil, helpers, globalConfigs); module.exports = publicInterface; diff --git a/www/cookie-handler.js b/www/cookie-handler.js index 0f97780..f8dd24c 100644 --- a/www/cookie-handler.js +++ b/www/cookie-handler.js @@ -1,73 +1,70 @@ -var pluginId = module.id.slice(0, module.id.lastIndexOf('.')); -var ToughCookie = require(pluginId + '.tough-cookie'); -var WebStorageCookieStore = require(pluginId + '.local-storage-store'); +module.exports = function init(storage, ToughCookie, WebStorageCookieStore) { + var storeKey = '__advancedHttpCookieStore__'; -var storage = window.localStorage; -var storeKey = '__advancedHttpCookieStore__'; + var store = new WebStorageCookieStore(storage, storeKey); + var cookieJar = new ToughCookie.CookieJar(store); -var store = new WebStorageCookieStore(storage, storeKey); -var cookieJar = new ToughCookie.CookieJar(store); - -module.exports = { + return { setCookieFromString: setCookieFromString, setCookie: setCookie, getCookieString: getCookieString, clearCookies: clearCookies, removeCookies: removeCookies -} + }; -function splitCookieString(cookieStr) { + function splitCookieString(cookieStr) { var cookieParts = cookieStr.split(','); var splitCookies = []; var processedCookie = null; for (var i = 0; i < cookieParts.length; ++i) { - if (cookieParts[i].substr(-11, 8).toLowerCase() === 'expires=') { - processedCookie = cookieParts[i] + ',' + cookieParts[i + 1]; - i++; - } else { - processedCookie = cookieParts[i]; - } + if (cookieParts[i].substr(-11, 8).toLowerCase() === 'expires=') { + processedCookie = cookieParts[i] + ',' + cookieParts[i + 1]; + i++; + } else { + processedCookie = cookieParts[i]; + } - processedCookie = processedCookie.trim(); - splitCookies.push(processedCookie); + processedCookie = processedCookie.trim(); + splitCookies.push(processedCookie); } return splitCookies; -} + } -function setCookieFromString(url, cookieStr) { + function setCookieFromString(url, cookieStr) { if (!cookieStr) return; var cookies = splitCookieString(cookieStr); for (var i = 0; i < cookies.length; ++i) { - cookieJar.setCookieSync(cookies[i], url, { ignoreError: true }); + cookieJar.setCookieSync(cookies[i], url, { ignoreError: true }); } -} + } -function setCookie(url, cookie, options) { - options = options || {}; - options.ignoreError = false; - cookieJar.setCookieSync(cookie, url, options); -} + function setCookie(url, cookie, options) { + options = options || {}; + options.ignoreError = false; + cookieJar.setCookieSync(cookie, url, options); + } -function getCookieString(url) { + function getCookieString(url) { return cookieJar.getCookieStringSync(url); -} + } -function clearCookies() { + function clearCookies() { window.localStorage.removeItem(storeKey); -} + } -function removeCookies(url, cb) { - cookieJar.getCookies(url, function(error, cookies) { - if (!cookies || cookies.length === 0) { + function removeCookies(url, cb) { + cookieJar.getCookies(url, function (error, cookies) { + if (!cookies || cookies.length === 0) { return cb(null, []); - } + } - var domain = cookies[0].domain; + var domain = cookies[0].domain; - cookieJar.store.removeCookies(domain, null, cb); + cookieJar.store.removeCookies(domain, null, cb); }); -} + } +}; diff --git a/www/global-configs.js b/www/global-configs.js new file mode 100644 index 0000000..761a1ff --- /dev/null +++ b/www/global-configs.js @@ -0,0 +1,7 @@ +var globalConfigs = { + headers: {}, + serializer: 'urlencoded', + timeout: 60.0, +}; + +module.exports = globalConfigs; diff --git a/www/helpers.js b/www/helpers.js index 929563b..06e82ee 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -1,253 +1,251 @@ -var pluginId = module.id.slice(0, module.id.lastIndexOf('.')); -var cookieHandler = require(pluginId + '.cookie-handler'); -var messages = require(pluginId + '.messages'); - -var validSerializers = [ 'urlencoded', 'json', 'utf8' ]; -var validCertModes = [ 'default', 'nocheck', 'pinned', 'legacy' ]; -var validHttpMethods = [ 'get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download' ]; - -module.exports = { - b64EncodeUnicode: b64EncodeUnicode, - getTypeOf: getTypeOf, - checkSerializer: checkSerializer, - checkSSLCertMode: checkSSLCertMode, - checkForBlacklistedHeaderKey: checkForBlacklistedHeaderKey, - checkForInvalidHeaderValue: checkForInvalidHeaderValue, - injectCookieHandler: injectCookieHandler, - injectFileEntryHandler: injectFileEntryHandler, - getMergedHeaders: getMergedHeaders, - getProcessedData: getProcessedData, - handleMissingCallbacks: handleMissingCallbacks, - handleMissingOptions: handleMissingOptions -}; - -// Thanks Mozilla: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22 -function b64EncodeUnicode(str) { - return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { - return String.fromCharCode('0x' + p1); - })); -} - -function mergeHeaders(globalHeaders, localHeaders) { - var globalKeys = Object.keys(globalHeaders); - var key; - - for (var i = 0; i < globalKeys.length; i++) { - key = globalKeys[i]; - - if (!localHeaders.hasOwnProperty(key)) { - localHeaders[key] = globalHeaders[key]; - } - } - - return localHeaders; -} - -function checkForValidStringValue(list, value, onInvalidValueMessage) { - if (getTypeOf(value) !== 'String') { - throw new Error(onInvalidValueMessage + ' ' + list.join(', ')); - } - - value = value.trim().toLowerCase(); - - if (list.indexOf(value) === -1) { - throw new Error(onInvalidValueMessage + ' ' + list.join(', ')); - } - - return value; -} - -function checkKeyValuePairObject(obj, allowedChildren, onInvalidValueMessage) { - if (getTypeOf(obj) !== 'Object') { - throw new Error(onInvalidValueMessage); - } - - var keys = Object.keys(obj); - - for (var i = 0; i < keys.length; i++) { - if (allowedChildren.indexOf(getTypeOf(obj[keys[i]])) === -1) { - throw new Error(onInvalidValueMessage); - } - } - - return obj; -} - -function checkHttpMethod(method) { - return checkForValidStringValue(validHttpMethods, method, messages.INVALID_HTTP_METHOD); -} - -function checkSerializer(serializer) { - return checkForValidStringValue(validSerializers, serializer, messages.INVALID_DATA_SERIALIZER); -} - -function checkSSLCertMode(mode) { - return checkForValidStringValue(validCertModes, mode, messages.INVALID_SSL_CERT_MODE); -} - -function checkForBlacklistedHeaderKey(key) { - if (key.toLowerCase() === 'cookie') { - throw new Error(messages.ADDING_COOKIES_NOT_SUPPORTED); - } - - return key; -} - -function checkForInvalidHeaderValue(value) { - if (getTypeOf(value) !== 'String') { - throw new Error(messages.INVALID_HEADERS_VALUE); - } - - return value; -} - -function checkTimeoutValue(timeout) { - if (getTypeOf(timeout) !== 'Number' || timeout < 0) { - throw new Error(messages.INVALID_TIMEOUT_VALUE); - } - - return timeout; -} - -function checkHeadersObject(headers) { - return checkKeyValuePairObject(headers, [ 'String' ], messages.INVALID_HEADERS_VALUE); -} - -function checkParamsObject(params) { - return checkKeyValuePairObject(params, [ 'String', 'Array' ], messages.INVALID_PARAMS_VALUE); -} - -function resolveCookieString(headers) { - var keys = Object.keys(headers || {}); - - for (var i = 0; i < keys.length; ++i) { - if (keys[i].match(/^set-cookie$/i)) { - return headers[keys[i]]; - } - } - - return null; -} - -function createFileEntry(rawEntry) { - var entry = new (require('cordova-plugin-file.FileEntry'))(); - - entry.isDirectory = rawEntry.isDirectory; - entry.isFile = rawEntry.isFile; - entry.name = rawEntry.name; - entry.fullPath = rawEntry.fullPath; - entry.filesystem = new FileSystem(rawEntry.filesystemName || (rawEntry.filesystem == window.PERSISTENT ? 'persistent' : 'temporary')); - entry.nativeURL = rawEntry.nativeURL; - - return entry; -} - -function injectCookieHandler(url, cb) { - return function(response) { - cookieHandler.setCookieFromString(url, resolveCookieString(response.headers)); - cb(response); - } -} - -function injectFileEntryHandler(cb) { - return function(response) { - cb(createFileEntry(response.file)); - } -} - -function getCookieHeader(url) { - return { Cookie: cookieHandler.getCookieString(url) }; -} - -function getMatchingHostHeaders(url, headersList) { - var matches = url.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i); - var domain = matches && matches[1]; - - return headersList[domain] || null; -} - -function getMergedHeaders(url, requestHeaders, predefinedHeaders) { - var globalHeaders = predefinedHeaders['*'] || {}; - var hostHeaders = getMatchingHostHeaders(url, predefinedHeaders) || {}; - var mergedHeaders = mergeHeaders(globalHeaders, hostHeaders); - - mergedHeaders = mergeHeaders(mergedHeaders, requestHeaders); - mergedHeaders = mergeHeaders(mergedHeaders, getCookieHeader(url)); - - return mergedHeaders; -} - -// typeof is not working reliably in JS -function getTypeOf(object) { - switch (Object.prototype.toString.call(object)) { - case '[object Array]': - return 'Array'; - case '[object Boolean]': - return 'Boolean'; - case '[object Function]': - return 'Function'; - case '[object Null]': - return 'Null'; - case '[object Number]': - return 'Number'; - case '[object Object]': - return 'Object'; - case '[object String]': - return 'String'; - case '[object Undefined]': - return 'Undefined'; - default: - return 'Unknown'; - } -} - -function getAllowedDataTypes(dataSerializer) { - switch (dataSerializer) { - case 'utf8': - return ['String']; - case 'urlencoded': - return ['Object']; - default: - return ['Array', 'Object']; - } -} - -function getProcessedData(data, dataSerializer) { - var currentDataType = getTypeOf(data); - var allowedDataTypes = getAllowedDataTypes(dataSerializer); - - if (allowedDataTypes.indexOf(currentDataType) === -1) { - throw new Error(messages.DATA_TYPE_MISMATCH + ' ' + allowedDataTypes.join(', ')); - } - - if (dataSerializer === 'utf8') { - data = { text: data }; - } - - return data; -} - -function handleMissingCallbacks(successFn, failFn) { - if (getTypeOf(successFn) !== 'Function') { - throw new Error(messages.MANDATORY_SUCCESS); - } - - if (getTypeOf(failFn) !== 'Function') { - throw new Error(messages.MANDATORY_FAIL); - } -} - -function handleMissingOptions(options, globals) { - options = options || {}; +module.exports = function init(cookieHandler, messages) { + var validSerializers = ['urlencoded', 'json', 'utf8']; + var validCertModes = ['default', 'nocheck', 'pinned', 'legacy']; + var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download']; return { - method: checkHttpMethod(options.method || validHttpMethods[0]), - serializer: checkSerializer(options.serializer || globals.serializer), - timeout: checkTimeoutValue(options.timeout || globals.timeout), - headers: checkHeadersObject(options.headers || {}), - params: checkParamsObject(options.params || {}), - data: getTypeOf(options.data) === 'Undefined' ? null : options.data, - filePath: options.filePath || '', - name: options.name || '' + b64EncodeUnicode: b64EncodeUnicode, + getTypeOf: getTypeOf, + checkSerializer: checkSerializer, + checkSSLCertMode: checkSSLCertMode, + checkForBlacklistedHeaderKey: checkForBlacklistedHeaderKey, + checkForInvalidHeaderValue: checkForInvalidHeaderValue, + injectCookieHandler: injectCookieHandler, + injectFileEntryHandler: injectFileEntryHandler, + getMergedHeaders: getMergedHeaders, + getProcessedData: getProcessedData, + handleMissingCallbacks: handleMissingCallbacks, + handleMissingOptions: handleMissingOptions }; -} + + // Thanks Mozilla: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22 + function b64EncodeUnicode(str) { + return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { + return String.fromCharCode('0x' + p1); + })); + } + + function mergeHeaders(globalHeaders, localHeaders) { + var globalKeys = Object.keys(globalHeaders); + var key; + + for (var i = 0; i < globalKeys.length; i++) { + key = globalKeys[i]; + + if (!localHeaders.hasOwnProperty(key)) { + localHeaders[key] = globalHeaders[key]; + } + } + + return localHeaders; + } + + function checkForValidStringValue(list, value, onInvalidValueMessage) { + if (getTypeOf(value) !== 'String') { + throw new Error(onInvalidValueMessage + ' ' + list.join(', ')); + } + + value = value.trim().toLowerCase(); + + if (list.indexOf(value) === -1) { + throw new Error(onInvalidValueMessage + ' ' + list.join(', ')); + } + + return value; + } + + function checkKeyValuePairObject(obj, allowedChildren, onInvalidValueMessage) { + if (getTypeOf(obj) !== 'Object') { + throw new Error(onInvalidValueMessage); + } + + var keys = Object.keys(obj); + + for (var i = 0; i < keys.length; i++) { + if (allowedChildren.indexOf(getTypeOf(obj[keys[i]])) === -1) { + throw new Error(onInvalidValueMessage); + } + } + + return obj; + } + + function checkHttpMethod(method) { + return checkForValidStringValue(validHttpMethods, method, messages.INVALID_HTTP_METHOD); + } + + function checkSerializer(serializer) { + return checkForValidStringValue(validSerializers, serializer, messages.INVALID_DATA_SERIALIZER); + } + + function checkSSLCertMode(mode) { + return checkForValidStringValue(validCertModes, mode, messages.INVALID_SSL_CERT_MODE); + } + + function checkForBlacklistedHeaderKey(key) { + if (key.toLowerCase() === 'cookie') { + throw new Error(messages.ADDING_COOKIES_NOT_SUPPORTED); + } + + return key; + } + + function checkForInvalidHeaderValue(value) { + if (getTypeOf(value) !== 'String') { + throw new Error(messages.INVALID_HEADERS_VALUE); + } + + return value; + } + + function checkTimeoutValue(timeout) { + if (getTypeOf(timeout) !== 'Number' || timeout < 0) { + throw new Error(messages.INVALID_TIMEOUT_VALUE); + } + + return timeout; + } + + function checkHeadersObject(headers) { + return checkKeyValuePairObject(headers, ['String'], messages.INVALID_HEADERS_VALUE); + } + + function checkParamsObject(params) { + return checkKeyValuePairObject(params, ['String', 'Array'], messages.INVALID_PARAMS_VALUE); + } + + function resolveCookieString(headers) { + var keys = Object.keys(headers || {}); + + for (var i = 0; i < keys.length; ++i) { + if (keys[i].match(/^set-cookie$/i)) { + return headers[keys[i]]; + } + } + + return null; + } + + function createFileEntry(rawEntry) { + var entry = new (require('cordova-plugin-file.FileEntry'))(); + + entry.isDirectory = rawEntry.isDirectory; + entry.isFile = rawEntry.isFile; + entry.name = rawEntry.name; + entry.fullPath = rawEntry.fullPath; + entry.filesystem = new FileSystem(rawEntry.filesystemName || (rawEntry.filesystem == window.PERSISTENT ? 'persistent' : 'temporary')); + entry.nativeURL = rawEntry.nativeURL; + + return entry; + } + + function injectCookieHandler(url, cb) { + return function (response) { + cookieHandler.setCookieFromString(url, resolveCookieString(response.headers)); + cb(response); + } + } + + function injectFileEntryHandler(cb) { + return function (response) { + cb(createFileEntry(response.file)); + } + } + + function getCookieHeader(url) { + return { Cookie: cookieHandler.getCookieString(url) }; + } + + function getMatchingHostHeaders(url, headersList) { + var matches = url.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i); + var domain = matches && matches[1]; + + return headersList[domain] || null; + } + + function getMergedHeaders(url, requestHeaders, predefinedHeaders) { + var globalHeaders = predefinedHeaders['*'] || {}; + var hostHeaders = getMatchingHostHeaders(url, predefinedHeaders) || {}; + var mergedHeaders = mergeHeaders(globalHeaders, hostHeaders); + + mergedHeaders = mergeHeaders(mergedHeaders, requestHeaders); + mergedHeaders = mergeHeaders(mergedHeaders, getCookieHeader(url)); + + return mergedHeaders; + } + + // typeof is not working reliably in JS + function getTypeOf(object) { + switch (Object.prototype.toString.call(object)) { + case '[object Array]': + return 'Array'; + case '[object Boolean]': + return 'Boolean'; + case '[object Function]': + return 'Function'; + case '[object Null]': + return 'Null'; + case '[object Number]': + return 'Number'; + case '[object Object]': + return 'Object'; + case '[object String]': + return 'String'; + case '[object Undefined]': + return 'Undefined'; + default: + return 'Unknown'; + } + } + + function getAllowedDataTypes(dataSerializer) { + switch (dataSerializer) { + case 'utf8': + return ['String']; + case 'urlencoded': + return ['Object']; + default: + return ['Array', 'Object']; + } + } + + function getProcessedData(data, dataSerializer) { + var currentDataType = getTypeOf(data); + var allowedDataTypes = getAllowedDataTypes(dataSerializer); + + if (allowedDataTypes.indexOf(currentDataType) === -1) { + throw new Error(messages.DATA_TYPE_MISMATCH + ' ' + allowedDataTypes.join(', ')); + } + + if (dataSerializer === 'utf8') { + data = { text: data }; + } + + return data; + } + + function handleMissingCallbacks(successFn, failFn) { + if (getTypeOf(successFn) !== 'Function') { + throw new Error(messages.MANDATORY_SUCCESS); + } + + if (getTypeOf(failFn) !== 'Function') { + throw new Error(messages.MANDATORY_FAIL); + } + } + + function handleMissingOptions(options, globals) { + options = options || {}; + + return { + method: checkHttpMethod(options.method || validHttpMethods[0]), + serializer: checkSerializer(options.serializer || globals.serializer), + timeout: checkTimeoutValue(options.timeout || globals.timeout), + headers: checkHeadersObject(options.headers || {}), + params: checkParamsObject(options.params || {}), + data: getTypeOf(options.data) === 'Undefined' ? null : options.data, + filePath: options.filePath || '', + name: options.name || '' + }; + } +}; diff --git a/www/local-storage-store.js b/www/local-storage-store.js index 4c5b7b0..2b90278 100644 --- a/www/local-storage-store.js +++ b/www/local-storage-store.js @@ -30,30 +30,27 @@ 'use strict'; -var pluginId = module.id.slice(0, module.id.lastIndexOf('.')); -var ToughCookie = require(pluginId + '.tough-cookie'); -var _ = require(pluginId + '.lodash'); - -function WebStorageCookieStore(storage, storeKey) { +module.exports = function init(ToughCookie, _) { + function WebStorageCookieStore(storage, storeKey) { ToughCookie.Store.call(this); - this._storage = storage || window.localStorage; + this._storage = storage; this._storeKey = storeKey || '__cookieStore__'; this.synchronous = true; -} + } -WebStorageCookieStore.prototype = Object.create(ToughCookie.Store); + WebStorageCookieStore.prototype = Object.create(ToughCookie.Store); -WebStorageCookieStore.prototype.findCookie = function(domain, path, key, callback) { + WebStorageCookieStore.prototype.findCookie = function (domain, path, key, callback) { var store = this._readStore(); var cookie = _.get(store, [domain, path, key], null); callback(null, ToughCookie.Cookie.fromJSON(cookie)); -}; + }; -WebStorageCookieStore.prototype.findCookies = function(domain, path, callback) { + WebStorageCookieStore.prototype.findCookies = function (domain, path, callback) { if (!domain) { - callback(null, []); - return; + callback(null, []); + return; } var that = this; @@ -61,123 +58,124 @@ WebStorageCookieStore.prototype.findCookies = function(domain, path, callback) { var store = this._readStore(); var domains = ToughCookie.permuteDomain(domain) || [domain]; - domains.forEach(function(domain) { - if (!store[domain]) { - return; - } + domains.forEach(function (domain) { + if (!store[domain]) { + return; + } - var matchingPaths = Object.keys(store[domain]); + var matchingPaths = Object.keys(store[domain]); - if (path != null) { - matchingPaths = matchingPaths.filter(function(cookiePath) { - return that._isOnPath(cookiePath, path); - }); - } - - matchingPaths.forEach(function(path) { - Array.prototype.push.apply(cookies, _.values(store[domain][path])); + if (path != null) { + matchingPaths = matchingPaths.filter(function (cookiePath) { + return that._isOnPath(cookiePath, path); }); + } + + matchingPaths.forEach(function (path) { + Array.prototype.push.apply(cookies, _.values(store[domain][path])); + }); }); - cookies = cookies.map(function(cookie) { - return ToughCookie.Cookie.fromJSON(cookie); + cookies = cookies.map(function (cookie) { + return ToughCookie.Cookie.fromJSON(cookie); }); callback(null, cookies); -}; + }; -/** - * Returns whether `cookiePath` is on the given `urlPath` - */ -WebStorageCookieStore.prototype._isOnPath = function(cookiePath, urlPath) { + /** + * Returns whether `cookiePath` is on the given `urlPath` + */ + WebStorageCookieStore.prototype._isOnPath = function (cookiePath, urlPath) { if (!cookiePath) { - return false; + return false; } if (cookiePath === urlPath) { - return true; + return true; } if (urlPath.indexOf(cookiePath) !== 0) { - return false; + return false; } if (cookiePath[cookiePath.length - 1] !== '/' && urlPath[cookiePath.length] !== '/') { - return false; + return false; } return true; -}; + }; -WebStorageCookieStore.prototype.putCookie = function(cookie, callback) { - var store = this._readStore(); + WebStorageCookieStore.prototype.putCookie = function (cookie, callback) { + var store = this._readStore(); - _.set(store, [cookie.domain, cookie.path, cookie.key], cookie); - this._writeStore(store); - callback(null); -}; + _.set(store, [cookie.domain, cookie.path, cookie.key], cookie); + this._writeStore(store); + callback(null); + }; -WebStorageCookieStore.prototype.updateCookie = function(oldCookie, newCookie, callback) { + WebStorageCookieStore.prototype.updateCookie = function (oldCookie, newCookie, callback) { this.putCookie(newCookie, callback); -}; + }; -WebStorageCookieStore.prototype.removeCookie = function(domain, path, key, callback) { + WebStorageCookieStore.prototype.removeCookie = function (domain, path, key, callback) { var store = this._readStore(); _.unset(store, [domain, path, key]); this._writeStore(store); callback(null); -}; + }; -WebStorageCookieStore.prototype.removeCookies = function(domain, path, callback) { + WebStorageCookieStore.prototype.removeCookies = function (domain, path, callback) { var store = this._readStore(); if (path == null) { - _.unset(store, [domain]); + _.unset(store, [domain]); } else { - _.unset(store, [domain, path]); + _.unset(store, [domain, path]); } this._writeStore(store); callback(null); -}; + }; -WebStorageCookieStore.prototype.getAllCookies = function(callback) { + WebStorageCookieStore.prototype.getAllCookies = function (callback) { var cookies = []; var store = this._readStore(); - Object.keys(store).forEach(function(domain) { - Object.keys(store[domain]).forEach(function(path) { - Array.protype.push.apply(cookies, _.values(store[domain][path])); - }); + Object.keys(store).forEach(function (domain) { + Object.keys(store[domain]).forEach(function (path) { + Array.protype.push.apply(cookies, _.values(store[domain][path])); + }); }); - cookies = cookies.map(function(cookie) { - return ToughCookie.Cookie.fromJSON(cookie); + cookies = cookies.map(function (cookie) { + return ToughCookie.Cookie.fromJSON(cookie); }); - cookies.sort(function(c1, c2) { - return (c1.creationIndex || 0) - (c2.creationIndex || 0); + cookies.sort(function (c1, c2) { + return (c1.creationIndex || 0) - (c2.creationIndex || 0); }); callback(null, cookies); -}; + }; -WebStorageCookieStore.prototype._readStore = function() { + WebStorageCookieStore.prototype._readStore = function () { var json = this._storage.getItem(this._storeKey); if (json !== null) { - try { - return JSON.parse(json); - } catch (e) { } + try { + return JSON.parse(json); + } catch (e) { } } return {}; -}; + }; -WebStorageCookieStore.prototype._writeStore = function(store) { + WebStorageCookieStore.prototype._writeStore = function (store) { this._storage.setItem(this._storeKey, JSON.stringify(store)); -}; + }; -module.exports = WebStorageCookieStore; + return WebStorageCookieStore; +}; diff --git a/www/public-interface.js b/www/public-interface.js new file mode 100644 index 0000000..38fe2f2 --- /dev/null +++ b/www/public-interface.js @@ -0,0 +1,158 @@ +module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConfigs) { + const publicInterface = { + getBasicAuthHeader: getBasicAuthHeader, + useBasicAuth: useBasicAuth, + getHeaders: getHeaders, + setHeader: setHeader, + getDataSerializer: getDataSerializer, + setDataSerializer: setDataSerializer, + setCookie: setCookie, + clearCookies: clearCookies, + removeCookies: removeCookies, + getCookieString: getCookieString, + getRequestTimeout: getRequestTimeout, + setRequestTimeout: setRequestTimeout, + setSSLCertMode: setSSLCertMode, + disableRedirect: disableRedirect, + sendRequest: sendRequest, + post: post, + get: get, + put: put, + patch: patch, + delete: del, + head: head, + uploadFile: uploadFile, + downloadFile: downloadFile + }; + + function getBasicAuthHeader(username, password) { + return { 'Authorization': 'Basic ' + helpers.b64EncodeUnicode(username + ':' + password) }; + } + + function useBasicAuth(username, password) { + this.setHeader('*', 'Authorization', 'Basic ' + helpers.b64EncodeUnicode(username + ':' + password)); + } + + function getHeaders(host) { + return globalConfigs.headers[host || '*'] || null; + } + + function setHeader() { + // this one is for being backward compatible + var host = '*'; + var header = arguments[0]; + var value = arguments[1]; + + if (arguments.length === 3) { + host = arguments[0]; + header = arguments[1]; + value = arguments[2]; + } + + helpers.checkForBlacklistedHeaderKey(header); + helpers.checkForInvalidHeaderValue(value); + + globalConfigs.headers[host] = globalConfigs.headers[host] || {}; + globalConfigs.headers[host][header] = value; + } + + function getDataSerializer() { + return globalConfigs.serializer; + } + + function setDataSerializer(serializer) { + globalConfigs.serializer = helpers.checkSerializer(serializer); + } + + function setCookie(url, cookie, options) { + cookieHandler.setCookie(url, cookie, options); + } + + function clearCookies() { + cookieHandler.clearCookies(); + } + + function removeCookies(url, callback) { + cookieHandler.removeCookies(url, callback); + } + + function getCookieString(url) { + return cookieHandler.getCookieString(url); + } + + function getRequestTimeout() { + return globalConfigs.timeout; + } + + function setRequestTimeout(timeout) { + globalConfigs.timeout = timeout; + } + + function setSSLCertMode(mode, success, failure) { + return exec(success, failure, 'CordovaHttpPlugin', 'setSSLCertMode', [helpers.checkSSLCertMode(mode)]); + } + + function disableRedirect(disable, success, failure) { + return exec(success, failure, 'CordovaHttpPlugin', 'disableRedirect', [!!disable]); + } + + function sendRequest(url, options, success, failure) { + helpers.handleMissingCallbacks(success, failure); + + options = helpers.handleMissingOptions(options, globalConfigs); + url = urlUtil.appendQueryParamsString(url, urlUtil.serializeQueryParams(options.params, true)); + + var headers = helpers.getMergedHeaders(url, options.headers, globalConfigs.headers); + var onSuccess = helpers.injectCookieHandler(url, success); + var onFail = helpers.injectCookieHandler(url, failure); + + switch (options.method) { + case 'post': + case 'put': + case 'patch': + var data = helpers.getProcessedData(options.data, options.serializer); + return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout]); + case 'upload': + return exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFile', [url, headers, options.filePath, options.name, options.timeout]); + case 'download': + var onDownloadSuccess = helpers.injectCookieHandler(url, helpers.injectFileEntryHandler(success)); + return exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, options.filePath, options.timeout]); + default: + return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout]); + } + } + + function post(url, data, headers, success, failure) { + return publicInterface.sendRequest(url, { method: 'post', data: data, headers: headers }, success, failure); + }; + + function get(url, params, headers, success, failure) { + return publicInterface.sendRequest(url, { method: 'get', params: params, headers: headers }, success, failure); + }; + + function put(url, data, headers, success, failure) { + return publicInterface.sendRequest(url, { method: 'put', data: data, headers: headers }, success, failure); + } + + function patch(url, data, headers, success, failure) { + return publicInterface.sendRequest(url, { method: 'patch', data: data, headers: headers }, success, failure); + } + + function del(url, params, headers, success, failure) { + return publicInterface.sendRequest(url, { method: 'delete', params: params, headers: headers }, success, failure); + } + + function head(url, params, headers, success, failure) { + return publicInterface.sendRequest(url, { method: 'head', params: params, headers: headers }, success, failure); + } + + function uploadFile(url, params, headers, filePath, name, success, failure) { + return publicInterface.sendRequest(url, { method: 'upload', params: params, headers: headers, filePath: filePath, name: name }, success, failure); + } + + function downloadFile(url, params, headers, filePath, success, failure) { + return publicInterface.sendRequest(url, { method: 'download', params: params, headers: headers, filePath: filePath }, success, failure); + } + + return publicInterface; +} diff --git a/www/url-util.js b/www/url-util.js new file mode 100644 index 0000000..b1ce901 --- /dev/null +++ b/www/url-util.js @@ -0,0 +1,71 @@ +module.exports = function init(helpers) { + return { + parseUrl: parseUrl, + serializeQueryParams: serializeQueryParams, + appendQueryParamsString: appendQueryParamsString + } + + function parseUrl(url) { + var match = url.match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/); + + return match && { + protocol: match[1], + host: match[2], + hostname: match[3], + port: match[4] || '', + pathname: match[5], + search: match[6], + hash: match[7] + } + } + + function serializeQueryParams(params, encode) { + return serializeObject(params, encode); + } + + function serializeObject(object, encode) { + var fragments = []; + + for (var key in object) { + if (!object.hasOwnProperty(key)) { + continue; + } + + if (helpers.getTypeOf(object[key]) === 'Array') { + fragments.push(serializeArray(object[key], encode)); + continue; + } else if (helpers.getTypeOf(object[key]) === 'Object') { + fragments.push(serializeObject(object[key], encode)); + continue; + } + + if (encode) { + fragments.push(encodeURIComponent(key) + '=' + encodeURIComponent(object[key])); + } else { + fragments.push(key + '=' + object[key]); + } + } + + return fragments.join('&'); + } + + function serializeArray(array, encode) { + + } + + function appendQueryParamsString(url, params) { + if (!url.length || !params.length) { + return url; + } + + var parsed = parseUrl(url); + + return parsed.protocol + + '//' + + parsed.host + + parsed.pathname + + (parsed.search.length ? parsed.search + '&' + params : '?' + params) + + parsed.hash; + } +}; +