TNLExample/TNLXMultipartFormData.m (381 lines of code) (raw):

// // TNLMultipartFormData.m // TwitterNetworkLayer // // Created on 8/22/14. // Copyright © 2020 Twitter. All rights reserved. // #import "NSDictionary+TNLAdditions.h" #import "TNLError.h" #import "TNLHTTP.h" #import "TNLRequestHydrater.h" #import "TNLRequestOperation.h" #import "TNLTemporaryFile.h" #import "TNLXMultipartFormData.h" NSString * const TNLXMultipartFormDataErrorDomain = @"TNLXMultipartFormDataErrorDomain"; @interface TNLTemporaryFile : NSObject <TNLTemporaryFile> - (instancetype)init; + (instancetype)temporaryFileWithExistingFilePath:(NSString *)path error:(out NSError **)error; @property (nonatomic, readonly) NSString *path; @property (nonatomic, readonly, getter = isOpen) BOOL open; - (BOOL)consumeExistingFile:(NSString *)path error:(out NSError **)error; - (BOOL)close:(out NSError **)error; - (BOOL)open:(out NSError **)error; - (BOOL)appendData:(NSData *)data error:(out NSError **)error; - (BOOL)moveToPath:(NSString *)path error:(out NSError **)error; @end @interface TNLXMultipartFormDataRequestHydrater : NSObject <TNLRequestHydrater> @property (nonatomic, readonly) TNLXMultipartFormDataUploadFormat uploadFormat; - (instancetype)initWithUploadFormat:(TNLXMultipartFormDataUploadFormat)format; @end static NSError *TNLXMultipartFormDataErrorCreateWithCode(TNLXMultipartFormDataErrorCode code); static NSError *TNLXMultipartFormDataErrorCreateWithCodeAndUnderlyingError(TNLXMultipartFormDataErrorCode code, NSError *underlyingError); static NSError *TNLXMultipartFormDataErrorCreateWithCodeAndUserInfo(TNLXMultipartFormDataErrorCode code, NSDictionary *userInfo); NS_INLINE BOOL TNLXFormDataEntryAppendData(NSData *data, id dataOrTemporaryFile, NSError ** outError) { if ([dataOrTemporaryFile isKindOfClass:[TNLTemporaryFile class]]) { return [(TNLTemporaryFile *)dataOrTemporaryFile appendData:data error:outError]; } else { [(NSMutableData *)dataOrTemporaryFile appendData:data]; return YES; } } NS_INLINE BOOL TNLXFormDataEntryAppendString(NSString *string, id dataOrTemporaryFile, NSError ** outError) { return TNLXFormDataEntryAppendData([string dataUsingEncoding:NSUTF8StringEncoding], dataOrTemporaryFile, outError); } NS_INLINE BOOL TNLXFormDataEntryAppendFile(NSString *filePath, id dataOrTemporaryFile, NSError ** outError) { NSError *theError = nil; NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath]; if (!fileHandle) { theError = TNLXMultipartFormDataErrorCreateWithCodeAndUnderlyingError(TNLXMultipartFormDataErrorCodeInvalidFormDataEntry, [NSError errorWithDomain:NSPOSIXErrorDomain code:ENOENT userInfo:nil]); } else { do { @autoreleasepool { NSData *data = [fileHandle readDataOfLength:UINT16_MAX]; if (!data.length) { break; } TNLXFormDataEntryAppendData(data, dataOrTemporaryFile, &theError); } } while (!theError); [fileHandle closeFile]; } return !theError; } @interface TNLXMultipartFormDataUploadRequest : NSObject <TNLRequest> @property (nonatomic, readonly) NSURL *URL; @property (nonatomic, readonly) NSDictionary *allHTTPHeaderFields; - (instancetype)initWithURL:(NSURL *)url boundary:(NSString *)boundary headers:(NSDictionary *)headers; @end @interface TNLXMultipartFormDataUploadDataRequest : TNLXMultipartFormDataUploadRequest @property (nonatomic) NSData *HTTPBody; @end @interface TNLXMultipartFormDataUploadFileRequest : TNLXMultipartFormDataUploadRequest @property (nonatomic) TNLTemporaryFile *temporaryFile; @property (nonatomic, readonly) NSString *HTTPBodyFilePath; @end @interface TNLXFormDataEntry (Protected) - (BOOL)appendToDataOrTemporaryFile:(id)dataOrTemporaryFile withBoundary:(NSString *)boundary error:(out NSError **)error; @end @implementation TNLXMultipartFormDataRequest { NSMutableArray *_formDataObjects; } - (instancetype)init { self = [super init]; if (self) { _formDataObjects = [[NSMutableArray alloc] init]; } return self; } - (NSString *)boundary { static char sBoundaryCharacters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; if (!_boundary) { _boundary = [NSString stringWithFormat:@"TNLX.multipart.boundary-%c%c%c%c%c%c%c%c%c%c%c%c", sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))], sBoundaryCharacters[arc4random_uniform(sizeof(sBoundaryCharacters))]]; } return _boundary; } - (TNLHTTPMethod)HTTPMethodValue { return TNLHTTPMethodPOST; } - (void)addFormData:(TNLXFormDataEntry *)formData { [_formDataObjects addObject:formData]; } - (NSUInteger)formDataCount { return _formDataObjects.count; } - (id<TNLRequest>)generateRequestWithUploadFormat:(TNLXMultipartFormDataUploadFormat)uploadFormat error:(out NSError **)error { NSError *theError = nil; id<TNLRequest> request = nil; NSString *boundary = self.boundary; NSCharacterSet *charSet = [[NSCharacterSet characterSetWithCharactersInString:@"01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.-_abcdefghijklmnopqrstuvwxyz"] invertedSet]; if ([boundary rangeOfCharacterFromSet:charSet].length == 0) { NSArray *formDataItems = [_formDataObjects copy]; NSURL *url = self.URL; NSDictionary *headers = self.allHTTPHeaderFields; NSMutableData *data = nil; TNLTemporaryFile *tempFile = nil; if (TNLXMultipartFormDataUploadFormatFile == uploadFormat) { tempFile = [[TNLTemporaryFile alloc] init]; [tempFile open:&theError]; } else { data = [[NSMutableData alloc] init]; } if (!theError) { for (TNLXFormDataEntry *formDataItem in formDataItems) { if (![formDataItem appendToDataOrTemporaryFile:(tempFile ?: data) withBoundary:boundary error:&theError]) { break; } } } if (!theError) { TNLXFormDataEntryAppendString([NSString stringWithFormat:@"--%@--\r\n", boundary], (tempFile ?: data), &theError); } [tempFile close:(theError) ? NULL : &theError]; if (!theError) { switch (uploadFormat) { case TNLXMultipartFormDataUploadFormatFile: request = [[TNLXMultipartFormDataUploadFileRequest alloc] initWithURL:url boundary:boundary headers:headers]; [(TNLXMultipartFormDataUploadFileRequest *)request setTemporaryFile:tempFile]; break; case TNLXMultipartFormDataUploadFormatData: request = [[TNLXMultipartFormDataUploadDataRequest alloc] initWithURL:url boundary:boundary headers:headers]; [(TNLXMultipartFormDataUploadDataRequest *)request setHTTPBody:data]; break; default: assert(false); break; } } } else { theError = TNLXMultipartFormDataErrorCreateWithCodeAndUserInfo(TNLXMultipartFormDataErrorCodeInvalidBoundary, @{ @"boundary" : boundary }); } assert(theError || request); if (error) { *error = theError; } return (theError) ? nil : request; } - (id)copyWithZone:(NSZone *)zone { TNLXMultipartFormDataRequest *copy = [[TNLXMultipartFormDataRequest alloc] init]; copy->_formDataObjects = [_formDataObjects mutableCopy]; copy.boundary = self.boundary; copy.URL = self.URL; copy.allHTTPHeaderFields = self.allHTTPHeaderFields; return copy; } + (id<TNLRequestHydrater>)multipartFormDataRequestHydraterForUploadFormat:(TNLXMultipartFormDataUploadFormat)uploadFormat { return [[TNLXMultipartFormDataRequestHydrater alloc] initWithUploadFormat:uploadFormat]; } @end @implementation TNLXFormDataEntry - (instancetype)initWithFilePath:(NSString *)filePath data:(NSData *)data name:(NSString *)name type:(NSString *)type fileName:(NSString *)fileName { if (self = [super init]) { _filePath = [filePath copy]; _data = data; _name = [name copy]; _type = [type copy]; _fileName = [fileName copy]; } return self; } - (id)copyWithZone:(NSZone *)zone { TNLXFormDataEntry *formData = [[TNLXFormDataEntry alloc] initWithFilePath:self.filePath data:self.data name:self.name type:self.type fileName:self.fileName]; return formData; } + (instancetype)formDataWithData:(NSData *)data name:(NSString *)name { return [[TNLXFormDataEntry alloc] initWithFilePath:nil data:data name:name type:nil fileName:nil]; } + (instancetype)formDataWithData:(NSData *)data name:(NSString *)name type:(NSString *)type fileName:(NSString *)fileName { return [[TNLXFormDataEntry alloc] initWithFilePath:nil data:data name:name type:type fileName:fileName]; } + (instancetype)formDataWithFile:(NSString *)filePath name:(NSString *)name type:(NSString *)type fileName:(NSString *)fileName { return [[TNLXFormDataEntry alloc] initWithFilePath:filePath data:nil name:name type:type fileName:fileName]; } @end @implementation TNLXFormDataEntry (SpecificFormData) + (instancetype)formDataWithJPEGFile:(NSString *)filePath name:(NSString *)name fileName:(NSString *)fileName { return [[TNLXFormDataEntry alloc] initWithFilePath:filePath data:nil name:name type:TNLHTTPContentTypeJPEGImage fileName:fileName]; } + (instancetype)formDataWithJPEGData:(NSData *)data name:(NSString *)name fileName:(NSString *)fileName { return [[TNLXFormDataEntry alloc] initWithFilePath:nil data:data name:name type:TNLHTTPContentTypeJPEGImage fileName:fileName]; } + (instancetype)formDataWithQuicktimeVideoFile:(NSString *)filePath name:(NSString *)name fileName:(NSString *)fileName { return [[TNLXFormDataEntry alloc] initWithFilePath:filePath data:nil name:name type:TNLHTTPContentTypeQuicktimeVideo fileName:fileName]; } + (instancetype)formDataWithText:(NSString *)text name:(NSString *)name { return [[TNLXFormDataEntry alloc] initWithFilePath:nil data:[text dataUsingEncoding:NSUTF8StringEncoding] name:name type:nil fileName:nil]; } + (instancetype)formDataWithJSONFile:(NSString *)filePath name:(NSString *)name fileName:(NSString *)fileName { return [[TNLXFormDataEntry alloc] initWithFilePath:filePath data:nil name:name type:TNLHTTPContentTypeJSON fileName:fileName]; } + (instancetype)formDataWithJSONData:(NSData *)data name:(NSString *)name fileName:(NSString *)fileName { return [[TNLXFormDataEntry alloc] initWithFilePath:nil data:data name:name type:TNLHTTPContentTypeJSON fileName:fileName]; } + (instancetype)formDataWithJSONObject:(id)object name:(NSString *)name fileName:(NSString *)fileName { return [[TNLXFormDataEntry alloc] initWithFilePath:nil data:[NSJSONSerialization dataWithJSONObject:object options:0 error:NULL] name:name type:TNLHTTPContentTypeJSON fileName:fileName]; } @end @implementation TNLXFormDataEntry (Protected) - (BOOL)appendToDataOrTemporaryFile:(id)dataOrTemporaryFile withBoundary:(NSString *)boundary error:(out NSError *__autoreleasing *)error { NSError *theError = nil; NSString *name = self.name; NSString *contentType = self.type; NSString *fileName = self.fileName; NSString *filePath = self.filePath; NSData *data = self.data; if (!name) { theError = TNLXMultipartFormDataErrorCreateWithCode(TNLXMultipartFormDataErrorCodeInvalidFormDataEntry); } else if (!!contentType ^ !!fileName) { theError = TNLXMultipartFormDataErrorCreateWithCode(TNLXMultipartFormDataErrorCodeInvalidFormDataEntry); } else if (!(!!filePath ^ !!data)) { theError = TNLXMultipartFormDataErrorCreateWithCode(TNLXMultipartFormDataErrorCodeInvalidFormDataEntry); } if (!theError) { TNLXFormDataEntryAppendString(@"--", dataOrTemporaryFile, &theError); } if (!theError) { TNLXFormDataEntryAppendString(boundary, dataOrTemporaryFile, &theError); } if (!theError) { TNLXFormDataEntryAppendString(@"\r\n", dataOrTemporaryFile, &theError); } if (!theError) { TNLXFormDataEntryAppendString(@"Content-Disposition: form-data; name=\"", dataOrTemporaryFile, &theError); } if (!theError) { TNLXFormDataEntryAppendString(name, dataOrTemporaryFile, &theError); } if (!theError) { TNLXFormDataEntryAppendString(@"\"", dataOrTemporaryFile, &theError); } if (contentType) { if (!theError) { TNLXFormDataEntryAppendString(@"; filename=\"", dataOrTemporaryFile, &theError); } if (!theError) { assert(fileName); TNLXFormDataEntryAppendString(fileName, dataOrTemporaryFile, &theError); } if (!theError) { TNLXFormDataEntryAppendString(@"\"\r\nContent-Type: ", dataOrTemporaryFile, &theError); } if (!theError) { TNLXFormDataEntryAppendString(contentType, dataOrTemporaryFile, &theError); } } if (!theError) { TNLXFormDataEntryAppendString(@"\r\n\r\n", dataOrTemporaryFile, &theError); } if (filePath) { if (!theError) { TNLXFormDataEntryAppendFile(filePath, dataOrTemporaryFile, &theError); } } else { if (!theError) { assert(data); TNLXFormDataEntryAppendData(data, dataOrTemporaryFile, &theError); } } if (!theError) { TNLXFormDataEntryAppendString(@"\r\n", dataOrTemporaryFile, &theError); } if (error) { *error = theError; } return !theError; } @end @implementation TNLXMultipartFormDataUploadRequest - (instancetype)initWithURL:(NSURL *)url boundary:(NSString *)boundary headers:(NSDictionary *)headers { if (self = [super init]) { _URL = url; NSMutableDictionary *mHeaders = [headers mutableCopy] ?: [NSMutableDictionary dictionary]; [mHeaders tnl_setObject:[NSString stringWithFormat:@"%@; boundary=%@", TNLHTTPContentTypeMultipartFormData, boundary] forCaseInsensitiveKey:@"Content-Type"]; _allHTTPHeaderFields = [mHeaders copy]; } return self; } - (TNLHTTPMethod)HTTPMethodValue { return TNLHTTPMethodPOST; } - (id)copyWithZone:(NSZone *)zone { return self; } @end @implementation TNLXMultipartFormDataUploadDataRequest @end @implementation TNLXMultipartFormDataUploadFileRequest - (void)setTemporaryFile:(TNLTemporaryFile *)temporaryFile { if (temporaryFile != _temporaryFile) { _temporaryFile = temporaryFile; _HTTPBodyFilePath = temporaryFile.path; } } @end @implementation TNLXMultipartFormDataRequestHydrater - (instancetype)init { return [self initWithUploadFormat:TNLXMultipartFormDataUploadFormatDefault]; } - (instancetype)initWithUploadFormat:(TNLXMultipartFormDataUploadFormat)format { if (self = [super init]) { _uploadFormat = format; } return self; } - (void)tnl_requestOperation:(TNLRequestOperation *)op hydrateRequest:(id<TNLRequest>)request completion:(TNLRequestHydrateCompletionBlock)complete { NSError *error; if ([request isKindOfClass:[TNLXMultipartFormDataRequest class]]) { request = [(TNLXMultipartFormDataRequest *)request generateRequestWithUploadFormat:self.uploadFormat error:&error]; } complete(request, error); } @end static NSError *TNLXMultipartFormDataErrorCreateWithCode(TNLXMultipartFormDataErrorCode code) { return TNLXMultipartFormDataErrorCreateWithCodeAndUserInfo(code, nil); } static NSError *TNLXMultipartFormDataErrorCreateWithCodeAndUnderlyingError(TNLXMultipartFormDataErrorCode code, NSError *underlyingError) { assert(underlyingError); return TNLXMultipartFormDataErrorCreateWithCodeAndUserInfo(code, @{ NSUnderlyingErrorKey : underlyingError }); } static NSError *TNLXMultipartFormDataErrorCreateWithCodeAndUserInfo(TNLXMultipartFormDataErrorCode code, NSDictionary *userInfo) { return [NSError errorWithDomain:TNLErrorDomain code:code userInfo:userInfo]; }