diff --git a/plugin.xml b/plugin.xml index ac45f0b..8204cdb 100644 --- a/plugin.xml +++ b/plugin.xml @@ -28,6 +28,7 @@ + @@ -39,6 +40,7 @@ + @@ -87,4 +89,4 @@ - \ No newline at end of file + diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java index 6905b2f..011b0dc 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java @@ -30,6 +30,7 @@ abstract class CordovaHttpBase implements Runnable { protected String method; protected String url; protected String serializer = "none"; + protected String responseType; protected Object data; protected JSONObject headers; protected int timeout; @@ -38,7 +39,8 @@ abstract class CordovaHttpBase implements Runnable { protected CallbackContext callbackContext; public CordovaHttpBase(String method, String url, String serializer, Object data, JSONObject headers, int timeout, - boolean followRedirects, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, + CallbackContext callbackContext) { this.method = method; this.url = url; @@ -47,18 +49,20 @@ abstract class CordovaHttpBase implements Runnable { this.headers = headers; this.timeout = timeout; this.followRedirects = followRedirects; + this.responseType = responseType; this.tlsConfiguration = tlsConfiguration; this.callbackContext = callbackContext; } public CordovaHttpBase(String method, String url, JSONObject headers, int timeout, boolean followRedirects, - TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { this.method = method; this.url = url; this.headers = headers; this.timeout = timeout; this.followRedirects = followRedirects; + this.responseType = responseType; this.tlsConfiguration = tlsConfiguration; this.callbackContext = callbackContext; } @@ -158,17 +162,19 @@ abstract class CordovaHttpBase implements Runnable { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); request.receive(outputStream); - ByteBuffer rawOutput = ByteBuffer.wrap(outputStream.toByteArray()); - String decodedBody = HttpBodyDecoder.decodeBody(rawOutput, request.charset()); - response.setStatus(request.code()); response.setUrl(request.url().toString()); response.setHeaders(request.headers()); if (request.code() >= 200 && request.code() < 300) { - response.setBody(decodedBody); + if ("text".equals(this.responseType)) { + String decoded = HttpBodyDecoder.decodeBody(outputStream.toByteArray(), request.charset()); + response.setBody(decoded); + } else { + response.setData(outputStream.toByteArray()); + } } else { - response.setErrorMessage(decodedBody); + response.setErrorMessage(HttpBodyDecoder.decodeBody(outputStream.toByteArray(), request.charset())); } } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java index 3b30bfe..d89db82 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java @@ -19,7 +19,7 @@ class CordovaHttpDownload extends CordovaHttpBase { public CordovaHttpDownload(String url, JSONObject headers, String filePath, int timeout, boolean followRedirects, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { - super("GET", url, headers, timeout, followRedirects, tlsConfiguration, callbackContext); + super("GET", url, headers, timeout, followRedirects, "text", tlsConfiguration, callbackContext); this.filePath = filePath; } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java index 2a9a34c..5f17e5d 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java @@ -10,14 +10,16 @@ import org.json.JSONObject; class CordovaHttpOperation extends CordovaHttpBase { public CordovaHttpOperation(String method, String url, String serializer, Object data, JSONObject headers, - int timeout, boolean followRedirects, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, + CallbackContext callbackContext) { - super(method, url, serializer, data, headers, timeout, followRedirects, tlsConfiguration, callbackContext); + super(method, url, serializer, data, headers, timeout, followRedirects, responseType, tlsConfiguration, + callbackContext); } public CordovaHttpOperation(String method, String url, JSONObject headers, int timeout, boolean followRedirects, - TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { - super(method, url, headers, timeout, followRedirects, tlsConfiguration, callbackContext); + super(method, url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index 43ccd56..b7ea1b3 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -83,9 +83,10 @@ public class CordovaHttpPlugin extends CordovaPlugin { JSONObject headers = args.getJSONObject(1); int timeout = args.getInt(2) * 1000; boolean followRedirect = args.getBoolean(3); + String responseType = args.getString(4); CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect, - this.tlsConfiguration, callbackContext); + responseType, this.tlsConfiguration, callbackContext); cordova.getThreadPool().execute(request); @@ -101,9 +102,10 @@ public class CordovaHttpPlugin extends CordovaPlugin { JSONObject headers = args.getJSONObject(3); int timeout = args.getInt(4) * 1000; boolean followRedirect = args.getBoolean(5); + String responseType = args.getString(6); CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers, - timeout, followRedirect, this.tlsConfiguration, callbackContext); + timeout, followRedirect, responseType, this.tlsConfiguration, callbackContext); cordova.getThreadPool().execute(request); @@ -117,9 +119,10 @@ public class CordovaHttpPlugin extends CordovaPlugin { String uploadName = args.getString(3); int timeout = args.getInt(4) * 1000; boolean followRedirect = args.getBoolean(5); + String responseType = args.getString(6); CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePath, uploadName, timeout, followRedirect, - this.tlsConfiguration, callbackContext); + responseType, this.tlsConfiguration, callbackContext); cordova.getThreadPool().execute(upload); diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java b/src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java index 407af47..e6051bf 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpResponse.java @@ -1,5 +1,7 @@ package com.silkimen.cordovahttp; +import java.nio.ByteBuffer; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -9,15 +11,18 @@ import org.json.JSONObject; import android.text.TextUtils; import android.util.Log; +import android.util.Base64; class CordovaHttpResponse { private int status; private String url; private Map> headers; private String body; + private byte[] rawData; private JSONObject fileEntry; private boolean hasFailed; private boolean isFileOperation; + private boolean isRawResponse; private String error; public void setStatus(int status) { @@ -36,6 +41,11 @@ class CordovaHttpResponse { this.body = body; } + public void setData(byte[] rawData) { + this.isRawResponse = true; + this.rawData = rawData; + } + public void setFileEntry(JSONObject entry) { this.isFileOperation = true; this.fileEntry = entry; @@ -64,6 +74,8 @@ class CordovaHttpResponse { json.put("error", this.error); } else if (this.isFileOperation) { json.put("file", this.fileEntry); + } else if (this.isRawResponse) { + json.put("data", Base64.encodeToString(this.rawData, Base64.DEFAULT)); } else { json.put("data", this.body); } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java index 623e025..9d74736 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java @@ -20,9 +20,10 @@ class CordovaHttpUpload extends CordovaHttpBase { private String uploadName; public CordovaHttpUpload(String url, JSONObject headers, String filePath, String uploadName, int timeout, - boolean followRedirects, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, + CallbackContext callbackContext) { - super("POST", url, headers, timeout, followRedirects, tlsConfiguration, callbackContext); + super("POST", url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); this.filePath = filePath; this.uploadName = uploadName; } diff --git a/src/android/com/silkimen/http/HttpBodyDecoder.java b/src/android/com/silkimen/http/HttpBodyDecoder.java index 7aeddc3..92d69e2 100644 --- a/src/android/com/silkimen/http/HttpBodyDecoder.java +++ b/src/android/com/silkimen/http/HttpBodyDecoder.java @@ -10,21 +10,28 @@ import java.nio.charset.MalformedInputException; public class HttpBodyDecoder { private static final String[] ACCEPTED_CHARSETS = new String[] { "UTF-8", "ISO-8859-1" }; - public static String decodeBody(ByteBuffer rawOutput, String charsetName) + public static String decodeBody(byte[] body, String charsetName) + throws CharacterCodingException, MalformedInputException { + + return decodeBody(ByteBuffer.wrap(body), charsetName); + } + + public static String decodeBody(ByteBuffer body, String charsetName) throws CharacterCodingException, MalformedInputException { if (charsetName == null) { - return tryDecodeByteBuffer(rawOutput); + return tryDecodeByteBuffer(body); + } else { + return decodeByteBuffer(body, charsetName); } - - return decodeByteBuffer(rawOutput, charsetName); } - private static String tryDecodeByteBuffer(ByteBuffer rawOutput) throws CharacterCodingException, MalformedInputException { + private static String tryDecodeByteBuffer(ByteBuffer buffer) + throws CharacterCodingException, MalformedInputException { for (int i = 0; i < ACCEPTED_CHARSETS.length - 1; i++) { try { - return decodeByteBuffer(rawOutput, ACCEPTED_CHARSETS[i]); + return decodeByteBuffer(buffer, ACCEPTED_CHARSETS[i]); } catch (MalformedInputException e) { continue; } catch (CharacterCodingException e) { @@ -32,13 +39,13 @@ public class HttpBodyDecoder { } } - return decodeBody(rawOutput, ACCEPTED_CHARSETS[ACCEPTED_CHARSETS.length - 1]); + return decodeBody(buffer, ACCEPTED_CHARSETS[ACCEPTED_CHARSETS.length - 1]); } - private static String decodeByteBuffer(ByteBuffer rawOutput, String charsetName) + private static String decodeByteBuffer(ByteBuffer buffer, String charsetName) throws CharacterCodingException, MalformedInputException { - return createCharsetDecoder(charsetName).decode(rawOutput).toString(); + return createCharsetDecoder(charsetName).decode(buffer).toString(); } private static CharsetDecoder createCharsetDecoder(String charsetName) { diff --git a/src/ios/AFNetworking/AFURLResponseSerialization.h b/src/ios/AFNetworking/AFURLResponseSerialization.h index a9430ad..10e0fef 100644 --- a/src/ios/AFNetworking/AFURLResponseSerialization.h +++ b/src/ios/AFNetworking/AFURLResponseSerialization.h @@ -308,4 +308,11 @@ FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseErrorK FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey; +/** +`AFNetworkingOperationFailingURLResponseBodyErrorKey` +The corresponding value is an `NSString` containing the decoded error message. + */ + +FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseBodyErrorKey; + NS_ASSUME_NONNULL_END diff --git a/src/ios/AFNetworking/AFURLResponseSerialization.m b/src/ios/AFNetworking/AFURLResponseSerialization.m index 5e46799..f88d938 100755 --- a/src/ios/AFNetworking/AFURLResponseSerialization.m +++ b/src/ios/AFNetworking/AFURLResponseSerialization.m @@ -34,6 +34,7 @@ NSString * const AFURLResponseSerializationErrorDomain = @"com.alamofire.error.serialization.response"; NSString * const AFNetworkingOperationFailingURLResponseErrorKey = @"com.alamofire.serialization.response.error.response"; NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey = @"com.alamofire.serialization.response.error.data"; +NSString * const AFNetworkingOperationFailingURLResponseBodyErrorKey = @"com.alamofire.serialization.response.error.body"; static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) { if (!error) { @@ -525,7 +526,7 @@ static NSLock* imageLock = nil; dispatch_once(&onceToken, ^{ imageLock = [[NSLock alloc] init]; }); - + [imageLock lock]; image = [UIImage imageWithData:data]; [imageLock unlock]; @@ -539,7 +540,7 @@ static UIImage * AFImageWithDataAtScale(NSData *data, CGFloat scale) { if (image.images) { return image; } - + return [[UIImage alloc] initWithCGImage:[image CGImage] scale:scale orientation:image.imageOrientation]; } diff --git a/src/ios/BinaryResponseSerializer.h b/src/ios/BinaryResponseSerializer.h new file mode 100644 index 0000000..92af266 --- /dev/null +++ b/src/ios/BinaryResponseSerializer.h @@ -0,0 +1,8 @@ +#import +#import "AFURLResponseSerialization.h" + +@interface BinaryResponseSerializer : AFHTTPResponseSerializer + ++ (instancetype)serializer; + +@end diff --git a/src/ios/BinaryResponseSerializer.m b/src/ios/BinaryResponseSerializer.m new file mode 100644 index 0000000..a891f8f --- /dev/null +++ b/src/ios/BinaryResponseSerializer.m @@ -0,0 +1,126 @@ +#import "BinaryResponseSerializer.h" + +static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) { + if (!error) { + return underlyingError; + } + + if (!underlyingError || error.userInfo[NSUnderlyingErrorKey]) { + return error; + } + + NSMutableDictionary *mutableUserInfo = [error.userInfo mutableCopy]; + mutableUserInfo[NSUnderlyingErrorKey] = underlyingError; + + return [[NSError alloc] initWithDomain:error.domain code:error.code userInfo:mutableUserInfo]; +} + +static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger code, NSString *domain) { + if ([error.domain isEqualToString:domain] && error.code == code) { + return YES; + } else if (error.userInfo[NSUnderlyingErrorKey]) { + return AFErrorOrUnderlyingErrorHasCodeInDomain(error.userInfo[NSUnderlyingErrorKey], code, domain); + } + + return NO; +} + +@implementation BinaryResponseSerializer + ++ (instancetype)serializer { + BinaryResponseSerializer *serializer = [[self alloc] init]; + return serializer; +} + +- (instancetype)init { + self = [super init]; + + if (!self) { + return nil; + } + + self.acceptableContentTypes = nil; + + return self; +} + +- (NSString*)decodeResponseData:(NSData*)rawResponseData withEncoding:(CFStringEncoding)cfEncoding { + NSStringEncoding nsEncoding; + NSString* decoded = nil; + + if (cfEncoding != kCFStringEncodingInvalidId) { + nsEncoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); + } + + NSStringEncoding supportedEncodings[6] = { + NSUTF8StringEncoding, NSWindowsCP1252StringEncoding, NSISOLatin1StringEncoding, + NSISOLatin2StringEncoding, NSASCIIStringEncoding, NSUnicodeStringEncoding + }; + + for (int i = 0; i < sizeof(supportedEncodings) / sizeof(NSStringEncoding) && !decoded; ++i) { + if (cfEncoding == kCFStringEncodingInvalidId || nsEncoding == supportedEncodings[i]) { + decoded = [[NSString alloc] initWithData:rawResponseData encoding:supportedEncodings[i]]; + } + } + + return decoded; +} + +- (CFStringEncoding) getEncoding:(NSURLResponse *)response { + CFStringEncoding encoding = kCFStringEncodingInvalidId; + + if (response.textEncodingName) { + encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + } + + return encoding; +} + +#pragma mark - + +- (BOOL)validateResponse:(NSHTTPURLResponse *)response + data:(NSData *)data + error:(NSError * __autoreleasing *)error +{ + if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) { + if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) { + NSMutableDictionary *mutableUserInfo = [@{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode], + NSURLErrorFailingURLErrorKey: [response URL], + AFNetworkingOperationFailingURLResponseErrorKey: response, + } mutableCopy]; + + if (data) { + mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data; + + // trying to decode error message in body + mutableUserInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] = [self decodeResponseData:data withEncoding:[self getEncoding:response]]; + } + + if (error) { + *error = [NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo]; + } + + return NO; + } + } + + return YES; +} + +#pragma mark - AFURLResponseSerialization + +- (id)responseObjectForResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) { + if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) { + return nil; + } + } + + return [data base64EncodedStringWithOptions:0]; +} + +@end diff --git a/src/ios/CordovaHttpPlugin.m b/src/ios/CordovaHttpPlugin.m index ac1c99b..13d4369 100644 --- a/src/ios/CordovaHttpPlugin.m +++ b/src/ios/CordovaHttpPlugin.m @@ -1,5 +1,6 @@ #import "CordovaHttpPlugin.h" #import "CDVFile.h" +#import "BinaryResponseSerializer.h" #import "TextResponseSerializer.h" #import "TextRequestSerializer.h" #import "AFHTTPSessionManager.h" @@ -57,6 +58,15 @@ [manager.requestSerializer setTimeoutInterval:timeout]; } +- (void)setResponseSerializer:(NSString*)responseType forManager:(AFHTTPSessionManager*)manager { + if ([responseType isEqualToString: @"text"]) { + manager.responseSerializer = [TextResponseSerializer serializer]; + } else { + manager.responseSerializer = [BinaryResponseSerializer serializer]; + } +} + + - (void)handleSuccess:(NSMutableDictionary*)dictionary withResponse:(NSHTTPURLResponse*)response andData:(id)data { if (response != nil) { [dictionary setValue:response.URL.absoluteString forKey:@"url"]; @@ -74,8 +84,8 @@ [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[AFNetworkingOperationFailingURLResponseBodyKey]) { - [dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyKey] forKey:@"error"]; + if (error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey]) { + [dictionary setObject:error.userInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] forKey:@"error"]; } } else { [dictionary setObject:[self getStatusCode:error] forKey:@"status"]; @@ -161,14 +171,15 @@ 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; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -197,6 +208,7 @@ - (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]; @@ -208,7 +220,6 @@ [self setRedirect:followRedirect forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -243,14 +254,15 @@ 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; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -286,14 +298,15 @@ 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; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -329,14 +342,15 @@ 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; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -372,14 +386,15 @@ 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; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -415,15 +430,16 @@ NSString *name = [command.arguments objectAtIndex: 3]; NSTimeInterval timeoutInSeconds = [[command.arguments objectAtIndex:4] doubleValue]; bool followRedirect = [[command.arguments objectAtIndex:5] boolValue]; + NSString *responseType = [command.arguments objectAtIndex:6]; NSURL *fileURL = [NSURL URLWithString: filePath]; [self setRequestHeaders: headers forManager: manager]; [self setTimeout:timeoutInSeconds forManager:manager]; [self setRedirect:followRedirect forManager:manager]; + [self setResponseSerializer:responseType forManager:manager]; CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [TextResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { @@ -464,6 +480,7 @@ - (void)downloadFile:(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]; @@ -480,7 +497,6 @@ } CordovaHttpPlugin* __weak weakSelf = self; - manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [[SDNetworkActivityIndicator sharedActivityIndicator] startActivity]; @try { diff --git a/src/ios/TextResponseSerializer.h b/src/ios/TextResponseSerializer.h index 7bf87db..d086a8c 100644 --- a/src/ios/TextResponseSerializer.h +++ b/src/ios/TextResponseSerializer.h @@ -5,6 +5,4 @@ + (instancetype)serializer; -FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseBodyKey; - @end diff --git a/src/ios/TextResponseSerializer.m b/src/ios/TextResponseSerializer.m index 762f7c3..76b6530 100644 --- a/src/ios/TextResponseSerializer.m +++ b/src/ios/TextResponseSerializer.m @@ -1,8 +1,5 @@ #import "TextResponseSerializer.h" -NSString * const AFNetworkingOperationFailingURLResponseBodyKey = @"com.alamofire.serialization.response.error.body"; -NSStringEncoding const SupportedEncodings[6] = { NSUTF8StringEncoding, NSWindowsCP1252StringEncoding, NSISOLatin1StringEncoding, NSISOLatin2StringEncoding, NSASCIIStringEncoding, NSUnicodeStringEncoding }; - static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) { if (!error) { return underlyingError; @@ -55,9 +52,14 @@ static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger co nsEncoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } - for (int i = 0; i < sizeof(SupportedEncodings) / sizeof(NSStringEncoding) && !decoded; ++i) { - if (cfEncoding == kCFStringEncodingInvalidId || nsEncoding == SupportedEncodings[i]) { - decoded = [[NSString alloc] initWithData:rawResponseData encoding:SupportedEncodings[i]]; + NSStringEncoding supportedEncodings[6] = { + NSUTF8StringEncoding, NSWindowsCP1252StringEncoding, NSISOLatin1StringEncoding, + NSISOLatin2StringEncoding, NSASCIIStringEncoding, NSUnicodeStringEncoding + }; + + for (int i = 0; i < sizeof(supportedEncodings) / sizeof(NSStringEncoding) && !decoded; ++i) { + if (cfEncoding == kCFStringEncodingInvalidId || nsEncoding == supportedEncodings[i]) { + decoded = [[NSString alloc] initWithData:rawResponseData encoding:supportedEncodings[i]]; } } @@ -94,7 +96,7 @@ static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger co NSURLErrorFailingURLErrorKey:[response URL], AFNetworkingOperationFailingURLResponseErrorKey: response, AFNetworkingOperationFailingURLResponseDataErrorKey: data, - AFNetworkingOperationFailingURLResponseBodyKey: @"Could not decode response data due to invalid or unknown charset encoding", + AFNetworkingOperationFailingURLResponseBodyErrorKey: @"Could not decode response data due to invalid or unknown charset encoding", } mutableCopy]; validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError); @@ -108,7 +110,7 @@ static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger co if (data) { mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data; - mutableUserInfo[AFNetworkingOperationFailingURLResponseBodyKey] = *decoded; + mutableUserInfo[AFNetworkingOperationFailingURLResponseBodyErrorKey] = *decoded; } validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError); diff --git a/test/e2e-specs.js b/test/e2e-specs.js index 7092f52..17c46d1 100644 --- a/test/e2e-specs.js +++ b/test/e2e-specs.js @@ -62,6 +62,18 @@ const helpers = { }, done); }, done); }, done); + }, + // adopted from: https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ + hashArrayBuffer: function (buffer) { + var hash = 0; + var byteArray = new Uint8Array(buffer); + + for (var i = 0; i < byteArray.length; i++) { + hash = ((hash << 5) - hash) + byteArray[i]; + hash |= 0; // Convert to 32bit integer + } + + return hash; } }; @@ -682,6 +694,40 @@ const tests = [ result.type.should.be.equal('rejected'); should.equal(result.data.url, undefined); } + }, + { + description: 'should fetch binary correctly when response type is "arraybuffer"', + expected: 'resolved: {"hash":-1032603775,"byteLength":35588}', + func: function (resolve, reject) { + var url = 'https://httpbin.org/image/jpeg'; + var options = { method: 'get', responseType: 'arraybuffer' }; + var success = function (response) { + resolve({ + hash: helpers.hashArrayBuffer(response.data), + byteLength: response.data.byteLength + }); + }; + cordova.plugin.http.sendRequest(url, options, success, reject); + }, + validationFunc: function (driver, result) { + result.type.should.be.equal('resolved'); + result.data.hash.should.be.equal(-1032603775); + result.data.byteLength.should.be.equal(35588); + } + }, + { + description: 'should decode error body even if response type is "arraybuffer"', + expected: 'rejected: {"status": 418, ...', + func: function (resolve, reject) { + var url = 'https://httpbin.org/status/418'; + var options = { method: 'get', responseType: 'arraybuffer' }; + cordova.plugin.http.sendRequest(url, options, resolve, reject); + }, + validationFunc: function (driver, result) { + result.type.should.be.equal('rejected'); + result.data.status.should.be.equal(418); + result.data.error.should.be.equal("\n -=[ teapot ]=-\n\n _...._\n .' _ _ `.\n | .\"` ^ `\". _,\n \\_;`\"---\"`|//\n | ;/\n \\_ _/\n `\"\"\"`\n"); + } } // @TODO: not ready yet // { diff --git a/www/advanced-http.js b/www/advanced-http.js index b6732dc..a2a8700 100644 --- a/www/advanced-http.js +++ b/www/advanced-http.js @@ -5,6 +5,7 @@ var pluginId = module.id.slice(0, module.id.lastIndexOf('.')); var exec = require('cordova/exec'); +var base64 = require('cordova/base64'); var messages = require(pluginId + '.messages'); var globalConfigs = require(pluginId + '.global-configs'); var jsUtil = require(pluginId + '.js-util'); @@ -12,7 +13,7 @@ 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); +var helpers = require(pluginId + '.helpers')(jsUtil, cookieHandler, messages, base64); var urlUtil = require(pluginId + '.url-util')(jsUtil); var publicInterface = require(pluginId + '.public-interface')(exec, cookieHandler, urlUtil, helpers, globalConfigs); diff --git a/www/helpers.js b/www/helpers.js index 9e9127c..e6180d5 100644 --- a/www/helpers.js +++ b/www/helpers.js @@ -1,8 +1,9 @@ -module.exports = function init(jsUtil, cookieHandler, messages) { +module.exports = function init(jsUtil, cookieHandler, messages, base64) { var validSerializers = ['urlencoded', 'json', 'utf8']; var validCertModes = ['default', 'nocheck', 'pinned', 'legacy']; var validClientAuthModes = ['none', 'systemstore', 'buffer']; var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'upload', 'download']; + var validResponseTypes = ['text','arraybuffer']; var interface = { b64EncodeUnicode: b64EncodeUnicode, @@ -15,6 +16,7 @@ module.exports = function init(jsUtil, cookieHandler, messages) { checkTimeoutValue: checkTimeoutValue, checkFollowRedirectValue: checkFollowRedirectValue, injectCookieHandler: injectCookieHandler, + injectRawResponseHandler: injectRawResponseHandler, injectFileEntryHandler: injectFileEntryHandler, getMergedHeaders: getMergedHeaders, getProcessedData: getProcessedData, @@ -28,6 +30,7 @@ module.exports = function init(jsUtil, cookieHandler, messages) { interface.checkForValidStringValue = checkForValidStringValue; interface.checkKeyValuePairObject = checkKeyValuePairObject; interface.checkHttpMethod = checkHttpMethod; + interface.checkResponseType = checkResponseType; interface.checkHeadersObject = checkHeadersObject; interface.checkParamsObject = checkParamsObject; interface.resolveCookieString = resolveCookieString; @@ -95,6 +98,10 @@ module.exports = function init(jsUtil, cookieHandler, messages) { return checkForValidStringValue(validHttpMethods, method, messages.INVALID_HTTP_METHOD); } + function checkResponseType(type) { + return checkForValidStringValue(validResponseTypes, type, messages.INVALID_RESPONSE_TYPE); + } + function checkSerializer(serializer) { return checkForValidStringValue(validSerializers, serializer, messages.INVALID_DATA_SERIALIZER); } @@ -227,6 +234,17 @@ module.exports = function init(jsUtil, cookieHandler, messages) { } } + function injectRawResponseHandler(responseType, cb) { + return function (response) { + // arraybuffer + if (responseType === validResponseTypes[1]) { + response.data = base64.toArrayBuffer(response.data); + } + + cb(response); + } + } + function injectFileEntryHandler(cb) { return function (response) { cb(createFileEntry(response.file)); @@ -302,6 +320,7 @@ module.exports = function init(jsUtil, cookieHandler, messages) { return { method: checkHttpMethod(options.method || validHttpMethods[0]), + responseType: checkResponseType(options.responseType || validResponseTypes[0]), serializer: checkSerializer(options.serializer || globals.serializer), timeout: checkTimeoutValue(options.timeout || globals.timeout), followRedirect: checkFollowRedirectValue(options.followRedirect || globals.followRedirect), diff --git a/www/messages.js b/www/messages.js index 5eebeb4..dd14eaf 100644 --- a/www/messages.js +++ b/www/messages.js @@ -1,18 +1,19 @@ module.exports = { ADDING_COOKIES_NOT_SUPPORTED: 'advanced-http: "setHeader" does not support adding cookies, please use "setCookie" function instead', DATA_TYPE_MISMATCH: 'advanced-http: "data" argument supports only following data types:', - MANDATORY_SUCCESS: 'advanced-http: missing mandatory "onSuccess" callback function', - MANDATORY_FAIL: 'advanced-http: missing mandatory "onFail" callback function', - INVALID_HTTP_METHOD: 'advanced-http: invalid HTTP method, supported methods are:', - INVALID_DATA_SERIALIZER: 'advanced-http: invalid serializer, supported serializers are:', - INVALID_SSL_CERT_MODE: 'advanced-http: invalid SSL cert mode, supported modes are:', + INVALID_CLIENT_AUTH_ALIAS: 'advanced-http: invalid client certificate alias, needs to be a string or 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 object', - INVALID_CLIENT_AUTH_ALIAS: 'advanced-http: invalid client certificate alias, needs to be a string or undefined', - INVALID_CLIENT_AUTH_RAW_PKCS: 'advanced-http: invalid PKCS12 container, needs to be an array buffer', INVALID_CLIENT_AUTH_PKCS_PASSWORD: 'advanced-http: invalid PKCS12 container password, needs to be a string', - INVALID_HEADERS_VALUE: 'advanced-http: header values must be strings', - INVALID_TIMEOUT_VALUE: 'advanced-http: invalid timeout value, needs to be a positive numeric value', + INVALID_CLIENT_AUTH_RAW_PKCS: 'advanced-http: invalid PKCS12 container, needs to be an array buffer', + INVALID_DATA_SERIALIZER: 'advanced-http: invalid serializer, supported serializers are:', INVALID_FOLLOW_REDIRECT_VALUE: 'advanced-http: invalid follow redirect value, needs to be a boolean value', - INVALID_PARAMS_VALUE: 'advanced-http: invalid params object, needs to be an object with strings' + INVALID_HEADERS_VALUE: 'advanced-http: header values must be strings', + INVALID_HTTP_METHOD: 'advanced-http: invalid HTTP method, supported methods are:', + INVALID_PARAMS_VALUE: 'advanced-http: invalid params object, needs to be an object with strings', + INVALID_RESPONSE_TYPE: 'advanced-http: invalid response type, supported types are:', + INVALID_SSL_CERT_MODE: 'advanced-http: invalid SSL cert mode, supported modes are:', + INVALID_TIMEOUT_VALUE: 'advanced-http: invalid timeout value, needs to be a positive numeric value', + MANDATORY_FAIL: 'advanced-http: missing mandatory "onFail" callback function', + MANDATORY_SUCCESS: 'advanced-http: missing mandatory "onSuccess" callback function', }; diff --git a/www/public-interface.js b/www/public-interface.js index 60c850c..371c4da 100644 --- a/www/public-interface.js +++ b/www/public-interface.js @@ -143,22 +143,23 @@ module.exports = function init(exec, cookieHandler, urlUtil, helpers, globalConf url = urlUtil.appendQueryParamsString(url, urlUtil.serializeQueryParams(options.params, true)); var headers = helpers.getMergedHeaders(url, options.headers, globalConfigs.headers); - var onSuccess = helpers.injectCookieHandler(url, success); + var onFail = helpers.injectCookieHandler(url, failure); + var onSuccess = helpers.injectCookieHandler(url, helpers.injectRawResponseHandler(options.responseType, success)); switch (options.method) { case 'post': case 'put': case 'patch': var data = helpers.getProcessedData(options.data, options.serializer); - return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout, options.followRedirect]); + return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, data, options.serializer, headers, options.timeout, options.followRedirect, options.responseType]); case 'upload': - return exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFile', [url, headers, options.filePath, options.name, options.timeout, options.followRedirect]); + return exec(onSuccess, onFail, 'CordovaHttpPlugin', 'uploadFile', [url, headers, options.filePath, options.name, options.timeout, options.followRedirect, options.responseType]); case 'download': var onDownloadSuccess = helpers.injectCookieHandler(url, helpers.injectFileEntryHandler(success)); return exec(onDownloadSuccess, onFail, 'CordovaHttpPlugin', 'downloadFile', [url, headers, options.filePath, options.timeout, options.followRedirect]); default: - return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect]); + return exec(onSuccess, onFail, 'CordovaHttpPlugin', options.method, [url, headers, options.timeout, options.followRedirect, options.responseType]); } }