Compare commits

...

20 Commits

Author SHA1 Message Date
Sefa Ilkimen
7d7f02b4b3 release v2.3.0 2019-12-02 02:20:52 +01:00
Sefa Ilkimen
afc9e3e944 Update readme 2019-12-01 07:30:45 +01:00
Sefa Ilkimen
9af2d1c21a Update license and readme file 2019-12-01 06:47:29 +01:00
Sefa Ilkimen
9dce2fb964 Update badges in readme 2019-12-01 06:28:21 +01:00
Sefa Ilkimen
eb946b49ab Add GitHub Actions CI workflow 2019-12-01 06:08:16 +01:00
Sefa Ilkimen
72ca81b515 Update changelog 2019-11-30 17:36:49 +01:00
Sefa Ilkimen
68633f1bb8 chore: update changelog and readme 2019-11-18 04:22:37 +01:00
Sefa Ilkimen
ee26b78e0d chore: update mocha timeout values 2019-11-18 03:09:32 +01:00
Sefa Ilkimen
d924f98844 chore: update slack notifications for travis builds 2019-11-18 02:59:22 +01:00
Sefa Ilkimen
8fceb4df97 chore: configure mocha threshold values 2019-11-18 02:28:52 +01:00
Sefa Ilkimen
7a09fa9460 feat: add ponyfills to support multipart requests on android webview versions < 50 and iOS versions < 13.2 2019-11-18 02:01:02 +01:00
Sefa Ilkimen
3e5c941fdd refactor: iOS implementation 2019-11-17 22:46:51 +01:00
Sefa Ilkimen
0f273f401b feat(ios): implement multipart requests
- expose new AFHTTPSessionManager method "uploadTaskWithHTTPMethod"
2019-11-17 21:29:38 +01:00
Sefa Ilkimen
684874184d - refactor: iOS implementation
- chore: add flags to run only one spec or disable spec in e2e tests
2019-11-17 19:33:19 +01:00
Sefa Ilkimen
19e1e7206b implement e2e tests for #101 2019-11-14 04:45:20 +01:00
Sefa Ilkimen
21bec76c11 refactor e2e test scripts 2019-11-14 02:06:33 +01:00
Sefa Ilkimen
594d03aa41 WIP: major progress for #101
- feat(www): implement preprocessor for FormData instances
- feat(www): implement API checks for multipart requests
- feat(android): implement multipart requests
- chore(specs): implement www specs for new prprocessor
2019-11-11 04:49:35 +01:00
Sefa Ilkimen
b3276ad2d4 update cordova-ios in test app template 2019-11-10 17:05:52 +01:00
Sefa Ilkimen
3b4a5b7c26 add missing module reference for "dependency-validator.js" 2019-11-10 06:55:38 +01:00
Sefa Ilkimen
867b8ea202 - WIP: implement data pre-processor for #101
- implement checks for #101
- add some specs
2019-11-10 06:40:31 +01:00
37 changed files with 1580 additions and 825 deletions

56
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Cordova HTTP Plugin CI
on: [push]
env:
nodejs: '10.x'
jobs:
test-www-interface:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Install Node.js ${{ env.nodejs }}
uses: actions/setup-node@v1
with:
node-version: ${{ env.nodejs }}
- name: Install node modules
run: npm ci
- name: Run WWW interface tests
run: npm run testjs
build-ios:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Install Node.js ${{ env.nodejs }}
uses: actions/setup-node@v1
with:
node-version: ${{ env.nodejs }}
- name: Install node modules
run: npm ci
- name: Update test cert for httpbin.org
run: npm run updatecert
- name: Build test app
run: scripts/build-test-app.sh --ios --emulator
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Install Node.js ${{ env.nodejs }}
uses: actions/setup-node@v1
with:
node-version: ${{ env.nodejs }}
- name: Install node modules
run: npm ci
- name: Install JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Update test cert for httpbin.org
run: npm run updatecert
- name: Add workaround for mipsel reference
run: sudo mkdir -p $ANDROID_HOME/ndk-bundle/toolchains/mips64el-linux-android-4.9/prebuilt/linux-x86_64
- name: Build test app
run: scripts/build-test-app.sh --android --emulator

View File

@@ -1,6 +1,6 @@
notifications:
slack:
secure: lXE+2AgsxZU5G5dI91LkMAIgo8MAWfdM7DB5UOtn5LpuNln+2FmJo1gOI7tkdmLOqpXTGYnpI2VyQN3H4nOF21YhuouzD1Sh8n2wtQg1iTm353kuQpqiVhSBX8ZJ7Be1e1G8OsnxoYOxbs4Zo9qI40EruwkvqLCBHWM5MRGyd4M7EFWwb9Z29VZN0y1Nt5g/c3bT76kdKmF+JCLur2OeEKxAity7sIKgZekSqeIMwEVLSxXnda6Dbjc/cg0MJ0iDArkD7iu6fz/Fcrrxgm/pUxjcgvqze7Gy5i31mjEfspnrglWV1cshMd48BTDKCJ2AMmxH8O3GPSWE2txjIvGRWUve7iViNylvmQCVz3Eyf99+4EuuVGa+5PSodQ/CqODx/65EwtcN3PE1tNz2puKOK8nrOJcFkcbG8KTHKUlQtHCkjitbykUnj/hvhLK5/oWlQYVOLWWrHwdGUh8FI8aFPVGjRjWbHbhdayjEIqxwr1ns+6mYrP1EFNXbaeZxnLNC59XpJl1ifuezqYAk7YEiU5j4rtC7YKgyQ3ueb7anOHTJoTMyDn8mpZXgwuyhoBaeEYytQVgRyMtL6Y5cP98Jn2kv0+vdne3rkk9/JEBTo32HOjvoij6rsqEvXC0LhUDJSNadOVdHht0jjoN6zBH37HIE5/3zysLlPcAcHAS83ow=
secure: twDT06GAiu0jsKizow7TcghZj70KbuuTrlo02QGmbSxBk2rJfsXSrHAsA3+s/9Q4mudENk6na7fs1aPCxz+u2etUGp+2PaJKVKR5n3jrNNt3SnYeWsBgVo7o7H1aLXatX3a+TdPXh1F5gQ4Ycr93nTYbW/077jsOholwbOHDZE3VcU9dzNPwFaEvhrDbr/ei3tef0ZiM1qxIad74TgwWMKClwai3I7HCVkZOPsyV+ve6cdIJ8Dt47JzFUHSW3SZuoe5Kywxvp0VvMo/QAJw95y3edNafx4EXHwbaN71rpGWSJXIKSZzcSQalZJ9DxGYspIBkWvGsNuQRzG9CzIoNQK10iERlIVC5vKDfKX22gayOQPSDkswJzIduylBUC8zdTPCndXyNEM/Lrj6hg+ksFWN58vYNPgfUeiga7X+LV5HytftsMFW+xx2kbnGeU8doGeX8Q8G7h9OIkHCTTG7R0ldYMIqTm8YJGPkRIv4OReC5ZOhiZD+wSg4KQ0wmMeRi+hyn+I5UPnKEOHAIN8FmLNCZFbgr1wuPFp9xnJIOcumQnQVZ2t6vk6IjIbwhYPWCnf7Sr4BvJxE8eyiLrEaXK0FiPb3My9wK9tLFjj1zdD7e4+SLq+WFMeCxp2eXOGF0Bu+2VK2tGjgWhaudaIpjbRQAAQ5nPa43h16NruEvNWI=
cache:
directories:

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}

View File

@@ -1,5 +1,12 @@
# Changelog
## 2.3.0
- Feature #101: Support "multipart/form-data" requests (thanks SDA SE Open Industry Solutions)
#### Important information
This feature depends on several Web APIs. See https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.
## 2.2.0
- Feature #239: add enumeration style object for error codes

View File

@@ -1,5 +1,6 @@
The MIT License (MIT)
Copyright (c) 2019 Sefa Ilkimen
Copyright (c) 2017 Mobisys GmbH
Copyright (c) 2014 Wymsee, Inc

View File

@@ -3,7 +3,9 @@ Cordova Advanced HTTP
[![npm version](https://badge.fury.io/js/cordova-plugin-advanced-http.svg)](https://badge.fury.io/js/cordova-plugin-advanced-http)
[![downloads/month](https://img.shields.io/npm/dm/cordova-plugin-advanced-http.svg)](https://www.npmjs.com/package/cordova-plugin-advanced-http)
[![MIT Licence](https://badges.frapsoft.com/os/mit/mit.png)](https://opensource.org/licenses/mit-license.php)
[![Build Status](https://travis-ci.org/silkimen/cordova-plugin-advanced-http.svg?branch=master)](https://travis-ci.org/silkimen/cordova-plugin-advanced-http)
[![Travis Build Status](https://img.shields.io/travis/silkimen/cordova-plugin-advanced-http/master?label=Travis%20CI)](https://travis-ci.org/silkimen/cordova-plugin-advanced-http)
[![GitHub Build Status](https://img.shields.io/github/workflow/status/silkimen/cordova-plugin-advanced-http/Cordova%20HTTP%20Plugin%20CI/master?label=GitHub%20Actions)](https://github.com/silkimen/cordova-plugin-advanced-http/actions)
Cordova / Phonegap plugin for communicating with HTTP servers. Supports iOS, Android and [Browser](#browserSupport).
@@ -12,9 +14,9 @@ This is a fork of [Wymsee's Cordova-HTTP plugin](https://github.com/wymsee/cordo
## Advantages over Javascript requests
- Background threading - all requests are done in a background thread.
- Handling of HTTP code 401 - read more at [Issue CB-2415](https://issues.apache.org/jira/browse/CB-2415).
- SSL Pinning
- SSL / TLS Pinning
- CORS restrictions do not apply
- Handling of HTTP code 401 - read more at [Issue CB-2415](https://issues.apache.org/jira/browse/CB-2415)
## Updates
@@ -92,10 +94,13 @@ You can choose one of these:
* `urlencoded`: send data as url encoded content in body (content type "application/x-www-form-urlencoded")
* `json`: send data as JSON encoded content in body (content type "application/json")
* `utf8`: send data as plain UTF8 encoded string in body (content type "plain/text")
* `multipart`: send FormData objects as multipart content in body (content type "multipart/form-data")
This defaults to `urlencoded`. You can also override the default content type headers by specifying your own headers (see [setHeader](#setHeader)).
__Caution__: `urlencoded` does not support serializing deep structures whereas `json` does.
:warning: `urlencoded` does not support serializing deep structures whereas `json` does.
:warning: `multipart` depends on several Web API standards which need to be supported in your web view. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.
### setRequestTimeout
Set how long to wait for a request to respond, in seconds.
@@ -176,15 +181,6 @@ This function was deprecated in 2.0.9. Use ["setFollowRedirect"](#setFollowRedir
### setSSLCertMode (deprecated)
This function was deprecated in 2.0.8. Use ["setServerTrustMode"](#setServerTrustMode) instead.
### enableSSLPinning (obsolete)
This function was removed in 2.0.0. Use ["setServerTrustMode"](#setServerTrustMode) to enable SSL pinning (mode "pinned").
### acceptAllCerts (obsolete)
This function was removed in 2.0.0. Use ["setServerTrustMode"](#setServerTrustMode) to disable checking certs (mode "nocheck").
### validateDomainName (obsolete)
This function was removed in v1.6.2. Domain name validation is disabled automatically when you set server trust mode to "nocheck".
### removeCookies
Remove all cookies associated with a given URL.
@@ -388,6 +384,7 @@ Following features are *not* supported:
* Pinning SSL certificate
* Disabling SSL certificate check
* Disabling transparently following redirects (HTTP codes 3xx)
* Circumventing CORS restrictions
## Libraries
@@ -399,6 +396,16 @@ This plugin utilizes some awesome open source libraries:
We made a few modifications to the networking libraries.
## CI Builds & E2E Testing
This plugin uses amazing cloud services to maintain quality. CI Builds and E2E testing are powered by:
* [GitHub Actions](https://github.com/features/actions)
* [Travis CI](https://travis-ci.org/)
* [BrowserStack](https://www.browserstack.com/)
* [Sauce Labs](https://saucelabs.com/)
* [httpbin.org](https://httpbin.org/)
## Contribute & Develop
We've set up a separate document for our [contribution guidelines](CONTRIBUTING.md).

633
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,14 @@
{
"name": "cordova-plugin-advanced-http",
"version": "2.2.0",
"version": "2.3.0",
"description": "Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning",
"scripts": {
"updatecert": "node ./scripts/update-e2e-server-cert.js && node ./scripts/update-e2e-client-cert.js",
"buildbrowser": "./scripts/build-test-app.sh --browser",
"testandroid": "npm run updatecert && ./scripts/build-test-app.sh --android --emulator && ./scripts/test-app.sh --android --emulator",
"testios": "npm run updatecert && ./scripts/build-test-app.sh --ios --emulator && ./scripts/test-app.sh --ios --emulator",
"buildandroid": "./scripts/build-test-app.sh --android --emulator",
"buildios": "./scripts/build-test-app.sh --ios --emulator",
"testandroid": "npm run updatecert && npm run buildandroid && ./scripts/test-app.sh --android --emulator",
"testios": "npm run updatecert && npm run buildios && ./scripts/test-app.sh --ios --emulator",
"testapp": "npm run testandroid && npm run testios",
"testjs": "mocha ./test/js-specs.js",
"test": "npm run testjs && npm run testapp",

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-advanced-http" version="2.2.0">
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-advanced-http" version="2.3.0">
<name>Advanced HTTP plugin</name>
<description>
Cordova / Phonegap plugin for communicating with HTTP servers using SSL pinning
@@ -9,6 +9,7 @@
</engines>
<dependency id="cordova-plugin-file" version=">=2.0.0"/>
<js-module src="www/cookie-handler.js" name="cookie-handler"/>
<js-module src="www/dependency-validator.js" name="dependency-validator"/>
<js-module src="www/error-codes.js" name="error-codes"/>
<js-module src="www/global-configs.js" name="global-configs"/>
<js-module src="www/helpers.js" name="helpers"/>
@@ -16,6 +17,7 @@
<js-module src="www/local-storage-store.js" name="local-storage-store"/>
<js-module src="www/lodash.js" name="lodash"/>
<js-module src="www/messages.js" name="messages"/>
<js-module src="www/ponyfills.js" name="ponyfills"/>
<js-module src="www/public-interface.js" name="public-interface"/>
<js-module src="www/umd-tough-cookie.js" name="tough-cookie"/>
<js-module src="www/url-util.js" name="url-util"/>

View File

@@ -1,7 +1,9 @@
package com.silkimen.cordovahttp;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
@@ -19,9 +21,11 @@ import com.silkimen.http.TLSConfiguration;
import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Base64;
import android.util.Log;
abstract class CordovaHttpBase implements Runnable {
@@ -120,7 +124,7 @@ abstract class CordovaHttpBase implements Runnable {
request.readTimeout(this.timeout);
request.acceptCharset("UTF-8");
request.uncompress(true);
request.setConnectionFactory(new OkConnectionFactory());
HttpRequest.setConnectionFactory(new OkConnectionFactory());
if (this.tlsConfiguration.getHostnameVerifier() != null) {
request.setHostnameVerifier(this.tlsConfiguration.getHostnameVerifier());
@@ -141,6 +145,8 @@ abstract class CordovaHttpBase implements Runnable {
request.contentType("text/plain", "UTF-8");
} else if ("urlencoded".equals(this.serializer)) {
// intentionally left blank, because content type is set in HttpRequest.form()
} else if ("multipart".equals(this.serializer)) {
request.contentType("multipart/form-data");
}
}
@@ -155,6 +161,22 @@ abstract class CordovaHttpBase implements Runnable {
request.send(((JSONObject) this.data).getString("text"));
} else if ("urlencoded".equals(this.serializer)) {
request.form(JsonUtils.getObjectMap((JSONObject) this.data));
} else if ("multipart".equals(this.serializer)) {
JSONArray buffers = ((JSONObject) this.data).getJSONArray("buffers");
JSONArray names = ((JSONObject) this.data).getJSONArray("names");
JSONArray fileNames = ((JSONObject) this.data).getJSONArray("fileNames");
JSONArray types = ((JSONObject) this.data).getJSONArray("types");
for (int i = 0; i < buffers.length(); ++i) {
byte[] bytes = Base64.decode(buffers.getString(i), Base64.DEFAULT);
String name = names.getString(i);
if (fileNames.isNull(i)) {
request.part(name, new String(bytes, "UTF-8"));
} else {
request.part(name, fileNames.getString(i), types.getString(i), new ByteArrayInputStream(bytes));
}
}
}
}

View File

@@ -290,6 +290,64 @@ NS_ASSUME_NONNULL_BEGIN
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with a multipart request using given HTTP method.
@param HTTPMethod The HTTP method used to create the request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol.
@param uploadProgress A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:
*/
- (nullable NSURLSessionDataTask *)uploadTaskWithHTTPMethod:(NSString *)HTTPMethod
URLString:(NSString *)URLString
parameters:(nullable id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with given HTTP method.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param uploadProgress A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:
*/
- (nullable NSURLSessionDataTask *)uploadTaskWithHTTPMethod:(NSString *)HTTPMethod
URLString:(NSString *)URLString
parameters:(nullable id)parameters
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
/**
Creates and runs an `NSURLSessionDataTask` with a given HTTP method.
@param HTTPMethod The HTTP method used to create the request.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded according to the client request serializer.
@param downloadProgress A block object to be executed when the download progress is updated. Note this block is called on the session queue, not the main queue.
@param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer.
@param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred.
@see -dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:
*/
- (nullable NSURLSessionDataTask *)downloadTaskWithHTTPMethod:(NSString *)HTTPMethod
URLString:(NSString *)URLString
parameters:(nullable id)parameters
progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
@end
NS_ASSUME_NONNULL_END

View File

@@ -223,6 +223,42 @@
return dataTask;
}
- (NSURLSessionDataTask *)PUT:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
if (serializationError) {
if (failure) {
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
}
return nil;
}
__block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:uploadProgress completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(task, error);
}
} else {
if (success) {
success(task, responseObject);
}
}
}];
[task resume];
return task;
}
- (NSURLSessionDataTask *)PATCH:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
@@ -247,6 +283,71 @@
return dataTask;
}
- (NSURLSessionDataTask *)uploadTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSError *serializationError = nil;
NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
if (serializationError) {
if (failure) {
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
}
return nil;
}
__block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:uploadProgress completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(task, error);
}
} else {
if (success) {
success(task, responseObject);
}
}
}];
[task resume];
return task;
}
- (NSURLSessionDataTask *)uploadTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
progress:(void (^)(NSProgress * _Nonnull))uploadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:method URLString:URLString parameters:parameters uploadProgress:uploadProgress downloadProgress:nil success:success failure:failure];
[dataTask resume];
return dataTask;
}
- (NSURLSessionDataTask *)downloadTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
progress:(nullable void (^)(NSProgress * _Nonnull))downloadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success
failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{
NSURLSessionDataTask *task = [self dataTaskWithHTTPMethod:method URLString:URLString parameters:parameters uploadProgress:nil downloadProgress:downloadProgress success:success failure:failure];
[task resume];
return task;
}
- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters

View File

@@ -142,6 +142,142 @@
return headerFieldsCopy;
}
- (void)executeRequestWithoutData:(CDVInvokedUrlCommand*)command withMethod:(NSString*) method {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *headers = [command.arguments objectAtIndex:1];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:3] boolValue];
NSString *responseType = [command.arguments objectAtIndex:4];
[self setRequestSerializer: @"default" forManager: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
[self setResponseSerializer:responseType forManager:manager];
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
// no 'body' for HEAD request, omitting 'data'
if ([method isEqualToString:@"HEAD"]) {
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:nil];
} else {
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
}
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
};
void (^onFailure)(NSURLSessionTask *, NSError *) = ^(NSURLSessionTask *task, NSError *error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
};
[manager downloadTaskWithHTTPMethod:method URLString:url parameters:nil progress:nil success:onSuccess failure:onFailure];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
}
- (void)executeRequestWithData:(CDVInvokedUrlCommand*)command withMethod:(NSString*)method {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *data = [command.arguments objectAtIndex:1];
NSString *serializerName = [command.arguments objectAtIndex:2];
NSDictionary *headers = [command.arguments objectAtIndex:3];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:5] boolValue];
NSString *responseType = [command.arguments objectAtIndex:6];
[self setRequestSerializer: serializerName forManager: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
[self setResponseSerializer:responseType forManager:manager];
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
void (^constructBody)(id<AFMultipartFormData>) = ^(id<AFMultipartFormData> formData) {
NSArray *buffers = [data mutableArrayValueForKey:@"buffers"];
NSArray *fileNames = [data mutableArrayValueForKey:@"fileNames"];
NSArray *names = [data mutableArrayValueForKey:@"names"];
NSArray *types = [data mutableArrayValueForKey:@"types"];
NSError *error;
for (int i = 0; i < [buffers count]; ++i) {
NSData *decodedBuffer = [[NSData alloc] initWithBase64EncodedString:[buffers objectAtIndex:i] options:0];
NSString *fileName = [fileNames objectAtIndex:i];
NSString *partName = [names objectAtIndex:i];
NSString *partType = [types objectAtIndex:i];
if (![fileName isEqual:[NSNull null]]) {
[formData appendPartWithFileData:decodedBuffer name:partName fileName:fileName mimeType:partType];
} else {
[formData appendPartWithFormData:decodedBuffer name:[names objectAtIndex:i]];
}
}
if (error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[dictionary setObject:[NSNumber numberWithInt:400] forKey:@"status"];
[dictionary setObject:@"Could not add part to multipart request body." forKey:@"error"];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
return;
}
};
void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
};
void (^onFailure)(NSURLSessionTask *, NSError *) = ^(NSURLSessionTask *task, NSError *error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
};
if ([serializerName isEqualToString:@"multipart"]) {
[manager uploadTaskWithHTTPMethod:method URLString:url parameters:nil constructingBodyWithBlock:constructBody progress:nil success:onSuccess failure:onFailure];
} else {
[manager uploadTaskWithHTTPMethod:method URLString:url parameters:data progress:nil success:onSuccess failure:onFailure];
}
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
}
- (void)setServerTrustMode:(CDVInvokedUrlCommand*)command {
NSString *certMode = [command.arguments objectAtIndex:0];
@@ -164,260 +300,27 @@
}
- (void)get:(CDVInvokedUrlCommand*)command {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *headers = [command.arguments objectAtIndex:1];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:3] boolValue];
NSString *responseType = [command.arguments objectAtIndex:4];
[self setRequestSerializer: @"default" forManager: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
[self setResponseSerializer:responseType forManager:manager];
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager GET:url parameters:nil success:^(NSURLSessionTask *task, id responseObject) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
}
- (void)head:(CDVInvokedUrlCommand*)command {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = securityPolicy;
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *headers = [command.arguments objectAtIndex:1];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:3] boolValue];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager HEAD:url parameters:nil success:^(NSURLSessionTask *task) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
// no 'body' for HEAD request, omitting 'data'
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:nil];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
[self executeRequestWithoutData: command withMethod:@"GET"];
}
- (void)delete:(CDVInvokedUrlCommand*)command {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = securityPolicy;
[self executeRequestWithoutData: command withMethod:@"DELETE"];
}
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *headers = [command.arguments objectAtIndex:1];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:3] boolValue];
NSString *responseType = [command.arguments objectAtIndex:4];
[self setRequestSerializer: @"default" forManager: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
[self setResponseSerializer:responseType forManager:manager];
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager DELETE:url parameters:nil success:^(NSURLSessionTask *task, id responseObject) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
- (void)head:(CDVInvokedUrlCommand*)command {
[self executeRequestWithoutData: command withMethod:@"HEAD"];
}
- (void)post:(CDVInvokedUrlCommand*)command {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *data = [command.arguments objectAtIndex:1];
NSString *serializerName = [command.arguments objectAtIndex:2];
NSDictionary *headers = [command.arguments objectAtIndex:3];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:5] boolValue];
NSString *responseType = [command.arguments objectAtIndex:6];
[self setRequestSerializer: serializerName forManager: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
[self setResponseSerializer:responseType forManager:manager];
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager POST:url parameters:data success:^(NSURLSessionTask *task, id responseObject) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
[self executeRequestWithData: command withMethod:@"POST"];
}
- (void)put:(CDVInvokedUrlCommand*)command {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *data = [command.arguments objectAtIndex:1];
NSString *serializerName = [command.arguments objectAtIndex:2];
NSDictionary *headers = [command.arguments objectAtIndex:3];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:5] boolValue];
NSString *responseType = [command.arguments objectAtIndex:6];
[self setRequestSerializer: serializerName forManager: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
[self setResponseSerializer:responseType forManager:manager];
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager PUT:url parameters:data success:^(NSURLSessionTask *task, id responseObject) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
[self executeRequestWithData: command withMethod:@"PUT"];
}
- (void)patch:(CDVInvokedUrlCommand*)command {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.securityPolicy = securityPolicy;
NSString *url = [command.arguments objectAtIndex:0];
NSDictionary *data = [command.arguments objectAtIndex:1];
NSString *serializerName = [command.arguments objectAtIndex:2];
NSDictionary *headers = [command.arguments objectAtIndex:3];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:5] boolValue];
NSString *responseType = [command.arguments objectAtIndex:6];
[self setRequestSerializer: serializerName forManager: manager];
[self setRequestHeaders: headers forManager: manager];
[self setTimeout:timeoutInSeconds forManager:manager];
[self setRedirect:followRedirect forManager:manager];
[self setResponseSerializer:responseType forManager:manager];
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager PATCH:url parameters:data success:^(NSURLSessionTask *task, id responseObject) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
[self executeRequestWithData: command withMethod:@"PATCH"];
}
- (void)uploadFiles:(CDVInvokedUrlCommand*)command {
@@ -503,7 +406,7 @@
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
[manager GET:url parameters:nil success:^(NSURLSessionTask *task, id responseObject) {
[manager GET:url parameters:nil progress: nil success:^(NSURLSessionTask *task, id responseObject) {
/*
*
* Licensed to the Apache Software Foundation (ASF) under one

View File

@@ -24,7 +24,7 @@
</platform>
<engine name="android" spec="7.1.0" />
<engine name="browser" spec="5.0.0" />
<engine name="ios" spec="4.4.0" />
<engine name="ios" spec="5.0.1" />
<plugin name="cordova-plugin-file" spec="6.0.1" />
<preference name="AndroidPersistentFileLocation" value="Internal" />
</widget>

View File

@@ -14,7 +14,7 @@
"cordova": "7.0.1",
"cordova-android": "7.1.0",
"cordova-browser": "5.0.0",
"cordova-ios": "4.4.0"
"cordova-ios": "5.0.1"
},
"cordova": {
"platforms": [

View File

@@ -3,8 +3,25 @@ const app = {
lastResult: null,
testsFlaggedToRun: [],
initialize: function () {
document.getElementById('nextBtn').addEventListener('click', app.onNextBtnClick);
var onlyFlaggedTests = [];
var enabledTests = [];
tests.forEach(function (test) {
if (test.only) {
onlyFlaggedTests.push(test);
}
if (!test.disabled) {
enabledTests.push(test);
}
});
app.testsFlaggedToRun = onlyFlaggedTests.length ? onlyFlaggedTests : enabledTests;
},
printResult: function (prefix, content) {
@@ -47,9 +64,9 @@ const app = {
cb(app.lastResult);
},
runTest: function (index) {
runTest: function (tests, index) {
const testDefinition = tests[index];
const titleText = app.testIndex + ': ' + testDefinition.description;
const titleText = index + ': ' + testDefinition.description;
const expectedText = 'expected - ' + testDefinition.expected;
document.getElementById('statusInput').value = 'running';
@@ -130,8 +147,8 @@ const app = {
onNextBtnClick: function () {
app.testIndex += 1;
if (app.testIndex < tests.length) {
app.runTest(app.testIndex);
if (app.testIndex < app.testsFlaggedToRun.length) {
app.runTest(app.testsFlaggedToRun, app.testIndex);
} else {
app.onFinishedAllTests();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -33,6 +33,7 @@ const helpers = {
setJsonSerializer: function (resolve) { resolve(cordova.plugin.http.setDataSerializer('json')); },
setUtf8StringSerializer: function (resolve) { resolve(cordova.plugin.http.setDataSerializer('utf8')); },
setUrlEncodedSerializer: function (resolve) { resolve(cordova.plugin.http.setDataSerializer('urlencoded')); },
setMultipartSerializer: function (resolve) { resolve(cordova.plugin.http.setDataSerializer('multipart')); },
disableFollowingRedirect: function (resolve) { resolve(cordova.plugin.http.setFollowRedirect(false)); },
enableFollowingRedirect: function(resolve) { resolve(cordova.plugin.http.setFollowRedirect(true)); },
getWithXhr: function (done, url, type) {
@@ -74,6 +75,13 @@ const helpers = {
}
return hash;
},
checkResult: function (result, expected) {
if (result.type === 'throwed' && expected !== 'throwed') {
throw new Error('Expected function not to throw: ' + result.message);
}
result.type.should.be.equal(expected);
}
};
@@ -789,8 +797,55 @@ const tests = [
result.data.status.should.be.equal(418);
result.data.error.should.be.equal("\n -=[ teapot ]=-\n\n _...._\n .' _ _ `.\n | .\"` ^ `\". _,\n \\_;`\"---\"`|//\n | ;/\n \\_ _/\n `\"\"\"`\n");
}
},
{
description: 'should serialize FormData instance correctly when it contains string value',
expected: 'resolved: {"status": 200, ...',
before: helpers.setMultipartSerializer,
func: function (resolve, reject) {
var ponyfills = cordova.plugin.http.ponyfills;
var formData = new ponyfills.FormData();
formData.append('myString', 'This is a test!');
var url = 'https://httpbin.org/anything';
var options = { method: 'post', data: formData };
cordova.plugin.http.sendRequest(url, options, resolve, reject);
},
validationFunc: function (driver, result) {
helpers.checkResult(result, 'resolved');
result.data.status.should.be.equal(200);
JSON.parse(result.data.data).form.should.be.eql({ myString: 'This is a test!' });
}
},
{
description: 'should serialize FormData instance correctly when it contains blob value',
expected: 'resolved: {"status": 200, ...',
before: helpers.setMultipartSerializer,
func: function (resolve, reject) {
var ponyfills = cordova.plugin.http.ponyfills;
helpers.getWithXhr(function(blob) {
var formData = new ponyfills.FormData();
formData.append('CordovaLogo', blob);
var url = 'https://httpbin.org/anything';
var options = { method: 'post', data: formData };
cordova.plugin.http.sendRequest(url, options, resolve, reject);
}, './res/cordova_logo.png', 'blob');
},
validationFunc: function (driver, result) {
helpers.checkResult(result, 'resolved');
result.data.status.should.be.equal(200);
// httpbin.org encodes posted binaries in base64 and echoes them back
// therefore we need to check for base64 string with mime type prefix
const fs = require('fs');
const rawLogo = fs.readFileSync('./test/e2e-app-template/www/res/cordova_logo.png');
const b64Logo = rawLogo.toString('base64');
JSON.parse(result.data.data).files.CordovaLogo.should.be.equal('data:image/png;base64,' + b64Logo);
}
}
// @TODO: not ready yet
// TODO: not ready yet
// {
// description: 'should authenticate correctly when client cert auth is configured with a PKCS12 container',
// expected: 'resolved: {"status": 200, ...',

73
test/e2e-tooling/caps.js Normal file
View File

@@ -0,0 +1,73 @@
module.exports = { getCaps };
const path = require('path');
const configs = {
// testing on local machine
localIosDevice: {
platformName: 'iOS',
platformVersion: '10.3',
automationName: 'XCUITest',
deviceName: 'iPhone 8',
autoWebview: true,
app: path.resolve('temp/platforms/ios/build/emulator/HttpDemo.app')
},
localIosEmulator: {
platformName: 'iOS',
platformVersion: '13.2',
automationName: 'XCUITest',
deviceName: 'iPhone 8',
autoWebview: true,
app: path.resolve('temp/platforms/ios/build/emulator/HttpDemo.app')
},
localAndroidEmulator: {
platformName: 'Android',
platformVersion: '5',
deviceName: 'Android Emulator',
autoWebview: true,
fullReset: true,
app: path.resolve('temp/platforms/android/app/build/outputs/apk/debug/app-debug.apk')
},
// testing on SauceLabs
saucelabsIosDevice: {
browserName: '',
'appium-version': '1.9.1',
platformName: 'iOS',
platformVersion: '10.3',
deviceName: 'iPhone 6',
autoWebview: true,
app: 'sauce-storage:HttpDemo.app.zip'
},
saucelabsIosEmulator: {
browserName: '',
'appium-version': '1.9.1',
platformName: 'iOS',
platformVersion: '10.3',
deviceName: 'iPhone Simulator',
autoWebview: true,
app: 'sauce-storage:HttpDemo.app.zip'
},
saucelabsAndroidEmulator: {
browserName: '',
'appium-version': '1.9.1',
platformName: 'Android',
platformVersion: '5.1',
deviceName: 'Android Emulator',
autoWebview: true,
app: 'sauce-storage:HttpDemo.apk'
}
};
function getCaps(environment, os, runtime) {
const key = environment.toLowerCase() + capitalize(os) + capitalize(runtime);
const caps = configs[key];
caps.name = `cordova-plugin-advanced-http (${os})`;
return caps;
};
function capitalize(text) {
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
}

View File

@@ -1,10 +0,0 @@
const path = require('path');
if (process.env.SAUCE_USERNAME) {
exports.iosTestApp = 'sauce-storage:HttpDemo.app.zip';
exports.androidTestApp = 'sauce-storage:HttpDemo.apk';
} else {
// these paths are relative to working directory
exports.iosTestApp = path.resolve('temp/platforms/ios/build/emulator/HttpDemo.app');
exports.androidTestApp = path.resolve('temp/platforms/android/app/build/outputs/apk/debug/app-debug.apk');
}

View File

@@ -1,60 +0,0 @@
const local = {
iosDevice: {
platformName: 'iOS',
platformVersion: '10.3',
deviceName: 'iPhone 6',
autoWebview: true,
app: undefined // will be set later
},
iosEmulator: {
platformName: 'iOS',
platformVersion: '11.0',
deviceName: 'iPhone Simulator',
autoWebview: true,
app: undefined // will be set later
},
androidEmulator: {
platformName: 'Android',
platformVersion: '5.1',
deviceName: 'Android Emulator',
autoWebview: true,
fullReset: true,
app: undefined // will be set later
}
};
const sauce = {
iosDevice: {
browserName: '',
'appium-version': '1.9.1',
platformName: 'iOS',
platformVersion: '10.3',
deviceName: 'iPhone 6',
autoWebview: true,
app: undefined // will be set later
},
iosEmulator: {
browserName: '',
'appium-version': '1.9.1',
platformName: 'iOS',
platformVersion: '10.3',
deviceName: 'iPhone Simulator',
autoWebview: true,
app: undefined // will be set later
},
androidEmulator: {
browserName: '',
'appium-version': '1.9.1',
platformName: 'Android',
platformVersion: '5.1',
deviceName: 'Android Emulator',
autoWebview: true,
app: undefined // will be set later
}
};
if (process.env.SAUCE_USERNAME) {
module.exports = sauce;
} else {
module.exports = local;
}

View File

@@ -1,16 +0,0 @@
const local = {
host: 'localhost',
port: 4723
};
const sauce = {
host: 'ondemand.saucelabs.com',
port: 80,
auth: process.env.SAUCE_USERNAME + ":" + process.env.SAUCE_ACCESS_KEY
};
if (process.env.SAUCE_USERNAME) {
module.exports = sauce;
} else {
module.exports = local;
}

View File

@@ -1,4 +1,8 @@
exports.configure = driver => {
module.exports = { setupLogging };
function setupLogging(driver) {
require('colors');
driver.on('status', info => {
console.log(info.cyan);
});
@@ -10,4 +14,4 @@ exports.configure = driver => {
driver.on('http', (meth, path, data) => {
console.log(' > ' + meth.magenta, path, (data || '').grey);
});
};
}

View File

@@ -0,0 +1,17 @@
module.exports = { getServer };
const configs = {
local: {
host: 'localhost',
port: 4723
},
saucelabs: {
host: 'ondemand.saucelabs.com',
port: 80,
auth: process.env.SAUCE_USERNAME + ":" + process.env.SAUCE_ACCESS_KEY
}
}
function getServer(environment) {
return configs[environment.toLowerCase()];
}

View File

@@ -1,100 +1,124 @@
const wd = require('wd');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const apps = require('./helpers/apps');
const caps = Object.assign({}, require('./helpers/caps'));
const serverConfig = require('./helpers/server');
const logging = require('./logging');
const capsConfig = require('./caps');
const serverConfig = require('./server');
const testDefinitions = require('../e2e-specs');
const pkgjson = require('../../package.json');
chai.use(chaiAsPromised);
chaiAsPromised.transferPromiseness = wd.transferPromiseness;
global.should = chai.should();
require('colors');
describe('Advanced HTTP', function() {
describe('Advanced HTTP e2e test suite', function () {
const isSauceLabs = !!process.env.SAUCE_USERNAME;
const isVerbose = process.argv.includes('--verbose');
const isDevice = process.argv.includes('--device');
const isAndroid = process.argv.includes('--android');
const targetInfo = { isDevice, isAndroid };
let driver = null;
const targetInfo = { isSauceLabs, isDevice, isAndroid };
const environment = isSauceLabs ? 'saucelabs' : 'local';
let driver;
let allPassed = true;
this.timeout(900000);
this.timeout(15000);
this.slow(4000);
const getCaps = appName => {
const desiredOs = isAndroid ? 'android' : 'ios';
const desiredCaps = caps[desiredOs + (isDevice ? 'Device' : 'Emulator')];
const desiredApp = apps[desiredOs + appName];
before(async function () {
// connecting to saucelabs can take some time
this.timeout(300000);
desiredCaps.name = pkgjson.name + ` (${desiredOs})`;
desiredCaps.app = desiredApp;
driver = await wd.promiseChainRemote(serverConfig.getServer(environment));
return desiredCaps;
if (isVerbose) {
logging.setupLogging(driver);
}
await driver.init(
capsConfig.getCaps(
environment,
isAndroid ? 'android' : 'ios',
isDevice ? 'device' : 'emulator'
)
);
});
after(async function () {
await driver.quit().finally(
() => isSauceLabs && driver.sauceJobStatus(allPassed)
);
});
const defineTestForMocha = (test, index) => {
it(index + ': ' + test.description, async () => {
await clickNext(driver);
await validateTestIndex(driver, index);
await validateTestTitle(driver, test.description);
await waitToBeFinished(driver, test.timeout || 10000);
await validateResult(driver, test.validationFunc, targetInfo);
});
};
const validateTestIndex = number => driver
.elementById('descriptionLbl')
.text()
.then(text => parseInt(text.match(/(\d+):/)[1], 10))
.should.eventually.become(number, 'Test index is not matching!');
const onlyFlaggedTests = [];
const enabledTests = [];
const validateTestTitle = testTitle => driver
.elementById('descriptionLbl')
.text()
.then(text => text.match(/\d+:\ (.*)/)[1])
.should.eventually.become(testTitle, 'Test description is not matching!');
testDefinitions.tests.forEach(test => {
if (test.only) {
onlyFlaggedTests.push(test);
}
const waitToBeFinished = timeout => new Promise((resolve, reject) => {
const timeoutTimestamp = Date.now() + timeout;
const checkIfFinished = () => driver
.elementById('statusInput')
.getValue()
.then(value => {
if (value === 'finished') {
resolve();
} else if (Date.now() > timeoutTimestamp) {
reject(new Error('Test function timed out!'));
} else {
setTimeout(checkIfFinished, 500);
}
});
checkIfFinished();
if (!test.disabled) {
enabledTests.push(test);
}
});
const validateResult = testDefinition => driver
.safeExecute('app.lastResult')
.then(result => testDefinition.validationFunc(driver, result, targetInfo));
const clickNext = () => driver
.elementById('nextBtn')
.click()
.sleep(1000);
before(() => {
driver = wd.promiseChainRemote(serverConfig);
require('./helpers/logging').configure(driver);
return driver.init(getCaps('TestApp'));
});
after(() => driver
.quit()
.finally(function () {
if (process.env.SAUCE_USERNAME) {
return driver.sauceJobStatus(allPassed);
}
}));
testDefinitions.tests.forEach((definition, index) => {
it(index + ': ' + definition.description, function() {
return clickNext()
.then(() => validateTestIndex(index))
.then(() => validateTestTitle(definition.description))
.then(() => waitToBeFinished(definition.timeout || 10000))
.then(() => validateResult(definition))
});
});
if (onlyFlaggedTests.length) {
onlyFlaggedTests.forEach(defineTestForMocha);
} else {
enabledTests.forEach(defineTestForMocha);
}
});
async function clickNext(driver) {
await driver.elementById('nextBtn').click().sleep(1000);
}
async function validateTestIndex(driver, testIndex) {
const description = await driver.elementById('descriptionLbl').text();
const index = parseInt(description.match(/(\d+):/)[1], 10);
index.should.be.equal(testIndex, 'Test index is not matching!');
}
async function validateTestTitle(driver, testTitle) {
const description = await driver.elementById('descriptionLbl').text();
const title = description.match(/\d+:\ (.*)/)[1];
title.should.be.equal(testTitle, 'Test description is not matching!');
}
async function waitToBeFinished(driver, timeout) {
const timeoutTimestamp = Date.now() + timeout;
while (true) {
if (await driver.elementById('statusInput').getValue() === 'finished') {
return true;
}
if (Date.now() > timeoutTimestamp) {
throw new Error('Test function timed out!');
}
await sleep(500);
}
}
async function validateResult(driver, validationFunc, targetInfo) {
const result = await driver.safeExecute('app.lastResult');
validationFunc(driver, result, targetInfo);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -1,7 +1,14 @@
const chai = require('chai');
const mock = require('mock-require');
const util = require('util');
const should = chai.should();
const BlobMock = require('./mocks/Blob.mock');
const ConsoleMock = require('./mocks/Console.mock');
const FileMock = require('./mocks/File.mock');
const FileReaderMock = require('./mocks/FileReader.mock');
const FormDataMock = require('./mocks/FormData.mock');
describe('Advanced HTTP public interface', function () {
const messages = require('../www/messages');
@@ -17,7 +24,7 @@ describe('Advanced HTTP public interface', function () {
const errorCodes = require('../www/error-codes');
const WebStorageCookieStore = require('../www/local-storage-store')(ToughCookie, lodash);
const cookieHandler = require('../www/cookie-handler')(null, ToughCookie, WebStorageCookieStore);
const helpers = require('../www/helpers')(jsUtil, cookieHandler, messages, errorCodes);
const helpers = require('../www/helpers')(null, jsUtil, cookieHandler, messages, errorCodes);
const urlUtil = require('../www/url-util')(jsUtil);
return { exec: noop, cookieHandler, urlUtil: urlUtil, helpers, globalConfigs, errorCodes };
@@ -29,7 +36,7 @@ describe('Advanced HTTP public interface', function () {
beforeEach(() => {
// mocked btoa function (base 64 encoding strings)
global.btoa = decoded => new Buffer(decoded).toString('base64');
global.btoa = decoded => Buffer.from(decoded).toString('base64');
loadHttp(getDependenciesBlueprint());
});
@@ -265,13 +272,13 @@ describe('Common helpers', function () {
const init = require('../www/helpers');
init.debug = true;
const helpers = init(null, null, null);
const helpers = init(null, null, null, null, null, null);
it('merges empty header sets correctly', () => {
helpers.mergeHeaders({}, {}).should.eql({});
});
it('merges ssimple header sets without collision correctly', () => {
it('merges simple header sets without collision correctly', () => {
helpers.mergeHeaders({ a: 1 }, { b: 2 }).should.eql({ a: 1, b: 2 });
});
@@ -282,13 +289,13 @@ describe('Common helpers', function () {
describe('getCookieHeader(url)', function () {
it('resolves cookie header correctly when no cookie is set #198', () => {
const helpers = require('../www/helpers')(null, { getCookieString: () => '' }, null);
const helpers = require('../www/helpers')(null, null, { getCookieString: () => '' }, null);
helpers.getCookieHeader('http://ilkimen.net').should.eql({});
});
it('resolves cookie header correctly when a cookie is set', () => {
const helpers = require('../www/helpers')(null, { getCookieString: () => 'cookie=value' }, null);
const helpers = require('../www/helpers')(null, null, { getCookieString: () => 'cookie=value' }, null);
helpers.getCookieHeader('http://ilkimen.net').should.eql({ Cookie: 'cookie=value' });
});
@@ -297,7 +304,7 @@ describe('Common helpers', function () {
describe('checkClientAuthOptions()', function () {
const jsUtil = require('../www/js-util');
const messages = require('../www/messages');
const helpers = require('../www/helpers')(jsUtil, null, messages);
const helpers = require('../www/helpers')(null, jsUtil, null, messages);
it('returns options object with empty values when mode is "none" and no options are given', () => {
helpers.checkClientAuthOptions('none').should.eql({
@@ -362,7 +369,7 @@ describe('Common helpers', function () {
describe('handleMissingOptions()', function () {
const jsUtil = require('../www/js-util');
const messages = require('../www/messages');
const helpers = require('../www/helpers')(jsUtil, null, messages);
const helpers = require('../www/helpers')(null, jsUtil, null, messages);
const mockGlobals = {
headers: {},
serializer: 'urlencoded',
@@ -394,7 +401,7 @@ describe('Common helpers', function () {
};
it('does not change response data if it is an ArrayBuffer', () => {
const helpers = require('../www/helpers')(jsUtil, null, messages, null, errorCodes);
const helpers = require('../www/helpers')(null, jsUtil, null, messages, null, errorCodes);
const buffer = new ArrayBuffer(5);
const handler = helpers.injectRawResponseHandler(
'arraybuffer',
@@ -406,7 +413,7 @@ describe('Common helpers', function () {
it('does not change response data if it is a Blob', () => {
const fakeJsUtil = { getTypeOf: () => 'Blob' };
const helpers = require('../www/helpers')(fakeJsUtil, null, messages, null, errorCodes);
const helpers = require('../www/helpers')(null, fakeJsUtil, null, messages, null, errorCodes);
const handler = helpers.injectRawResponseHandler(
'blob',
response => response.data.should.be.equal('fakeData')
@@ -416,7 +423,7 @@ describe('Common helpers', function () {
});
it('does not change response data if response type is "text"', () => {
const helpers = require('../www/helpers')(jsUtil, null, messages, null, errorCodes);
const helpers = require('../www/helpers')(null, jsUtil, null, messages, null, errorCodes);
const example = 'exampleText';
const handler = helpers.injectRawResponseHandler(
'text',
@@ -428,7 +435,7 @@ describe('Common helpers', function () {
it('handles response type "json" correctly', () => {
const fakeData = { myString: 'bla', myNumber: 10 };
const helpers = require('../www/helpers')(jsUtil, null, messages, null, errorCodes);
const helpers = require('../www/helpers')(null, jsUtil, null, messages, null, errorCodes);
const handler = helpers.injectRawResponseHandler(
'json',
response => response.data.should.be.eql(fakeData)
@@ -438,7 +445,7 @@ describe('Common helpers', function () {
});
it('handles response type "arraybuffer" correctly', () => {
const helpers = require('../www/helpers')(jsUtil, null, messages, fakeBase64, errorCodes);
const helpers = require('../www/helpers')(null, jsUtil, null, messages, fakeBase64, errorCodes);
const handler = helpers.injectRawResponseHandler(
'arraybuffer',
response => response.data.should.be.equal('fakeArrayBuffer')
@@ -448,7 +455,7 @@ describe('Common helpers', function () {
});
it('handles response type "blob" correctly', () => {
const helpers = require('../www/helpers')(jsUtil, null, messages, fakeBase64, errorCodes);
const helpers = require('../www/helpers')(null, jsUtil, null, messages, fakeBase64, errorCodes);
const handler = helpers.injectRawResponseHandler(
'blob',
(response) => {
@@ -462,7 +469,7 @@ describe('Common helpers', function () {
});
it('calls failure callback when post-processing fails', () => {
const helpers = require('../www/helpers')(jsUtil, null, messages, fakeBase64, errorCodes);
const helpers = require('../www/helpers')(null, jsUtil, null, messages, fakeBase64, errorCodes);
const handler = helpers.injectRawResponseHandler(
'json',
null,
@@ -476,10 +483,10 @@ describe('Common helpers', function () {
});
});
describe('checkUploadFileOptions()', 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);
const helpers = require('../www/helpers')(null, jsUtil, null, messages, null, null);
it('checks valid file options correctly', () => {
const opts = {
@@ -508,4 +515,232 @@ describe('Common helpers', function () {
(() => helpers.checkUploadFileOptions(['file://path/to/file.png'], [1])).should.throw(messages.NAMES_TYPE_MISMATCH);
});
});
})
describe('processData()', function () {
const mockWindow = {
Blob: BlobMock,
File: FileMock,
FileReader: FileReaderMock,
FormData: FormDataMock,
TextEncoder: util.TextEncoder,
}
const base64 = { fromArrayBuffer: ab => Buffer.from(ab).toString('base64') };
const jsUtil = require('../www/js-util');
const messages = require('../www/messages');
const dependencyValidator = require('../www/dependency-validator')(mockWindow, null, messages);
const helpers = require('../www/helpers')(mockWindow, jsUtil, null, messages, base64, null, dependencyValidator, {});
const testString = 'Test String öäüß 👍😉';
const testStringBase64 = Buffer.from(testString).toString('base64');
it('throws an error when given data does not match allowed data types', () => {
(() => helpers.processData('myString', 'urlencoded')).should.throw(messages.TYPE_MISMATCH_DATA);
(() => helpers.processData('myString', 'json')).should.throw(messages.TYPE_MISMATCH_DATA);
(() => helpers.processData({}, 'utf8')).should.throw(messages.TYPE_MISMATCH_DATA);
});
it('throws an error when given data does not match allowed instance types', () => {
(() => helpers.processData('myString', 'multipart')).should.throw(messages.INSTANCE_TYPE_MISMATCH_DATA);
});
it('processes data correctly when serializer "utf8" is configured', (cb) => {
helpers.processData('myString', 'utf8', (data) => {
data.should.be.eql({text: 'myString'});
cb();
})
});
it('processes data correctly when serializer "multipart" is configured and form data contains string value', (cb) => {
const formData = new FormDataMock();
formData.append('myString', testString);
helpers.processData(formData, 'multipart', (data) => {
data.buffers.length.should.be.equal(1);
data.names.length.should.be.equal(1);
data.fileNames.length.should.be.equal(1);
data.types.length.should.be.equal(1);
data.buffers[0].should.be.eql(testStringBase64);
data.names[0].should.be.equal('myString');
should.equal(data.fileNames[0], null);
data.types[0].should.be.equal('text/plain');
cb();
});
});
it('processes data correctly when serializer "multipart" is configured and form data contains file value', (cb) => {
const formData = new FormDataMock();
formData.append('myFile', new BlobMock([testString], { type: 'application/octet-stream' }));
helpers.processData(formData, 'multipart', (data) => {
data.buffers.length.should.be.equal(1);
data.names.length.should.be.equal(1);
data.fileNames.length.should.be.equal(1);
data.types.length.should.be.equal(1);
data.buffers[0].should.be.eql(testStringBase64);
data.names[0].should.be.equal('myFile');
data.fileNames[0].should.be.equal('blob');
data.types[0].should.be.equal('application/octet-stream');
cb();
});
});
});
});
describe('Dependency Validator', function () {
const messages = require('../www/messages');
describe('logWarnings()', function () {
it('logs a warning message if FormData API is not supported', function () {
const console = new ConsoleMock();
require('../www/dependency-validator')({}, console, messages).logWarnings();
console.messageList.length.should.be.equal(1);
console.messageList[0].type.should.be.equal('warn');
console.messageList[0].message.should.be.eql([messages.MISSING_FORMDATA_API]);
});
it('logs a warning message if FormData.entries() API is not supported', function () {
const console = new ConsoleMock();
require('../www/dependency-validator')({ FormData: {} }, console, messages).logWarnings();
console.messageList.length.should.be.equal(1);
console.messageList[0].type.should.be.equal('warn');
console.messageList[0].message.should.be.eql([messages.MISSING_FORMDATA_ENTRIES_API]);
});
});
describe('checkBlobApi()', function () {
it('throws an error if Blob API is not supported', function () {
const console = new ConsoleMock();
const validator = require('../www/dependency-validator')({}, console, messages);
(() => validator.checkBlobApi()).should.throw(messages.MISSING_BLOB_API);
});
});
describe('checkFileReaderApi()', function () {
it('throws an error if FileReader API is not supported', function () {
const console = new ConsoleMock();
const validator = require('../www/dependency-validator')({}, console, messages);
(() => validator.checkFileReaderApi()).should.throw(messages.MISSING_FILE_READER_API);
});
});
describe('checkFormDataInstance()', function () {
it('throws an error if FormData.entries() is not supported on given instance', function () {
const console = new ConsoleMock();
const validator = require('../www/dependency-validator')({ FormData: {}}, console, messages);
(() => validator.checkFormDataInstance({})).should.throw(messages.MISSING_FORMDATA_ENTRIES_API);
});
});
describe('checkTextEncoderApi()', function () {
it('throws an error if TextEncoder API is not supported', function () {
const console = new ConsoleMock();
const validator = require('../www/dependency-validator')({}, console, messages);
(() => validator.checkTextEncoderApi()).should.throw(messages.MISSING_TEXT_ENCODER_API);
});
});
});
describe('Ponyfills', function () {
const mockWindow = {
Blob: BlobMock,
File: FileMock,
};
const init = require('../www/ponyfills');
init.debug = true;
const ponyfills = init(mockWindow);
describe('Iterator', function () {
it('exposes interface correctly', () => {
const iterator = new ponyfills.Iterator([]);
iterator.next.should.be.a('function');
});
describe('next()', function () {
it('returns iteration object correctly when list is empty', () => {
const iterator = new ponyfills.Iterator([]);
iterator.next().should.be.eql({ done: true, value: undefined });
});
it('returns iteration object correctly when end posititon of list is not reached yet', () => {
const iterator = new ponyfills.Iterator([['first', 'this is the first item']]);
iterator.next().should.be.eql({ done: false, value: ['first', 'this is the first item'] });
});
it('returns iteration object correctly when end posititon of list is already reached', () => {
const iterator = new ponyfills.Iterator([['first', 'this is the first item']]);
iterator.next();
iterator.next().should.be.eql({ done: true, value: undefined });
});
});
});
describe('FormData', function () {
it('exposes interface correctly', () => {
const formData = new ponyfills.FormData();
formData.append.should.be.a('function');
formData.entries.should.be.a('function');
});
describe('append()', function () {
it('appends string value correctly', () => {
const formData = new ponyfills.FormData();
formData.append('test', 'myTestString');
formData.__items[0].should.be.eql(['test', 'myTestString']);
});
it('appends numeric value correctly', () => {
const formData = new ponyfills.FormData();
formData.append('test', 10);
formData.__items[0].should.be.eql(['test', '10']);
formData.__items[0][1].should.be.a('string');
});
it('appends Blob value correctly', () => {
const formData = new ponyfills.FormData();
const blob = new BlobMock(['another test'], { type: 'text/plain' });
formData.append('myBlob', blob, 'myFileName.txt');
formData.__items[0].should.be.eql(['myBlob', blob]);
formData.__items[0][1].name.should.be.equal('myFileName.txt');
formData.__items[0][1].lastModifiedDate.should.be.a('Date');
});
it('appends File value correctly', () => {
const formData = new ponyfills.FormData();
const blob = new BlobMock(['another test'], { type: 'text/plain' });
const file = new FileMock(blob, 'myFileName.txt');
formData.append('myFile', file, 'myOverriddenFileName.txt');
formData.__items[0].should.be.eql(['myFile', file]);
formData.__items[0][1].name.should.be.equal('myFileName.txt');
formData.__items[0][1].lastModifiedDate.should.be.eql(file.lastModifiedDate);
});
});
describe('entries()', function () {
it('returns an iterator correctly', () => {
const formData = new ponyfills.FormData();
formData.entries().should.be.an.instanceof(ponyfills.Iterator);
})
});
});
});

35
test/mocks/Blob.mock.js Normal file
View File

@@ -0,0 +1,35 @@
module.exports = class BlobMock {
constructor(blobParts, options) {
if (blobParts instanceof BlobMock) {
this._buffer = blobParts._buffer;
} else {
this._buffer = new Uint8Array(Buffer.concat(blobParts.map(part => Buffer.from(part, 'utf8')))).buffer;
}
this._type = options.type || '';
}
get size() {
return this._buffer.length;
}
get type() {
return this._type;
}
arrayBuffer() {
throw new Error('Not implemented in BlobMock.');
}
slice() {
throw new Error('Not implemented in BlobMock.');
}
stream() {
throw new Error('Not implemented in BlobMock.');
}
text() {
throw new Error('Not implemented in BlobMock.');
}
}

View File

@@ -0,0 +1,11 @@
module.exports = class ConsoleMock {
constructor() {
this.messageList = [];
}
debug(...message) { this.messageList.push({ type: 'debug', message }); }
error(...message) { this.messageList.push({ type: 'error', message }); }
log(...message) { this.messageList.push({ type: 'log', message }); }
info(...message) { this.messageList.push({ type: 'info', message }); }
warn(...message) { this.messageList.push({ type: 'warn', message }); }
}

17
test/mocks/File.mock.js Normal file
View File

@@ -0,0 +1,17 @@
const BlobMock = require('./Blob.mock');
module.exports = class FileMock extends BlobMock {
constructor(blob, fileName) {
super(blob, { type: blob.type });
this._fileName = fileName || '';
this.__lastModifiedDate = new Date();
}
get name() {
return this._fileName;
}
get lastModifiedDate() {
return this.__lastModifiedDate;
}
}

View File

@@ -0,0 +1,39 @@
module.exports = class FileReaderMock {
constructor() {
this.EMPTY = 0;
this.LOADING = 1;
this.DONE = 2;
this.error = null;
this.onabort = () => {};
this.onerror = () => {};
this.onload = () => {};
this.onloadend = () => {};
this.onloadstart = () => {};
this.onprogress = () => {};
this.readyState = this.EMPTY;
this.result = null;
}
readAsArrayBuffer(file) {
this.readyState = this.LOADING;
this.onloadstart();
this.onprogress();
this.result = file._buffer;
this.readyState = this.DONE;
this.onloadend();
this.onload();
}
readAsBinaryString() {
throw new Error('Not implemented in FileReaderMock.');
}
readAsDataUrl() {
throw new Error('Not implemented in FileReaderMock.');
}
readAsText() {
throw new Error('Not implemented in FileReaderMock.');
}
}

View File

@@ -0,0 +1,52 @@
const BlobMock = require('./Blob.mock');
const FileMock = require('./File.mock');
module.exports = class FormDataMock {
constructor() {
this.map = new Map();
}
append(name, value, filename) {
if (value instanceof BlobMock) {
this.map.set(name, new FileMock(value, filename))
} else {
this.map.set(name, value);
}
}
delete() {
throw new Error('Not implemented in FormDataMock.');
}
entries() {
return this.map.entries();
}
forEach(cb) {
return this.map.forEach(cb);
}
get(key) {
return this.map.get(key);
}
getAll() {
throw new Error('Not implemented in FormDataMock.');
}
has(key) {
return this.map.has(key);
}
keys() {
return this.map.keys();
}
set(key, value) {
return this.map.set(key, value);
}
values() {
return this.map.values();
}
};

View File

@@ -14,8 +14,12 @@ 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')(jsUtil, cookieHandler, messages, base64, errorCodes);
var dependencyValidator = require(pluginId + '.dependency-validator')(window, window.console, messages);
var ponyfills = require(pluginId + '.ponyfills')(window);
var helpers = require(pluginId + '.helpers')(window, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator, ponyfills);
var urlUtil = require(pluginId + '.url-util')(jsUtil);
var publicInterface = require(pluginId + '.public-interface')(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes);
var publicInterface = require(pluginId + '.public-interface')(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes, ponyfills);
dependencyValidator.logWarnings();
module.exports = publicInterface;

View File

@@ -0,0 +1,43 @@
module.exports = function init(global, console, messages) {
var interface = {
checkBlobApi: checkBlobApi,
checkFileReaderApi: checkFileReaderApi,
checkFormDataInstance: checkFormDataInstance,
checkTextEncoderApi: checkTextEncoderApi,
logWarnings: logWarnings,
};
return interface;
function logWarnings() {
if (!global.FormData) {
console.warn(messages.MISSING_FORMDATA_API);
} else if (!global.FormData.prototype || !global.FormData.prototype.entries) {
console.warn(messages.MISSING_FORMDATA_ENTRIES_API);
}
}
function checkBlobApi() {
if (!global.Blob || !global.Blob.prototype) {
throw new Error(messages.MISSING_BLOB_API);
}
}
function checkFileReaderApi() {
if (!global.FileReader || !global.FileReader.prototype) {
throw new Error(messages.MISSING_FILE_READER_API);
}
}
function checkFormDataInstance(instance) {
if (!instance || !instance.entries) {
throw new Error(messages.MISSING_FORMDATA_ENTRIES_API);
}
}
function checkTextEncoderApi() {
if (!global.TextEncoder || !global.TextEncoder.prototype) {
throw new Error(messages.MISSING_TEXT_ENCODER_API);
}
}
};

View File

@@ -1,5 +1,5 @@
module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCodes) {
var validSerializers = ['urlencoded', 'json', 'utf8'];
module.exports = function init(global, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator, ponyfills) {
var validSerializers = ['urlencoded', 'json', 'utf8', 'multipart'];
var validCertModes = ['default', 'nocheck', 'pinned', 'legacy'];
var validClientAuthModes = ['none', 'systemstore', 'buffer'];
var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download'];
@@ -18,7 +18,7 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod
checkTimeoutValue: checkTimeoutValue,
checkUploadFileOptions: checkUploadFileOptions,
getMergedHeaders: getMergedHeaders,
getProcessedData: getProcessedData,
processData: processData,
handleMissingCallbacks: handleMissingCallbacks,
handleMissingOptions: handleMissingOptions,
injectCookieHandler: injectCookieHandler,
@@ -270,7 +270,7 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod
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.filesystem = new FileSystem(rawEntry.filesystemName || (rawEntry.filesystem == global.PERSISTENT ? 'persistent' : 'temporary'));
entry.nativeURL = rawEntry.nativeURL;
return entry;
@@ -363,24 +363,101 @@ module.exports = function init(jsUtil, cookieHandler, messages, base64, errorCod
return ['String'];
case 'urlencoded':
return ['Object'];
default:
case 'json':
return ['Array', 'Object'];
default:
return [];
}
}
function getProcessedData(data, dataSerializer) {
function getAllowedInstanceTypes(dataSerializer) {
return dataSerializer === 'multipart' ? ['FormData'] : null;
}
function processData(data, dataSerializer, cb) {
var currentDataType = jsUtil.getTypeOf(data);
var allowedDataTypes = getAllowedDataTypes(dataSerializer);
var allowedInstanceTypes = getAllowedInstanceTypes(dataSerializer);
if (allowedDataTypes.indexOf(currentDataType) === -1) {
if (allowedInstanceTypes) {
var isCorrectInstanceType = false;
allowedInstanceTypes.forEach(function(type) {
if ((global[type] && data instanceof global[type]) || (ponyfills[type] && data instanceof ponyfills[type])) {
isCorrectInstanceType = true;
}
});
if (!isCorrectInstanceType) {
throw new Error(messages.INSTANCE_TYPE_MISMATCH_DATA + ' ' + allowedInstanceTypes.join(', '));
}
}
if (!allowedInstanceTypes && allowedDataTypes.indexOf(currentDataType) === -1) {
throw new Error(messages.TYPE_MISMATCH_DATA + ' ' + allowedDataTypes.join(', '));
}
if (dataSerializer === 'utf8') {
data = { text: data };
switch (dataSerializer) {
case 'utf8':
return cb({ text: data });
case 'multipart':
return processFormData(data, cb);
default:
return cb(data);
}
}
function processFormData(data, cb) {
dependencyValidator.checkBlobApi();
dependencyValidator.checkFileReaderApi();
dependencyValidator.checkTextEncoderApi();
dependencyValidator.checkFormDataInstance(data);
var textEncoder = new global.TextEncoder('utf8');
var iterator = data.entries();
var result = {
buffers: [],
names: [],
fileNames: [],
types: []
};
processFormDataIterator(iterator, textEncoder, result, cb);
}
function processFormDataIterator(iterator, textEncoder, result, onFinished) {
var entry = iterator.next();
if (entry.done) {
return onFinished(result);
}
return data;
if (entry.value[1] instanceof global.Blob || entry.value[1] instanceof global.File) {
var reader = new global.FileReader();
reader.onload = function() {
result.buffers.push(base64.fromArrayBuffer(reader.result));
result.names.push(entry.value[0]);
result.fileNames.push(entry.value[1].name || 'blob');
result.types.push(entry.value[1].type || '');
processFormDataIterator(iterator, textEncoder, result, onFinished);
};
return reader.readAsArrayBuffer(entry.value[1]);
}
if (jsUtil.getTypeOf(entry.value[1]) === 'String') {
result.buffers.push(base64.fromArrayBuffer(textEncoder.encode(entry.value[1]).buffer));
result.names.push(entry.value[0]);
result.fileNames.push(null);
result.types.push('text/plain');
return processFormDataIterator(iterator, textEncoder, result, onFinished)
}
// skip items which are not supported
processFormDataIterator(iterator, textEncoder, result, onFinished);
}
function handleMissingCallbacks(successFn, failFn) {

View File

@@ -2,6 +2,7 @@ module.exports = {
ADDING_COOKIES_NOT_SUPPORTED: 'advanced-http: "setHeader" does not support adding cookies, please use "setCookie" function instead',
EMPTY_FILE_PATHS: 'advanced-http: "filePaths" option array must not be empty, <filePaths: string[]>',
EMPTY_NAMES: 'advanced-http: "names" option array must not be empty, <names: string[]>',
INSTANCE_TYPE_MISMATCH_DATA: 'advanced-http: "data" option is configured to support only following instance types:',
INVALID_CLIENT_AUTH_ALIAS: 'advanced-http: invalid client certificate alias, needs to be a string or undefined, <alias: string | undefined>',
INVALID_CLIENT_AUTH_MODE: 'advanced-http: invalid client certificate authentication mode, supported modes are:',
INVALID_CLIENT_AUTH_OPTIONS: 'advanced-http: invalid client certificate authentication options, needs to be an dictionary style object',
@@ -17,8 +18,13 @@ module.exports = {
INVALID_TIMEOUT_VALUE: 'advanced-http: invalid timeout value, needs to be a positive numeric value, <timeout: number>',
MANDATORY_FAIL: 'advanced-http: missing mandatory "onFail" callback function',
MANDATORY_SUCCESS: 'advanced-http: missing mandatory "onSuccess" callback function',
MISSING_BLOB_API: 'advanced-http: Blob API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_FILE_READER_API: 'advanced-http: FileReader API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_FORMDATA_API: 'advanced-http: FormData API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_FORMDATA_ENTRIES_API: 'advanced-http: Given instance of FormData does not implement FormData API specification correctly, FormData.entries() is missing. If you want to use "multipart/form-data" requests, you can use an included ponyfill. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
MISSING_TEXT_ENCODER_API: 'advanced-http: TextEncoder API is not supported in this webview. If you want to use "multipart/form-data" requests, you need to load a polyfill library before loading this plugin. Check out https://github.com/silkimen/cordova-plugin-advanced-http/wiki/Web-APIs-required-for-Multipart-requests for more info.',
POST_PROCESSING_FAILED: 'advanced-http: an error occured during post processing response:',
TYPE_MISMATCH_DATA: 'advanced-http: "data" option supports only following data types:',
TYPE_MISMATCH_DATA: 'advanced-http: "data" option is configured to support only following data types:',
TYPE_MISMATCH_FILE_PATHS: 'advanced-http: "filePaths" option needs to be an string array, <filePaths: string[]>',
TYPE_MISMATCH_HEADERS: 'advanced-http: "headers" option needs to be an dictionary style object with string values, <headers: {[key: string]: string}>',
TYPE_MISMATCH_NAMES: 'advanced-http: "names" option needs to be an string array, <names: string[]>',

47
www/ponyfills.js Normal file
View File

@@ -0,0 +1,47 @@
module.exports = function init(global) {
var interface = { FormData: FormData };
// expose all constructor functions for testing purposes
if (init.debug) {
interface.Iterator = Iterator;
}
function FormData() {
this.__items = [];
}
FormData.prototype.append = function(name, value, filename) {
if (global.File && value instanceof global.File) {
// nothing to do
} else if (global.Blob && value instanceof global.Blob) {
// mimic File instance by adding missing properties
value.lastModifiedDate = new Date();
value.name = filename || '';
} else {
value = value.toString ? value.toString() : value;
}
this.__items.push([ name, value ]);
};
FormData.prototype.entries = function() {
return new Iterator(this.__items);
};
function Iterator(items) {
this.__items = items;
this.__position = -1;
}
Iterator.prototype.next = function() {
this.__position += 1;
if (this.__position < this.__items.length) {
return { done: false, value: this.__items[this.__position] };
}
return { done: true, value: undefined };
}
return interface;
};

View File

@@ -1,4 +1,4 @@
module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes) {
module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConfigs, errorCodes, ponyfills) {
var publicInterface = {
getBasicAuthHeader: getBasicAuthHeader,
useBasicAuth: useBasicAuth,
@@ -29,7 +29,8 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
head: head,
uploadFile: uploadFile,
downloadFile: downloadFile,
ErrorCode: errorCodes
ErrorCode: errorCodes,
ponyfills: ponyfills
};
function getBasicAuthHeader(username, password) {
@@ -152,8 +153,9 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
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, options.followRedirect, options.responseType]);
return helpers.processData(options.data, options.serializer, function(data) {
exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout, options.followRedirect, options.responseType]);
});
case 'upload':
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]);