diff --git a/src/ios/CDVFileTransfer.h b/src/ios/CDVFileTransfer.h new file mode 100644 index 0000000..35e3fdd --- /dev/null +++ b/src/ios/CDVFileTransfer.h @@ -0,0 +1,82 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import +#import "CDVPlugin.h" + +enum CDVFileTransferError { + FILE_NOT_FOUND_ERR = 1, + INVALID_URL_ERR = 2, + CONNECTION_ERR = 3, + CONNECTION_ABORTED = 4 +}; +typedef int CDVFileTransferError; + +enum CDVFileTransferDirection { + CDV_TRANSFER_UPLOAD = 1, + CDV_TRANSFER_DOWNLOAD = 2, +}; +typedef int CDVFileTransferDirection; + +// Magic value within the options dict used to set a cookie. +extern NSString* const kOptionsKeyCookie; + +@interface CDVFileTransfer : CDVPlugin {} + +- (void)upload:(CDVInvokedUrlCommand*)command; +- (void)download:(CDVInvokedUrlCommand*)command; +- (NSString*)escapePathComponentForUrlString:(NSString*)urlString; + +// Visible for testing. +- (NSURLRequest*)requestForUploadCommand:(CDVInvokedUrlCommand*)command fileData:(NSData*)fileData; +- (NSMutableDictionary*)createFileTransferError:(int)code AndSource:(NSString*)source AndTarget:(NSString*)target; + +- (NSMutableDictionary*)createFileTransferError:(int)code + AndSource:(NSString*)source + AndTarget:(NSString*)target + AndHttpStatus:(int)httpStatus + AndBody:(NSString*)body; +@property (readonly) NSMutableDictionary* activeTransfers; +@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskID; +@end + +@class CDVFileTransferEntityLengthRequest; + +@interface CDVFileTransferDelegate : NSObject {} + +- (void)updateBytesExpected:(NSInteger)newBytesExpected; +- (void)cancelTransfer:(NSURLConnection*)connection; + +@property (strong) NSMutableData* responseData; // atomic +@property (nonatomic, strong) CDVFileTransfer* command; +@property (nonatomic, assign) CDVFileTransferDirection direction; +@property (nonatomic, strong) NSURLConnection* connection; +@property (nonatomic, copy) NSString* callbackId; +@property (nonatomic, copy) NSString* objectId; +@property (nonatomic, copy) NSString* source; +@property (nonatomic, copy) NSString* target; +@property (nonatomic, copy) NSString* mimeType; +@property (assign) int responseCode; // atomic +@property (nonatomic, assign) NSInteger bytesTransfered; +@property (nonatomic, assign) NSInteger bytesExpected; +@property (nonatomic, assign) BOOL trustAllHosts; +@property (strong) NSFileHandle* targetFileHandle; +@property (nonatomic, strong) CDVFileTransferEntityLengthRequest* entityLengthRequest; + +@end; diff --git a/src/ios/CDVFileTransfer.m b/src/ios/CDVFileTransfer.m new file mode 100644 index 0000000..0f6b174 --- /dev/null +++ b/src/ios/CDVFileTransfer.m @@ -0,0 +1,730 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +#import "CDV.h" + +#import +#import +#import +#import + +@interface CDVFileTransfer () +// Sets the requests headers for the request. +- (void)applyRequestHeaders:(NSDictionary*)headers toRequest:(NSMutableURLRequest*)req; +// Creates a delegate to handle an upload. +- (CDVFileTransferDelegate*)delegateForUploadCommand:(CDVInvokedUrlCommand*)command; +// Creates an NSData* for the file for the given upload arguments. +- (void)fileDataForUploadCommand:(CDVInvokedUrlCommand*)command; +@end + +// Buffer size to use for streaming uploads. +static const NSUInteger kStreamBufferSize = 32768; +// Magic value within the options dict used to set a cookie. +NSString* const kOptionsKeyCookie = @"__cookie"; +// Form boundary for multi-part requests. +NSString* const kFormBoundary = @"+++++org.apache.cordova.formBoundary"; + +// Writes the given data to the stream in a blocking way. +// If successful, returns bytesToWrite. +// If the stream was closed on the other end, returns 0. +// If there was an error, returns -1. +static CFIndex WriteDataToStream(NSData* data, CFWriteStreamRef stream) +{ + UInt8* bytes = (UInt8*)[data bytes]; + NSUInteger bytesToWrite = [data length]; + NSUInteger totalBytesWritten = 0; + + while (totalBytesWritten < bytesToWrite) { + CFIndex result = CFWriteStreamWrite(stream, + bytes + totalBytesWritten, + bytesToWrite - totalBytesWritten); + if (result < 0) { + CFStreamError error = CFWriteStreamGetError(stream); + NSLog(@"WriteStreamError domain: %ld error: %ld", error.domain, error.error); + return result; + } else if (result == 0) { + return result; + } + totalBytesWritten += result; + } + + return totalBytesWritten; +} + +@implementation CDVFileTransfer +@synthesize activeTransfers; + +- (NSString*)escapePathComponentForUrlString:(NSString*)urlString +{ + NSRange schemeAndHostRange = [urlString rangeOfString:@"://.*?/" options:NSRegularExpressionSearch]; + + if (schemeAndHostRange.length == 0) { + return urlString; + } + + NSInteger schemeAndHostEndIndex = NSMaxRange(schemeAndHostRange); + NSString* schemeAndHost = [urlString substringToIndex:schemeAndHostEndIndex]; + NSString* pathComponent = [urlString substringFromIndex:schemeAndHostEndIndex]; + pathComponent = [pathComponent stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + return [schemeAndHost stringByAppendingString:pathComponent]; +} + +- (void)applyRequestHeaders:(NSDictionary*)headers toRequest:(NSMutableURLRequest*)req +{ + [req setValue:@"XMLHttpRequest" forHTTPHeaderField:@"X-Requested-With"]; + + NSString* userAgent = [self.commandDelegate userAgent]; + if (userAgent) { + [req setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + + for (NSString* headerName in headers) { + id value = [headers objectForKey:headerName]; + if (!value || (value == [NSNull null])) { + value = @"null"; + } + + // First, remove an existing header if one exists. + [req setValue:nil forHTTPHeaderField:headerName]; + + if (![value isKindOfClass:[NSArray class]]) { + value = [NSArray arrayWithObject:value]; + } + + // Then, append all header values. + for (id __strong subValue in value) { + // Convert from an NSNumber -> NSString. + if ([subValue respondsToSelector:@selector(stringValue)]) { + subValue = [subValue stringValue]; + } + if ([subValue isKindOfClass:[NSString class]]) { + [req addValue:subValue forHTTPHeaderField:headerName]; + } + } + } +} + +- (NSURLRequest*)requestForUploadCommand:(CDVInvokedUrlCommand*)command fileData:(NSData*)fileData +{ + // arguments order from js: [filePath, server, fileKey, fileName, mimeType, params, debug, chunkedMode] + // however, params is a JavaScript object and during marshalling is put into the options dict, + // thus debug and chunkedMode are the 6th and 7th arguments + NSString* target = [command argumentAtIndex:0]; + NSString* server = [command argumentAtIndex:1]; + NSString* fileKey = [command argumentAtIndex:2 withDefault:@"file"]; + NSString* fileName = [command argumentAtIndex:3 withDefault:@"no-filename"]; + NSString* mimeType = [command argumentAtIndex:4 withDefault:nil]; + NSDictionary* options = [command argumentAtIndex:5 withDefault:nil]; + // BOOL trustAllHosts = [[arguments objectAtIndex:6 withDefault:[NSNumber numberWithBool:YES]] boolValue]; // allow self-signed certs + BOOL chunkedMode = [[command argumentAtIndex:7 withDefault:[NSNumber numberWithBool:YES]] boolValue]; + NSDictionary* headers = [command argumentAtIndex:8 withDefault:nil]; + // Allow alternative http method, default to POST. JS side checks + // for allowed methods, currently PUT or POST (forces POST for + // unrecognised values) + NSString* httpMethod = [command argumentAtIndex:10 withDefault:@"POST"]; + CDVPluginResult* result = nil; + CDVFileTransferError errorCode = 0; + + // NSURL does not accepts URLs with spaces in the path. We escape the path in order + // to be more lenient. + NSURL* url = [NSURL URLWithString:server]; + + if (!url) { + errorCode = INVALID_URL_ERR; + NSLog(@"File Transfer Error: Invalid server URL %@", server); + } else if (!fileData) { + errorCode = FILE_NOT_FOUND_ERR; + } + + if (errorCode > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:errorCode AndSource:target AndTarget:server]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return nil; + } + + NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url]; + + [req setHTTPMethod:httpMethod]; + + // Magic value to set a cookie + if ([options objectForKey:kOptionsKeyCookie]) { + [req setValue:[options objectForKey:kOptionsKeyCookie] forHTTPHeaderField:@"Cookie"]; + [req setHTTPShouldHandleCookies:NO]; + } + + NSString* contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kFormBoundary]; + [req setValue:contentType forHTTPHeaderField:@"Content-Type"]; + [self applyRequestHeaders:headers toRequest:req]; + + NSData* formBoundaryData = [[NSString stringWithFormat:@"--%@\r\n", kFormBoundary] dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableData* postBodyBeforeFile = [NSMutableData data]; + + for (NSString* key in options) { + id val = [options objectForKey:key]; + if (!val || (val == [NSNull null]) || [key isEqualToString:kOptionsKeyCookie]) { + continue; + } + // if it responds to stringValue selector (eg NSNumber) get the NSString + if ([val respondsToSelector:@selector(stringValue)]) { + val = [val stringValue]; + } + // finally, check whether it is a NSString (for dataUsingEncoding selector below) + if (![val isKindOfClass:[NSString class]]) { + continue; + } + + [postBodyBeforeFile appendData:formBoundaryData]; + [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; + [postBodyBeforeFile appendData:[val dataUsingEncoding:NSUTF8StringEncoding]]; + [postBodyBeforeFile appendData:[@"\r\n" dataUsingEncoding : NSUTF8StringEncoding]]; + } + + [postBodyBeforeFile appendData:formBoundaryData]; + [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fileKey, fileName] dataUsingEncoding:NSUTF8StringEncoding]]; + if (mimeType != nil) { + [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n", mimeType] dataUsingEncoding:NSUTF8StringEncoding]]; + } + [postBodyBeforeFile appendData:[[NSString stringWithFormat:@"Content-Length: %d\r\n\r\n", [fileData length]] dataUsingEncoding:NSUTF8StringEncoding]]; + + DLog(@"fileData length: %d", [fileData length]); + NSData* postBodyAfterFile = [[NSString stringWithFormat:@"\r\n--%@--\r\n", kFormBoundary] dataUsingEncoding:NSUTF8StringEncoding]; + + NSUInteger totalPayloadLength = [postBodyBeforeFile length] + [fileData length] + [postBodyAfterFile length]; + [req setValue:[[NSNumber numberWithInteger:totalPayloadLength] stringValue] forHTTPHeaderField:@"Content-Length"]; + + if (chunkedMode) { + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + CFStreamCreateBoundPair(NULL, &readStream, &writeStream, kStreamBufferSize); + [req setHTTPBodyStream:CFBridgingRelease(readStream)]; + + self.backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskID]; + self.backgroundTaskID = UIBackgroundTaskInvalid; + NSLog(@"Background task to upload media finished."); + }]; + + [self.commandDelegate runInBackground:^{ + if (CFWriteStreamOpen(writeStream)) { + NSData* chunks[] = {postBodyBeforeFile, fileData, postBodyAfterFile}; + int numChunks = sizeof(chunks) / sizeof(chunks[0]); + + for (int i = 0; i < numChunks; ++i) { + CFIndex result = WriteDataToStream(chunks[i], writeStream); + if (result <= 0) { + break; + } + } + } else { + NSLog(@"FileTransfer: Failed to open writeStream"); + } + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + }]; + } else { + [postBodyBeforeFile appendData:fileData]; + [postBodyBeforeFile appendData:postBodyAfterFile]; + [req setHTTPBody:postBodyBeforeFile]; + } + return req; +} + +- (CDVFileTransferDelegate*)delegateForUploadCommand:(CDVInvokedUrlCommand*)command +{ + NSString* source = [command.arguments objectAtIndex:0]; + NSString* server = [command.arguments objectAtIndex:1]; + BOOL trustAllHosts = [[command.arguments objectAtIndex:6 withDefault:[NSNumber numberWithBool:YES]] boolValue]; // allow self-signed certs + NSString* objectId = [command.arguments objectAtIndex:9]; + + CDVFileTransferDelegate* delegate = [[CDVFileTransferDelegate alloc] init]; + + delegate.command = self; + delegate.callbackId = command.callbackId; + delegate.direction = CDV_TRANSFER_UPLOAD; + delegate.objectId = objectId; + delegate.source = source; + delegate.target = server; + delegate.trustAllHosts = trustAllHosts; + + return delegate; +} + +- (void)fileDataForUploadCommand:(CDVInvokedUrlCommand*)command +{ + NSString* target = (NSString*)[command.arguments objectAtIndex:0]; + NSError* __autoreleasing err = nil; + + // return unsupported result for assets-library URLs + if ([target hasPrefix:kCDVAssetsLibraryPrefix]) { + // Instead, we return after calling the asynchronous method and send `result` in each of the blocks. + ALAssetsLibraryAssetForURLResultBlock resultBlock = ^(ALAsset* asset) { + if (asset) { + // We have the asset! Get the data and send it off. + ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; + Byte* buffer = (Byte*)malloc([assetRepresentation size]); + NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil]; + NSData* fileData = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; + [self uploadData:fileData command:command]; + } else { + // We couldn't find the asset. Send the appropriate error. + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + } + }; + ALAssetsLibraryAccessFailureBlock failureBlock = ^(NSError* error) { + // Retrieving the asset failed for some reason. Send the appropriate error. + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsString:[error localizedDescription]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + }; + + ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; + [assetsLibrary assetForURL:[NSURL URLWithString:target] resultBlock:resultBlock failureBlock:failureBlock]; + return; + } else { + // Extract the path part out of a file: URL. + NSString* filePath = [target hasPrefix:@"/"] ? [target copy] : [[NSURL URLWithString:target] path]; + if (filePath == nil) { + // We couldn't find the asset. Send the appropriate error. + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsInt:NOT_FOUND_ERR]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + // Memory map the file so that it can be read efficiently even if it is large. + NSData* fileData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&err]; + + if (err != nil) { + NSLog(@"Error opening file %@: %@", target, err); + } + [self uploadData:fileData command:command]; + } +} + +- (void)upload:(CDVInvokedUrlCommand*)command +{ + // fileData and req are split into helper functions to ease the unit testing of delegateForUpload. + // First, get the file data. This method will call `uploadData:command`. + [self fileDataForUploadCommand:command]; +} + +- (void)uploadData:(NSData*)fileData command:(CDVInvokedUrlCommand*)command +{ + NSURLRequest* req = [self requestForUploadCommand:command fileData:fileData]; + + if (req == nil) { + return; + } + CDVFileTransferDelegate* delegate = [self delegateForUploadCommand:command]; + [NSURLConnection connectionWithRequest:req delegate:delegate]; + + if (activeTransfers == nil) { + activeTransfers = [[NSMutableDictionary alloc] init]; + } + + [activeTransfers setObject:delegate forKey:delegate.objectId]; +} + +- (void)abort:(CDVInvokedUrlCommand*)command +{ + NSString* objectId = [command.arguments objectAtIndex:0]; + + CDVFileTransferDelegate* delegate = [activeTransfers objectForKey:objectId]; + + if (delegate != nil) { + [delegate cancelTransfer:delegate.connection]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:CONNECTION_ABORTED AndSource:delegate.source AndTarget:delegate.target]]; + [self.commandDelegate sendPluginResult:result callbackId:delegate.callbackId]; + } +} + +- (void)download:(CDVInvokedUrlCommand*)command +{ + DLog(@"File Transfer downloading file..."); + NSString* sourceUrl = [command.arguments objectAtIndex:0]; + NSString* filePath = [command.arguments objectAtIndex:1]; + BOOL trustAllHosts = [[command.arguments objectAtIndex:2 withDefault:[NSNumber numberWithBool:YES]] boolValue]; // allow self-signed certs + NSString* objectId = [command.arguments objectAtIndex:3]; + NSDictionary* headers = [command.arguments objectAtIndex:4 withDefault:nil]; + + // return unsupported result for assets-library URLs + if ([filePath hasPrefix:kCDVAssetsLibraryPrefix]) { + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_MALFORMED_URL_EXCEPTION messageAsString:@"download not supported for assets-library URLs."]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + CDVPluginResult* result = nil; + CDVFileTransferError errorCode = 0; + + NSURL* file; + + if ([filePath hasPrefix:@"/"]) { + file = [NSURL fileURLWithPath:filePath]; + } else { + file = [NSURL URLWithString:filePath]; + } + + NSURL* url = [NSURL URLWithString:sourceUrl]; + + if (!url) { + errorCode = INVALID_URL_ERR; + NSLog(@"File Transfer Error: Invalid server URL %@", sourceUrl); + } else if (![file isFileURL]) { + errorCode = FILE_NOT_FOUND_ERR; + NSLog(@"File Transfer Error: Invalid file path or URL %@", filePath); + } + + if (errorCode > 0) { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[self createFileTransferError:errorCode AndSource:sourceUrl AndTarget:filePath]]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; + return; + } + + NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url]; + [self applyRequestHeaders:headers toRequest:req]; + + CDVFileTransferDelegate* delegate = [[CDVFileTransferDelegate alloc] init]; + delegate.command = self; + delegate.direction = CDV_TRANSFER_DOWNLOAD; + delegate.callbackId = command.callbackId; + delegate.objectId = objectId; + delegate.source = sourceUrl; + delegate.target = filePath; + delegate.trustAllHosts = trustAllHosts; + + delegate.connection = [NSURLConnection connectionWithRequest:req delegate:delegate]; + + if (activeTransfers == nil) { + activeTransfers = [[NSMutableDictionary alloc] init]; + } + + [activeTransfers setObject:delegate forKey:delegate.objectId]; +} + +- (NSMutableDictionary*)createFileTransferError:(int)code AndSource:(NSString*)source AndTarget:(NSString*)target +{ + NSMutableDictionary* result = [NSMutableDictionary dictionaryWithCapacity:3]; + + [result setObject:[NSNumber numberWithInt:code] forKey:@"code"]; + if (source != nil) { + [result setObject:source forKey:@"source"]; + } + if (target != nil) { + [result setObject:target forKey:@"target"]; + } + NSLog(@"FileTransferError %@", result); + + return result; +} + +- (NSMutableDictionary*)createFileTransferError:(int)code + AndSource:(NSString*)source + AndTarget:(NSString*)target + AndHttpStatus:(int)httpStatus + AndBody:(NSString*)body +{ + NSMutableDictionary* result = [NSMutableDictionary dictionaryWithCapacity:5]; + + [result setObject:[NSNumber numberWithInt:code] forKey:@"code"]; + if (source != nil) { + [result setObject:source forKey:@"source"]; + } + if (target != nil) { + [result setObject:target forKey:@"target"]; + } + [result setObject:[NSNumber numberWithInt:httpStatus] forKey:@"http_status"]; + if (body != nil) { + [result setObject:body forKey:@"body"]; + } + NSLog(@"FileTransferError %@", result); + + return result; +} + +- (void)onReset +{ + for (CDVFileTransferDelegate* delegate in [activeTransfers allValues]) { + [delegate.connection cancel]; + } + + [activeTransfers removeAllObjects]; +} + +@end + +@interface CDVFileTransferEntityLengthRequest : NSObject { + NSURLConnection* _connection; + CDVFileTransferDelegate* __weak _originalDelegate; +} + +- (CDVFileTransferEntityLengthRequest*)initWithOriginalRequest:(NSURLRequest*)originalRequest andDelegate:(CDVFileTransferDelegate*)originalDelegate; + +@end; + +@implementation CDVFileTransferEntityLengthRequest; + +- (CDVFileTransferEntityLengthRequest*)initWithOriginalRequest:(NSURLRequest*)originalRequest andDelegate:(CDVFileTransferDelegate*)originalDelegate +{ + if (self) { + DLog(@"Requesting entity length for GZIPped content..."); + + NSMutableURLRequest* req = [originalRequest mutableCopy]; + [req setHTTPMethod:@"HEAD"]; + [req setValue:@"identity" forHTTPHeaderField:@"Accept-Encoding"]; + + _originalDelegate = originalDelegate; + _connection = [NSURLConnection connectionWithRequest:req delegate:self]; + } + return self; +} + +- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response +{ + DLog(@"HEAD request returned; content-length is %lld", [response expectedContentLength]); + [_originalDelegate updateBytesExpected:[response expectedContentLength]]; +} + +- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data +{} + +- (void)connectionDidFinishLoading:(NSURLConnection*)connection +{} + +@end + +@implementation CDVFileTransferDelegate + +@synthesize callbackId, connection = _connection, source, target, responseData, command, bytesTransfered, bytesExpected, direction, responseCode, objectId, targetFileHandle; + +- (void)connectionDidFinishLoading:(NSURLConnection*)connection +{ + NSString* uploadResponse = nil; + NSString* downloadResponse = nil; + NSMutableDictionary* uploadResult; + CDVPluginResult* result = nil; + BOOL bDirRequest = NO; + CDVFile* file; + + NSLog(@"File Transfer Finished with response code %d", self.responseCode); + + if (self.direction == CDV_TRANSFER_UPLOAD) { + uploadResponse = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding]; + + if ((self.responseCode >= 200) && (self.responseCode < 300)) { + // create dictionary to return FileUploadResult object + uploadResult = [NSMutableDictionary dictionaryWithCapacity:3]; + if (uploadResponse != nil) { + [uploadResult setObject:uploadResponse forKey:@"response"]; + } + [uploadResult setObject:[NSNumber numberWithInt:self.bytesTransfered] forKey:@"bytesSent"]; + [uploadResult setObject:[NSNumber numberWithInt:self.responseCode] forKey:@"responseCode"]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:uploadResult]; + } else { + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:CONNECTION_ERR AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:uploadResponse]]; + } + } + if (self.direction == CDV_TRANSFER_DOWNLOAD) { + if (self.targetFileHandle) { + [self.targetFileHandle closeFile]; + self.targetFileHandle = nil; + DLog(@"File Transfer Download success"); + + file = [[CDVFile alloc] init]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:[file getDirectoryEntry:target isDirectory:bDirRequest]]; + } else { + downloadResponse = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding]; + result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:CONNECTION_ERR AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:downloadResponse]]; + } + } + + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; + + // remove connection for activeTransfers + [command.activeTransfers removeObjectForKey:objectId]; + + // remove background id task in case our upload was done in the background + [[UIApplication sharedApplication] endBackgroundTask:self.command.backgroundTaskID]; + self.command.backgroundTaskID = UIBackgroundTaskInvalid; +} + +- (void)removeTargetFile +{ + NSFileManager* fileMgr = [NSFileManager defaultManager]; + + [fileMgr removeItemAtPath:self.target error:nil]; +} + +- (void)cancelTransfer:(NSURLConnection*)connection +{ + [connection cancel]; + [self.command.activeTransfers removeObjectForKey:self.objectId]; + [self removeTargetFile]; +} + +- (void)cancelTransferWithError:(NSURLConnection*)connection errorMessage:(NSString*)errorMessage +{ + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION messageAsDictionary:[self.command createFileTransferError:FILE_NOT_FOUND_ERR AndSource:self.source AndTarget:self.target AndHttpStatus:self.responseCode AndBody:errorMessage]]; + + NSLog(@"File Transfer Error: %@", errorMessage); + [self cancelTransfer:connection]; + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response +{ + NSError* __autoreleasing error = nil; + + self.mimeType = [response MIMEType]; + self.targetFileHandle = nil; + + // required for iOS 4.3, for some reason; response is + // a plain NSURLResponse, not the HTTP subclass + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; + + self.responseCode = [httpResponse statusCode]; + self.bytesExpected = [response expectedContentLength]; + if ((self.direction == CDV_TRANSFER_DOWNLOAD) && (self.responseCode == 200) && (self.bytesExpected == NSURLResponseUnknownLength)) { + // Kick off HEAD request to server to get real length + // bytesExpected will be updated when that response is returned + self.entityLengthRequest = [[CDVFileTransferEntityLengthRequest alloc] initWithOriginalRequest:connection.currentRequest andDelegate:self]; + } + } else if ([response.URL isFileURL]) { + NSDictionary* attr = [[NSFileManager defaultManager] attributesOfItemAtPath:[response.URL path] error:nil]; + self.responseCode = 200; + self.bytesExpected = [attr[NSFileSize] longLongValue]; + } else { + self.responseCode = 200; + self.bytesExpected = NSURLResponseUnknownLength; + } + if ((self.direction == CDV_TRANSFER_DOWNLOAD) && (self.responseCode >= 200) && (self.responseCode < 300)) { + // Download response is okay; begin streaming output to file + NSString* parentPath = [self.target stringByDeletingLastPathComponent]; + + // create parent directories if needed + if ([[NSFileManager defaultManager] createDirectoryAtPath:parentPath withIntermediateDirectories:YES attributes:nil error:&error] == NO) { + if (error) { + [self cancelTransferWithError:connection errorMessage:[NSString stringWithFormat:@"Could not create path to save downloaded file: %@", [error localizedDescription]]]; + } else { + [self cancelTransferWithError:connection errorMessage:@"Could not create path to save downloaded file"]; + } + return; + } + // create target file + if ([[NSFileManager defaultManager] createFileAtPath:self.target contents:nil attributes:nil] == NO) { + [self cancelTransferWithError:connection errorMessage:@"Could not create target file"]; + return; + } + // open target file for writing + self.targetFileHandle = [NSFileHandle fileHandleForWritingAtPath:self.target]; + if (self.targetFileHandle == nil) { + [self cancelTransferWithError:connection errorMessage:@"Could not open target file for writing"]; + } + DLog(@"Streaming to file %@", target); + } +} + +- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error +{ + NSString* body = [[NSString alloc] initWithData:self.responseData encoding:NSUTF8StringEncoding]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:[command createFileTransferError:CONNECTION_ERR AndSource:source AndTarget:target AndHttpStatus:self.responseCode AndBody:body]]; + + NSLog(@"File Transfer Error: %@", [error localizedDescription]); + + [self cancelTransfer:connection]; + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; +} + +- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data +{ + self.bytesTransfered += data.length; + if (self.targetFileHandle) { + [self.targetFileHandle writeData:data]; + } else { + [self.responseData appendData:data]; + } + [self updateProgress]; +} + +- (void)updateBytesExpected:(NSInteger)newBytesExpected +{ + DLog(@"Updating bytesExpected to %d", newBytesExpected); + self.bytesExpected = newBytesExpected; + [self updateProgress]; +} + +- (void)updateProgress +{ + if (self.direction == CDV_TRANSFER_DOWNLOAD) { + BOOL lengthComputable = (self.bytesExpected != NSURLResponseUnknownLength); + // If the response is GZipped, and we have an outstanding HEAD request to get + // the length, then hold off on sending progress events. + if (!lengthComputable && (self.entityLengthRequest != nil)) { + return; + } + NSMutableDictionary* downloadProgress = [NSMutableDictionary dictionaryWithCapacity:3]; + [downloadProgress setObject:[NSNumber numberWithBool:lengthComputable] forKey:@"lengthComputable"]; + [downloadProgress setObject:[NSNumber numberWithInt:self.bytesTransfered] forKey:@"loaded"]; + [downloadProgress setObject:[NSNumber numberWithInt:self.bytesExpected] forKey:@"total"]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:downloadProgress]; + [result setKeepCallbackAsBool:true]; + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (void)connection:(NSURLConnection*)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite +{ + if (self.direction == CDV_TRANSFER_UPLOAD) { + NSMutableDictionary* uploadProgress = [NSMutableDictionary dictionaryWithCapacity:3]; + + [uploadProgress setObject:[NSNumber numberWithBool:true] forKey:@"lengthComputable"]; + [uploadProgress setObject:[NSNumber numberWithInt:totalBytesWritten] forKey:@"loaded"]; + [uploadProgress setObject:[NSNumber numberWithInt:totalBytesExpectedToWrite] forKey:@"total"]; + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:uploadProgress]; + [result setKeepCallbackAsBool:true]; + [self.command.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + self.bytesTransfered = totalBytesWritten; +} + +// for self signed certificates +- (void)connection:(NSURLConnection*)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge +{ + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + if (self.trustAllHosts) { + NSURLCredential* credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; + [challenge.sender useCredential:credential forAuthenticationChallenge:challenge]; + } + [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge]; + } else { + [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge]; + } +} + +- (id)init +{ + if ((self = [super init])) { + self.responseData = [NSMutableData data]; + self.targetFileHandle = nil; + } + return self; +} + +@end;