>();
+
try {
KeyStore store = KeyStore.getInstance("AndroidCAStore");
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
@@ -73,6 +82,8 @@ public class CordovaHttpPlugin extends CordovaPlugin {
return this.setServerTrustMode(args, callbackContext);
} else if ("setClientAuthMode".equals(action)) {
return this.setClientAuthMode(args, callbackContext);
+ } else if ("abort".equals(action)) {
+ return this.abort(args, callbackContext);
} else {
return false;
}
@@ -87,10 +98,13 @@ public class CordovaHttpPlugin extends CordovaPlugin {
boolean followRedirect = args.getBoolean(3);
String responseType = args.getString(4);
- CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect,
- responseType, this.tlsConfiguration, callbackContext);
+ Integer reqId = args.getInt(5);
+ CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId);
- cordova.getThreadPool().execute(request);
+ CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect,
+ responseType, this.tlsConfiguration, observableCallbackContext);
+
+ startRequest(reqId, observableCallbackContext, request);
return true;
}
@@ -106,10 +120,13 @@ public class CordovaHttpPlugin extends CordovaPlugin {
boolean followRedirect = args.getBoolean(5);
String responseType = args.getString(6);
- CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers,
- timeout, followRedirect, responseType, this.tlsConfiguration, callbackContext);
+ Integer reqId = args.getInt(7);
+ CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId);
- cordova.getThreadPool().execute(request);
+ CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers,
+ timeout, followRedirect, responseType, this.tlsConfiguration, observableCallbackContext);
+
+ startRequest(reqId, observableCallbackContext, request);
return true;
}
@@ -123,10 +140,13 @@ public class CordovaHttpPlugin extends CordovaPlugin {
boolean followRedirect = args.getBoolean(5);
String responseType = args.getString(6);
- CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect,
- responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), callbackContext);
+ Integer reqId = args.getInt(7);
+ CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId);
- cordova.getThreadPool().execute(upload);
+ CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect,
+ responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), observableCallbackContext);
+
+ startRequest(reqId, observableCallbackContext, upload);
return true;
}
@@ -138,14 +158,25 @@ public class CordovaHttpPlugin extends CordovaPlugin {
int timeout = args.getInt(3) * 1000;
boolean followRedirect = args.getBoolean(4);
- CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect,
- this.tlsConfiguration, callbackContext);
+ Integer reqId = args.getInt(5);
+ CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId);
- cordova.getThreadPool().execute(download);
+ CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect,
+ this.tlsConfiguration, observableCallbackContext);
+
+ startRequest(reqId, observableCallbackContext, download);
return true;
}
+ private void startRequest(Integer reqId, CordovaObservableCallbackContext observableCallbackContext, CordovaHttpBase request) {
+ synchronized (reqMapLock) {
+ observableCallbackContext.setObserver(this);
+ Future> task = cordova.getThreadPool().submit(request);
+ this.addReq(reqId, task, observableCallbackContext);
+ }
+ }
+
private boolean setServerTrustMode(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
CordovaServerTrust runnable = new CordovaServerTrust(args.getString(0), this.cordova.getActivity(),
this.tlsConfiguration, callbackContext);
@@ -166,4 +197,44 @@ public class CordovaHttpPlugin extends CordovaPlugin {
return true;
}
+
+ private boolean abort(final JSONArray args, final CallbackContext callbackContext) throws JSONException {
+
+ int reqId = args.getInt(0);
+ boolean result = false;
+ // NOTE no synchronized (reqMapLock), since even if the req was already removed from reqMap,
+ // the worst that would happen calling task.cancel(true) is a result of false
+ // (i.e. same result as locking & not finding the req in reqMap)
+ Future> task = this.reqMap.get(reqId);
+ if (task != null && !task.isDone()) {
+ result = task.cancel(true);
+ }
+ callbackContext.success(new JSONObject().put("aborted", result));
+
+ return true;
+ }
+
+ private void addReq(final Integer reqId, final Future> task, final CordovaObservableCallbackContext observableCallbackContext) {
+ synchronized (reqMapLock) {
+ if(!task.isDone()){
+ this.reqMap.put(reqId, task);
+ }
+ }
+ }
+
+ private void removeReq(final Integer reqId) {
+ synchronized (reqMapLock) {
+ this.reqMap.remove(reqId);
+ }
+ }
+
+ @Override
+ public void update(Observable o, Object arg) {
+ synchronized (reqMapLock) {
+ CordovaObservableCallbackContext c = (CordovaObservableCallbackContext) arg;
+ if (c.getCallbackContext().isFinished()) {
+ removeReq(c.getRequestId());
+ }
+ }
+ }
}
diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java
index dcbcd06..a62a98a 100644
--- a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java
+++ b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java
@@ -17,7 +17,6 @@ import java.net.URI;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
-import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -28,7 +27,7 @@ class CordovaHttpUpload extends CordovaHttpBase {
public CordovaHttpUpload(String url, JSONObject headers, JSONArray filePaths, JSONArray uploadNames, int timeout,
boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration,
- Context applicationContext, CallbackContext callbackContext) {
+ Context applicationContext, CordovaObservableCallbackContext callbackContext) {
super("POST", url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext);
this.filePaths = filePaths;
diff --git a/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java b/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java
new file mode 100644
index 0000000..cff0583
--- /dev/null
+++ b/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java
@@ -0,0 +1,58 @@
+package com.silkimen.cordovahttp;
+
+import org.apache.cordova.CallbackContext;
+import org.json.JSONObject;
+
+import java.util.Observer;
+
+public class CordovaObservableCallbackContext {
+
+ private CallbackContext callbackContext;
+ private Integer requestId;
+ private Observer observer;
+
+ public CordovaObservableCallbackContext(CallbackContext callbackContext, Integer requestId) {
+ this.callbackContext = callbackContext;
+ this.requestId = requestId;
+ }
+
+ public void success(JSONObject message) {
+ this.callbackContext.success(message);
+ this.notifyObserver();
+ }
+
+ public void error(JSONObject message) {
+ this.callbackContext.error(message);
+ this.notifyObserver();
+ }
+
+ public Integer getRequestId() {
+ return this.requestId;
+ }
+
+ public CallbackContext getCallbackContext() {
+ return callbackContext;
+ }
+
+ public Observer getObserver() {
+ return observer;
+ }
+
+ protected void notifyObserver() {
+ if(this.observer != null){
+ this.observer.update(null, this);
+ }
+ }
+
+ /**
+ * Set an observer that is notified, when {@link #success(JSONObject)}
+ * or {@link #error(JSONObject)} are called.
+ *
+ * NOTE the observer is notified with
+ * observer.update(null, cordovaObservableCallbackContext)
+ * @param observer
+ */
+ public void setObserver(Observer observer) {
+ this.observer = observer;
+ }
+}
diff --git a/src/browser/cordova-http-plugin.js b/src/browser/cordova-http-plugin.js
index a18eb8a..5a28525 100644
--- a/src/browser/cordova-http-plugin.js
+++ b/src/browser/cordova-http-plugin.js
@@ -3,6 +3,8 @@ var pluginId = module.id.slice(0, module.id.lastIndexOf('.'));
var cordovaProxy = require('cordova/exec/proxy');
var jsUtil = require(pluginId + '.js-util');
+var reqMap = {};
+
function serializeJsonData(data) {
try {
return JSON.stringify(data);
@@ -115,6 +117,13 @@ function createXhrFailureObject(xhr) {
return obj;
}
+function injectRequestIdHandler(reqId, cb) {
+ return function (response) {
+ delete reqMap[reqId];
+ cb(response);
+ }
+}
+
function getHeaderValue(headers, headerName) {
let result = null;
@@ -142,7 +151,7 @@ function setHeaders(xhr, headers) {
}
function sendRequest(method, withData, opts, success, failure) {
- var data, serializer, headers, timeout, followRedirect, responseType;
+ var data, serializer, headers, timeout, followRedirect, responseType, reqId;
var url = opts[0];
if (withData) {
@@ -152,25 +161,31 @@ function sendRequest(method, withData, opts, success, failure) {
timeout = opts[4];
followRedirect = opts[5];
responseType = opts[6];
+ reqId = opts[7];
} else {
headers = opts[1];
timeout = opts[2];
followRedirect = opts[3];
responseType = opts[4];
-
+ reqId = opts[5];
}
+ var onSuccess = injectRequestIdHandler(reqId, success);
+ var onFail = injectRequestIdHandler(reqId, failure);
+
var processedData = null;
var xhr = new XMLHttpRequest();
+ reqMap[reqId] = xhr;
+
xhr.open(method, url);
if (headers.Cookie && headers.Cookie.length > 0) {
- return failure('advanced-http: custom cookies not supported on browser platform');
+ return onFail('advanced-http: custom cookies not supported on browser platform');
}
if (!followRedirect) {
- return failure('advanced-http: disabling follow redirect not supported on browser platform');
+ return onFail('advanced-http: disabling follow redirect not supported on browser platform');
}
switch (serializer) {
@@ -179,7 +194,7 @@ function sendRequest(method, withData, opts, success, failure) {
processedData = serializeJsonData(data);
if (processedData === null) {
- return failure('advanced-http: failed serializing data');
+ return onFail('advanced-http: failed serializing data');
}
break;
@@ -218,11 +233,20 @@ function sendRequest(method, withData, opts, success, failure) {
setHeaders(xhr, headers);
xhr.onerror = function () {
- return failure(createXhrFailureObject(xhr));
+ return onFail(createXhrFailureObject(xhr));
+ };
+
+ xhr.onabort = function () {
+ return onFail({
+ status: -8,
+ error: 'Request was aborted',
+ url: url,
+ headers: {}
+ });
};
xhr.ontimeout = function () {
- return failure({
+ return onFail({
status: -4,
error: 'Request timed out',
url: url,
@@ -234,15 +258,28 @@ function sendRequest(method, withData, opts, success, failure) {
if (xhr.readyState !== xhr.DONE) return;
if (xhr.status < 200 || xhr.status > 299) {
- return failure(createXhrFailureObject(xhr));
+ return onFail(createXhrFailureObject(xhr));
}
- return success(createXhrSuccessObject(xhr));
+ return onSuccess(createXhrSuccessObject(xhr));
};
xhr.send(processedData);
}
+function abort(opts, success, failure) {
+ var reqId = opts[0];
+ var result = false;
+
+ var xhr = reqMap[reqId];
+ if(xhr && xhr.readyState !== xhr.DONE){
+ xhr.abort();
+ result = true;
+ }
+
+ success({aborted: result});
+}
+
var browserInterface = {
get: function (success, failure, opts) {
return sendRequest('get', false, opts, success, failure);
@@ -262,6 +299,9 @@ var browserInterface = {
patch: function (success, failure, opts) {
return sendRequest('patch', true, opts, success, failure);
},
+ abort: function (success, failure, opts) {
+ return abort(opts, success, failure);
+ },
uploadFile: function (success, failure, opts) {
return failure('advanced-http: function "uploadFile" not supported on browser platform');
},
diff --git a/src/ios/CordovaHttpPlugin.h b/src/ios/CordovaHttpPlugin.h
index 0e5d867..cce86b4 100644
--- a/src/ios/CordovaHttpPlugin.h
+++ b/src/ios/CordovaHttpPlugin.h
@@ -15,5 +15,6 @@
- (void)options:(CDVInvokedUrlCommand*)command;
- (void)uploadFiles:(CDVInvokedUrlCommand*)command;
- (void)downloadFile:(CDVInvokedUrlCommand*)command;
+- (void)abort:(CDVInvokedUrlCommand*)command;
@end
diff --git a/src/ios/CordovaHttpPlugin.m b/src/ios/CordovaHttpPlugin.m
index d7f8fcc..482fdcf 100644
--- a/src/ios/CordovaHttpPlugin.m
+++ b/src/ios/CordovaHttpPlugin.m
@@ -9,6 +9,8 @@
@interface CordovaHttpPlugin()
+- (void)addRequest:(NSNumber*)reqId forTask:(NSURLSessionDataTask*)task;
+- (void)removeRequest:(NSNumber*)reqId;
- (void)setRequestHeaders:(NSDictionary*)headers forManager:(AFHTTPSessionManager*)manager;
- (void)handleSuccess:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response andData:(id)data;
- (void)handleError:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response error:(NSError*)error;
@@ -22,10 +24,20 @@
@implementation CordovaHttpPlugin {
AFSecurityPolicy *securityPolicy;
NSURLCredential *x509Credential;
+ NSMutableDictionary *reqDict;
}
- (void)pluginInitialize {
securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
+ reqDict = [NSMutableDictionary dictionary];
+}
+
+- (void)addRequest:(NSNumber*)reqId forTask:(NSURLSessionDataTask*)task {
+ [reqDict setObject:task forKey:reqId];
+}
+
+- (void)removeRequest:(NSNumber*)reqId {
+ [reqDict removeObjectForKey:reqId];
}
- (void)setRequestSerializer:(NSString*)serializerName forManager:(AFHTTPSessionManager*)manager {
@@ -52,7 +64,7 @@
if (![self->securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
return NSURLSessionAuthChallengeRejectProtectionSpace;
}
-
+
if (credential) {
return NSURLSessionAuthChallengeUseCredential;
}
@@ -62,7 +74,7 @@
*credential = self->x509Credential;
return NSURLSessionAuthChallengeUseCredential;
}
-
+
return NSURLSessionAuthChallengePerformDefaultHandling;
}];
}
@@ -111,14 +123,21 @@
}
- (void)handleError:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response error:(NSError*)error {
+ bool aborted = error.code == NSURLErrorCancelled;
+ if(aborted){
+ [dictionary setObject:[NSNumber numberWithInt:-8] forKey:@"status"];
+ [dictionary setObject:@"Request was aborted" forKey:@"error"];
+ }
if (response != nil) {
[dictionary setValue:response.URL.absoluteString forKey:@"url"];
- [dictionary setObject:[NSNumber numberWithInt:(int)response.statusCode] forKey:@"status"];
[dictionary setObject:[self copyHeaderFields:response.allHeaderFields] forKey:@"headers"];
- if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey]) {
- [dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] forKey:@"error"];
+ if(!aborted){
+ [dictionary setObject:[NSNumber numberWithInt:(int)response.statusCode] forKey:@"status"];
+ if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey]) {
+ [dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] forKey:@"error"];
+ }
}
- } else {
+ } else if(!aborted) {
[dictionary setObject:[self getStatusCode:error] forKey:@"status"];
[dictionary setObject:[error localizedDescription] forKey:@"error"];
}
@@ -181,6 +200,7 @@
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:2] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:3] boolValue];
NSString *responseType = [command.arguments objectAtIndex:4];
+ NSNumber *reqId = [command.arguments objectAtIndex:5];
[self setRequestSerializer: @"default" forManager: manager];
[self setupAuthChallengeBlock: manager];
@@ -194,30 +214,35 @@
@try {
void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) {
+ [weakSelf removeRequest:reqId];
+
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) {
+ [weakSelf removeRequest:reqId];
+
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];
+
+ NSURLSessionDataTask *task = [manager downloadTaskWithHTTPMethod:method URLString:url parameters:nil progress:nil success:onSuccess failure:onFailure];
+ [self addRequest:reqId forTask:task];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
@@ -235,6 +260,7 @@
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:5] boolValue];
NSString *responseType = [command.arguments objectAtIndex:6];
+ NSNumber *reqId = [command.arguments objectAtIndex:7];
[self setRequestSerializer: serializerName forManager: manager];
[self setupAuthChallengeBlock: manager];
@@ -245,30 +271,32 @@
CordovaHttpPlugin* __weak weakSelf = self;
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
-
+
@try {
void (^constructBody)(id) = ^(id 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) {
+ [weakSelf removeRequest:reqId];
+
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[dictionary setObject:[NSNumber numberWithInt:400] forKey:@"status"];
[dictionary setObject:@"Could not add part to multipart request body." forKey:@"error"];
@@ -278,30 +306,36 @@
return;
}
};
-
+
void (^onSuccess)(NSURLSessionTask *, id) = ^(NSURLSessionTask *task, id responseObject) {
+ [weakSelf removeRequest:reqId];
+
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) {
+ [weakSelf removeRequest:reqId];
+
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];
};
-
+
+ NSURLSessionDataTask *task;
if ([serializerName isEqualToString:@"multipart"]) {
- [manager uploadTaskWithHTTPMethod:method URLString:url parameters:nil constructingBodyWithBlock:constructBody progress:nil success:onSuccess failure:onFailure];
+ task = [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];
+ task = [manager uploadTaskWithHTTPMethod:method URLString:url parameters:data progress:nil success:onSuccess failure:onFailure];
}
+ [self addRequest:reqId forTask:task];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
@@ -333,27 +367,27 @@
- (void)setClientAuthMode:(CDVInvokedUrlCommand*)command {
CDVPluginResult* pluginResult;
NSString *mode = [command.arguments objectAtIndex:0];
-
+
if ([mode isEqualToString:@"none"]) {
x509Credential = nil;
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
}
-
+
if ([mode isEqualToString:@"systemstore"]) {
NSString *alias = [command.arguments objectAtIndex:1];
-
+
// TODO
-
+
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"mode 'systemstore' is not supported on iOS"];
}
-
+
if ([mode isEqualToString:@"buffer"]) {
CFDataRef container = (__bridge CFDataRef) [command.arguments objectAtIndex:2];
CFStringRef password = (__bridge CFStringRef) [command.arguments objectAtIndex:3];
-
+
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
-
+
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
CFArrayRef items;
OSStatus securityError = SecPKCS12Import(container, options, &items);
@@ -367,7 +401,7 @@
self->x509Credential = [NSURLCredential credentialWithIdentity:identity certificates: nil persistence:NSURLCredentialPersistenceForSession];
CFRelease(items);
-
+
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
}
}
@@ -413,6 +447,7 @@
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:5] boolValue];
NSString *responseType = [command.arguments objectAtIndex:6];
+ NSNumber *reqId = [command.arguments objectAtIndex:7];
[self setRequestHeaders: headers forManager: manager];
[self setupAuthChallengeBlock: manager];
@@ -424,7 +459,7 @@
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
- [manager POST:url parameters:nil constructingBodyWithBlock:^(id formData) {
+ NSURLSessionDataTask *task = [manager POST:url parameters:nil constructingBodyWithBlock:^(id formData) {
NSError *error;
for (int i = 0; i < [filePaths count]; i++) {
NSString *filePath = (NSString *) [filePaths objectAtIndex:i];
@@ -433,6 +468,8 @@
[formData appendPartWithFileURL:fileURL name:uploadName error:&error];
}
if (error) {
+ [weakSelf removeRequest:reqId];
+
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[dictionary setObject:[NSNumber numberWithInt:500] forKey:@"status"];
[dictionary setObject:@"Could not add file to post body." forKey:@"error"];
@@ -442,6 +479,8 @@
return;
}
} progress:nil success:^(NSURLSessionTask *task, id responseObject) {
+ [weakSelf removeRequest:reqId];
+
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleSuccess:dictionary withResponse:(NSHTTPURLResponse*)task.response andData:responseObject];
@@ -449,6 +488,8 @@
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
+ [weakSelf removeRequest:reqId];
+
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
@@ -456,6 +497,7 @@
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
+ [self addRequest:reqId forTask:task];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
@@ -472,6 +514,7 @@
NSString *filePath = [command.arguments objectAtIndex: 2];
NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:3] doubleValue];
bool followRedirect = [[command.arguments objectAtIndex:4] boolValue];
+ NSNumber *reqId = [command.arguments objectAtIndex:5];
[self setRequestHeaders: headers forManager: manager];
[self setupAuthChallengeBlock: manager];
@@ -486,7 +529,8 @@
[[SDNetworkActivityIndicator sharedActivityIndicator] startActivity];
@try {
- [manager GET:url parameters:nil progress: nil success:^(NSURLSessionTask *task, id responseObject) {
+ NSURLSessionDataTask *task = [manager GET:url parameters:nil progress: nil success:^(NSURLSessionTask *task, id responseObject) {
+ [weakSelf removeRequest:reqId];
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
@@ -547,6 +591,7 @@
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
} failure:^(NSURLSessionTask *task, NSError *error) {
+ [weakSelf removeRequest:reqId];
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
[self handleError:dictionary withResponse:(NSHTTPURLResponse*)task.response error:error];
[dictionary setObject:@"There was an error downloading the file" forKey:@"error"];
@@ -555,6 +600,7 @@
[weakSelf.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
}];
+ [self addRequest:reqId forTask:task];
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
@@ -562,4 +608,32 @@
}
}
+- (void)abort:(CDVInvokedUrlCommand*)command {
+
+ NSNumber *reqId = [command.arguments objectAtIndex:0];
+
+ CDVPluginResult *pluginResult;
+ bool removed = false;
+ NSURLSessionDataTask *task = [reqDict objectForKey:reqId];
+ if(task){
+ @try{
+ [task cancel];
+ removed = true;
+ } @catch (NSException *exception) {
+ NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
+ [dictionary setValue:exception.userInfo forKey:@"error"];
+ [dictionary setObject:[NSNumber numberWithInt:-1] forKey:@"status"];
+ pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dictionary];
+ }
+ }
+
+ if(!pluginResult){
+ NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
+ [dictionary setObject:[NSNumber numberWithBool:removed] forKey:@"aborted"];
+ pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
+ }
+
+ [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+}
+
@end
diff --git a/test/e2e-app-template/config.xml b/test/e2e-app-template/config.xml
index 92f40b2..8138da6 100644
--- a/test/e2e-app-template/config.xml
+++ b/test/e2e-app-template/config.xml
@@ -16,6 +16,10 @@
+
+
+
+
diff --git a/test/e2e-app-template/network_security_config.xml b/test/e2e-app-template/network_security_config.xml
new file mode 100644
index 0000000..d908a81
--- /dev/null
+++ b/test/e2e-app-template/network_security_config.xml
@@ -0,0 +1,7 @@
+
+
+
+ localhost
+ httpbin.org
+
+
diff --git a/test/e2e-app-template/package.json b/test/e2e-app-template/package.json
index a2c6202..50e722f 100644
--- a/test/e2e-app-template/package.json
+++ b/test/e2e-app-template/package.json
@@ -20,7 +20,12 @@
"platforms": [
"android",
"ios"
- ]
+ ],
+ "plugins": {
+ "cordova-plugin-device": {}
+ }
},
- "devDependencies": {}
-}
\ No newline at end of file
+ "devDependencies": {
+ "cordova-plugin-device": "2.0.3"
+ }
+}
diff --git a/test/e2e-app-template/www/index.js b/test/e2e-app-template/www/index.js
index 86d8cd4..016fded 100644
--- a/test/e2e-app-template/www/index.js
+++ b/test/e2e-app-template/www/index.js
@@ -10,12 +10,12 @@ const app = {
var onlyFlaggedTests = [];
var enabledTests = [];
-
+
tests.forEach(function (test) {
if (test.only) {
onlyFlaggedTests.push(test);
}
-
+
if (!test.disabled) {
enabledTests.push(test);
}
@@ -50,6 +50,16 @@ const app = {
};
},
+ skip: function (content) {
+ document.getElementById('statusInput').value = 'finished';
+ app.printResult('result - skipped', content);
+
+ app.lastResult = {
+ type: 'skipped',
+ data: content
+ };
+ },
+
throw: function (error) {
document.getElementById('statusInput').value = 'finished';
app.printResult('result - throwed', error.message);
@@ -126,7 +136,7 @@ const app = {
const execTest = function () {
try {
- testDefinition.func(app.resolve, app.reject);
+ testDefinition.func(app.resolve, app.reject, app.skip);
} catch (error) {
app.throw(error);
}
diff --git a/test/e2e-specs.js b/test/e2e-specs.js
index 6c210d9..80e175d 100644
--- a/test/e2e-specs.js
+++ b/test/e2e-specs.js
@@ -83,7 +83,16 @@ const helpers = {
}
result.type.should.be.equal(expected);
- }
+ },
+ isAbortSupported: function () {
+ if (window.cordova && window.cordova.platformId === 'android') {
+ var version = device.version; //NOTE will throw error if cordova is present without cordova-plugin-device
+ var major = parseInt(/^(\d+)(\.|$)/.exec(version)[1], 10);
+ return isFinite(major) && major >= 6;
+ }
+ return true;
+ },
+ getAbortDelay: function () { return 10; },
};
const messageFactory = {
@@ -988,6 +997,118 @@ const tests = [
JSON.parse(result.data.data).cookies.should.be.eql({});
}
},
+ {
+ description: 'should be able to abort (POST)',
+ expected: 'rejected: {"status":-8, "error": "Request ...}',
+ before: helpers.setRawSerializer,
+ func: function (resolve, reject, skip) {
+ if (!helpers.isAbortSupported()) {
+ skip();
+ return;
+ }
+ helpers.getWithXhr(function (buffer) {
+ var reqId = cordova.plugin.http.post('http://httpbin.org/anything', buffer, {}, resolve, reject);
+
+ setTimeout(function () {
+ cordova.plugin.http.abort(reqId);
+ }, helpers.getAbortDelay());
+
+ }, './res/cordova_logo.png', 'arraybuffer');
+ },
+ validationFunc: function (driver, result) {
+ helpers.checkResult(result, 'rejected');
+ result.data.status.should.be.equal(-8);
+ }
+ },
+ {
+ description: 'should be able to abort (GET)',
+ expected: 'rejected: {"status":-8, "error": "Request ...}',
+ func: function (resolve, reject, skip) {
+ if (!helpers.isAbortSupported()) {
+ skip();
+ return;
+ }
+ var url = 'https://httpbin.org/image/jpeg';
+ var options = { method: 'get', responseType: 'blob' };
+ var success = function (response) {
+ resolve({
+ isBlob: response.data.constructor === Blob,
+ type: response.data.type,
+ byteLength: response.data.size
+ });
+ };
+
+ var reqId = cordova.plugin.http.sendRequest(url, options, success, reject);
+ setTimeout(function () {
+ cordova.plugin.http.abort(reqId);
+ }, helpers.getAbortDelay());
+ },
+ validationFunc: function (driver, result) {
+ helpers.checkResult(result, 'rejected');
+ result.data.status.should.be.equal(-8);
+ }
+ },
+ {
+ description: 'should be able to abort downloading a file',
+ expected: 'rejected: {"status":-8, "error": "Request ...}',
+ func: function (resolve, reject, skip) {
+ if (!helpers.isAbortSupported()) {
+ skip();
+ return;
+ }
+ var sourceUrl = 'http://httpbin.org/xml';
+ var targetPath = cordova.file.cacheDirectory + 'test.xml';
+
+ var reqId = cordova.plugin.http.downloadFile(sourceUrl, {}, {}, targetPath, function (entry) {
+ helpers.getWithXhr(function (content) {
+ resolve({
+ sourceUrl: sourceUrl,
+ targetPath: targetPath,
+ fullPath: entry.fullPath,
+ name: entry.name,
+ content: content
+ });
+ }, targetPath);
+ }, reject);
+
+ setTimeout(function () {
+ cordova.plugin.http.abort(reqId);
+ }, helpers.getAbortDelay());
+
+ },
+ validationFunc: function (driver, result) {
+ helpers.checkResult(result, 'rejected');
+ result.data.status.should.be.equal(-8);
+ }
+ },
+ {
+ description: 'should be able to abort uploading a file',
+ expected: 'rejected: {"status":-8, "error": "Request ...}',
+ func: function (resolve, reject, skip) {
+ if (!helpers.isAbortSupported()) {
+ skip();
+ return;
+ }
+ var fileName = 'test-file.txt';
+ var fileContent = 'I am a dummy file. I am used for testing purposes!';
+ var sourcePath = cordova.file.cacheDirectory + fileName;
+ var targetUrl = 'http://httpbin.org/post';
+
+ helpers.writeToFile(function () {
+
+ var reqId = cordova.plugin.http.uploadFile(targetUrl, {}, {}, sourcePath, fileName, resolve, reject);
+
+ setTimeout(function () {
+ cordova.plugin.http.abort(reqId);
+ }, helpers.getAbortDelay());
+
+ }, fileName, fileContent);
+ },
+ validationFunc: function (driver, result) {
+ helpers.checkResult(result, 'rejected');
+ result.data.status.should.be.equal(-8);
+ }
+ },
];
if (typeof module !== 'undefined' && module.exports) {
diff --git a/test/e2e-tooling/test.js b/test/e2e-tooling/test.js
index c34917d..30a7f14 100644
--- a/test/e2e-tooling/test.js
+++ b/test/e2e-tooling/test.js
@@ -49,12 +49,19 @@ describe('Advanced HTTP e2e test suite', function () {
});
const defineTestForMocha = (test, index) => {
- it(index + ': ' + test.description, async () => {
+ it(index + ': ' + test.description, async function () {
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 skipped = await checkSkipped(driver);
+
+ if (skipped) {
+ this.skip();
+ } else {
+ await validateResult(driver, test.validationFunc, targetInfo);
+ }
});
};
@@ -117,6 +124,11 @@ async function validateResult(driver, validationFunc, targetInfo) {
validationFunc(driver, result, targetInfo);
}
+async function checkSkipped(driver) {
+ const result = await driver.safeExecute('app.lastResult');
+ return result.type === 'skipped';
+}
+
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
diff --git a/test/js-specs.js b/test/js-specs.js
index 10c6236..384a846 100644
--- a/test/js-specs.js
+++ b/test/js-specs.js
@@ -676,6 +676,20 @@ describe('Common helpers', function () {
})
});
});
+
+ describe('nextRequestId()', function () {
+ const helpers = require('../www/helpers')(null, null, null, null, null, null);
+
+ it('returns number requestIds', () => {
+ helpers.nextRequestId().should.be.a('number');
+ });
+
+ it('returns unique requestIds', () => {
+ const ids = [helpers.nextRequestId(), helpers.nextRequestId(), helpers.nextRequestId()];
+ const set = new Set(ids);
+ ids.should.to.deep.equal(Array.from(set));
+ });
+ });
});
describe('Dependency Validator', function () {
diff --git a/www/error-codes.js b/www/error-codes.js
index a0bfd6c..15c13c0 100644
--- a/www/error-codes.js
+++ b/www/error-codes.js
@@ -6,4 +6,5 @@ module.exports = {
UNSUPPORTED_URL: -5,
NOT_CONNECTED: -6,
POST_PROCESSING_FAILED: -7,
+ ABORTED: -8,
};
diff --git a/www/helpers.js b/www/helpers.js
index b88d776..19ad259 100644
--- a/www/helpers.js
+++ b/www/helpers.js
@@ -5,6 +5,13 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64,
var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'options', 'upload', 'download'];
var validResponseTypes = ['text', 'json', 'arraybuffer', 'blob'];
+ var nextRequestId = (function(){
+ var currReqId = 0;
+ return function nextRequestId() {
+ return ++currReqId;
+ }
+ })();
+
var interface = {
b64EncodeUnicode: b64EncodeUnicode,
checkClientAuthMode: checkClientAuthMode,
@@ -24,6 +31,7 @@ module.exports = function init(global, jsUtil, cookieHandler, messages, base64,
injectCookieHandler: injectCookieHandler,
injectFileEntryHandler: injectFileEntryHandler,
injectRawResponseHandler: injectRawResponseHandler,
+ nextRequestId: nextRequestId,
};
// expose all functions for testing purposes
diff --git a/www/public-interface.js b/www/public-interface.js
index 73f75a9..650b5fa 100644
--- a/www/public-interface.js
+++ b/www/public-interface.js
@@ -26,6 +26,7 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
options: options,
uploadFile: uploadFile,
downloadFile: downloadFile,
+ abort: abort,
ErrorCode: errorCodes,
ponyfills: ponyfills
};
@@ -143,23 +144,31 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
var onFail = helpers.injectCookieHandler(url, failure);
var onSuccess = helpers.injectCookieHandler(url, helpers.injectRawResponseHandler(options.responseType, success, failure));
+ var reqId = helpers.nextRequestId();
+
switch (options.method) {
case 'post':
case 'put':
case 'patch':
- 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]);
+ 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, reqId]);
});
+ break;
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]);
+ exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFiles', [url, headers, fileOptions.filePaths, fileOptions.names, options.timeout, options.followRedirect, options.responseType, reqId]);
+ break;
case 'download':
var filePath = helpers.checkDownloadFilePath(options.filePath);
var onDownloadSuccess = helpers.injectCookieHandler(url, helpers.injectFileEntryHandler(success));
- return exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, filePath, options.timeout, options.followRedirect]);
+ exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, filePath, options.timeout, options.followRedirect, reqId]);
+ break;
default:
- return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect, options.responseType]);
+ exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect, options.responseType, reqId]);
+ break;
}
+
+ return reqId;
}
function post(url, data, headers, success, failure) {
@@ -198,5 +207,9 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf
return publicInterface.sendRequest(url, { method: 'download', params: params, headers: headers, filePath: filePath }, success, failure);
}
+ function abort(requestId , success, failure) {
+ return exec(success, failure, 'CordovaHttpPlugin', 'abort', [requestId]);
+ }
+
return publicInterface;
}