diff --git a/AFNetworking/AFRestClient.h b/AFNetworking/AFRestClient.h index 0b4ac17..f420292 100644 --- a/AFNetworking/AFRestClient.h +++ b/AFNetworking/AFRestClient.h @@ -23,7 +23,7 @@ #import #import "AFHTTPRequestOperation.h" -#import "NSMutableURLRequest+AFNetworking.h" +@protocol AFMultipartFormDataProxy; @interface AFRestClient : NSObject { @private @@ -112,6 +112,12 @@ - (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters; +- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method + path:(NSString *)path + parameters:(NSDictionary *)parameters + constructingBodyWithBlock:(void (^)(id formData))block; + + ///-------------------------------- /// @name Enqueuing HTTP Operations ///-------------------------------- @@ -132,9 +138,9 @@ ///--------------------------- /// @name Making HTTP Requests ///--------------------------- -- (void)getPath:(NSString *)path - parameters:(NSDictionary *)parameters - success:(void (^)(id response))success +- (void)getPath:(NSString *)path + parameters:(NSDictionary *)parameters + success:(void (^)(id response))success failure:(void (^)(NSError *error))failure; - (void)postPath:(NSString *)path @@ -152,3 +158,13 @@ success:(void (^)(id response))success failure:(void (^)(NSError *error))failure; @end + +#pragma mark - + +@protocol AFMultipartFormDataProxy +- (void)appendPartWithHeaders:(NSDictionary *)headers body:(NSData *)body; +- (void)appendPartWithFormData:(NSData *)data name:(NSString *)name; +- (void)appendPartWithFile:(NSURL *)fileURL fileName:(NSString *)fileNameOrNil; +- (void)appendData:(NSData *)data; +- (void)appendString:(NSString *)string; +@end diff --git a/AFNetworking/AFRestClient.m b/AFNetworking/AFRestClient.m index ff72495..210aa9d 100644 --- a/AFNetworking/AFRestClient.m +++ b/AFNetworking/AFRestClient.m @@ -23,6 +23,31 @@ #import "AFRestClient.h" #import "AFJSONRequestOperation.h" +static NSString * const kAFMultipartFormLineDelimiter = @"\r\n"; // CRLF +static NSString * const kAFMultipartFormBoundary = @"Boundary+0xAbCdEfGbOuNdArY"; + +static NSString * AFMultipartFormEncapsulationBoundary() { + return [NSString stringWithFormat:@"--%@", kAFMultipartFormBoundary]; +} + +static NSString * AFMultipartFormFinalBoundary() { + return [NSString stringWithFormat:@"--%@--", kAFMultipartFormBoundary]; +} + +@interface AFMutableMultipartFormData : NSObject { +@private + NSStringEncoding _stringEncoding; + NSMutableArray *_lines; +} + +- (id)initWithStringEncoding:(NSStringEncoding)encoding; + +- (NSData *)data; + +@end + +#pragma mark - + static NSString * AFBase64EncodedStringFromString(NSString *string) { NSData *data = [NSData dataWithBytes:[string UTF8String] length:[string length]]; NSUInteger length = [data length]; @@ -161,6 +186,42 @@ - (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)p return request; } +- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method + path:(NSString *)path + parameters:(NSDictionary *)parameters + constructingBodyWithBlock:(void (^)(id formData))block +{ + if (!([method isEqualToString:@"POST"] || [method isEqualToString:@"PUT"] || [method isEqualToString:@"DELETE"])) { + [NSException raise:@"Invalid HTTP Method" format:@"%@ is not supported for multipart form requests; must be either POST, PUT, or DELETE", method]; + return nil; + } + + NSMutableURLRequest *request = [self requestWithMethod:method path:path parameters:nil]; + __block AFMutableMultipartFormData *formData = [[AFMutableMultipartFormData alloc] init]; + + id key = nil; + NSEnumerator *enumerator = [parameters keyEnumerator]; + while ((key = [enumerator nextObject])) { + id value = [parameters valueForKey:key]; + if (![value isKindOfClass:[NSData class]]) { + value = [value description]; + } + + [formData appendPartWithFormData:[value dataUsingEncoding:self.stringEncoding] name:[key description]]; + } + + if (block) { + block(formData); + } + + [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", kAFMultipartFormBoundary] forHTTPHeaderField:@"Content-Type"]; + [request setHTTPBody:[formData data]]; + + [formData autorelease]; + + return request; +} + - (void)enqueueHTTPOperation:(AFHTTPRequestOperation *)operation { [self.operationQueue addOperation:operation]; } @@ -209,3 +270,80 @@ - (void)deletePath:(NSString *)path parameters:(NSDictionary *)parameters succes } @end + +#pragma mark - + +// multipart/form-data; see http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.2 +@interface AFMutableMultipartFormData () +@property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding; +@property (readwrite, nonatomic, retain) NSMutableArray *lines; + +- (void)appendBlankLine; +@end + +@implementation AFMutableMultipartFormData +@synthesize stringEncoding = _stringEncoding; +@synthesize lines = _lines; + +- (id)initWithStringEncoding:(NSStringEncoding)encoding { + self = [super init]; + if (!self) { + return nil; + } + + self.stringEncoding = encoding; + self.lines = [NSMutableArray array]; + + return self; +} + +- (void)dealloc { + [_lines release]; + [super dealloc]; +} + +- (NSData *)data { + NSLog(@"DATA: %@", [[self.lines componentsJoinedByString:kAFMultipartFormLineDelimiter] stringByAppendingString:AFMultipartFormFinalBoundary()]); + + return [[[[self.lines componentsJoinedByString:kAFMultipartFormLineDelimiter] stringByAppendingString:AFMultipartFormFinalBoundary()] stringByAppendingString:kAFMultipartFormLineDelimiter] dataUsingEncoding:self.stringEncoding]; +} + +#pragma mark - AFMultipartFormDataProxy + +- (void)appendPartWithHeaders:(NSDictionary *)headers body:(NSData *)body { + for (NSString *field in [headers allKeys]) { + [self appendString:[NSString stringWithFormat:@"%@: %@", field, [headers valueForKey:field]]]; + } + + [self appendBlankLine]; + [self appendData:body]; +} + +- (void)appendPartWithFormData:(NSData *)data name:(NSString *)name { + [self appendPartWithHeaders:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"] body:data]; +} + +- (void)appendPartWithFile:(NSURL *)fileURL fileName:(NSString *)fileNameOrNil { + if (![fileURL isFileURL]) { + [NSException raise:@"Invalid fileURL value" format:@"%@ must be a valid file URL", fileURL]; + return; + } + + NSData *data = [NSData dataWithContentsOfFile:[fileURL absoluteString]]; + NSString *fileName = fileNameOrNil ? fileNameOrNil : [[fileURL lastPathComponent] stringByAppendingPathExtension:[fileURL pathExtension]]; + [self appendPartWithHeaders:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"file; filename=\"%@\"", fileName] forKey:@"Content-Disposition"] body:data]; +} + +- (void)appendData:(NSData *)data { + [self appendString:[[[NSString alloc] initWithData:data encoding:self.stringEncoding] autorelease]]; +} + +- (void)appendString:(NSString *)string { + [self.lines addObject:string]; +} + +- (void)appendBlankLine { + [self appendString:@""]; +} + +@end diff --git a/AFNetworking/NSMutableURLRequest+AFNetworking.h b/AFNetworking/NSMutableURLRequest+AFNetworking.h deleted file mode 100644 index 7823f12..0000000 --- a/AFNetworking/NSMutableURLRequest+AFNetworking.h +++ /dev/null @@ -1,33 +0,0 @@ -// NSMutableURLRequest+AFNetworking.h -// -// Copyright (c) 2011 Gowalla (http://gowalla.com/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#import - -@interface NSMutableURLRequest (AFNetworking) - -- (void)setHTTPBodyWithData:(NSData *)data - mimeType:(NSString *)mimeType - forParameterNamed:(NSString *)parameterName - parameters:(NSDictionary *)parameters - useGzipCompression:(BOOL)useGzipCompression; - -@end diff --git a/AFNetworking/NSMutableURLRequest+AFNetworking.m b/AFNetworking/NSMutableURLRequest+AFNetworking.m deleted file mode 100644 index 3d02670..0000000 --- a/AFNetworking/NSMutableURLRequest+AFNetworking.m +++ /dev/null @@ -1,76 +0,0 @@ -// NSMutableURLRequest+AFNetworking.m -// -// Copyright (c) 2011 Gowalla (http://gowalla.com/) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#import "NSMutableURLRequest+AFNetworking.h" -#import "NSData+AFNetworking.h" - -@implementation NSMutableURLRequest (AFNetworking) - -- (void)setHTTPBodyWithData:(NSData *)data - mimeType:(NSString *)mimeType - forParameterNamed:(NSString *)parameterName - parameters:(NSDictionary *)parameters - useGzipCompression:(BOOL)useGzipCompression -{ - if ([[self HTTPMethod] isEqualToString:@"GET"]) { - [self setHTTPMethod:@"POST"]; - } - - NSString *filename = [[NSString stringWithFormat:@"%d", [[NSDate date] hash]] stringByAppendingPathExtension:[mimeType lastPathComponent]]; - - static NSString * const boundary = @"----Boundary+0xAbCdEfGbOuNdArY"; - [self setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"]; - - NSMutableData *mutableData = [NSMutableData data]; - [mutableData appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - - id key; - NSEnumerator *enumerator = [parameters keyEnumerator]; - while ((key = [enumerator nextObject])) { - [mutableData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; - [mutableData appendData:[[NSString stringWithFormat:@"%@", [parameters valueForKey:key]] dataUsingEncoding:NSUTF8StringEncoding]]; - [mutableData appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - } - - [mutableData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"", parameterName, filename, nil] dataUsingEncoding:NSUTF8StringEncoding]]; - [mutableData appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; - [mutableData appendData:[[NSString stringWithFormat:@"Content-Type: %@", mimeType] dataUsingEncoding:NSUTF8StringEncoding]]; - [mutableData appendData:[@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; - [mutableData appendData:data]; - [mutableData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; - - if (useGzipCompression) { - NSError *error = nil; - NSData *compressedData = [mutableData dataByGZipCompressingWithError:&error]; - - if (!error && compressedData) { - [self setHTTPBody:compressedData]; - - // Content-Encoding HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11 - [self setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"]; - } - } else { - [self setHTTPBody:mutableData]; - } -} - -@end diff --git a/Example/AFNetworking Example.xcodeproj/project.pbxproj b/Example/AFNetworking Example.xcodeproj/project.pbxproj index 08cc028..f8a66cf 100644 --- a/Example/AFNetworking Example.xcodeproj/project.pbxproj +++ b/Example/AFNetworking Example.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - F85CE2DC13EC4A4200BFAE01 /* NSMutableURLRequest+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = F85CE2DB13EC4A4200BFAE01 /* NSMutableURLRequest+AFNetworking.m */; }; F874B5D913E0AA6500B28E3E /* AFHTTPRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = F874B5C913E0AA6500B28E3E /* AFHTTPRequestOperation.m */; }; F874B5DA13E0AA6500B28E3E /* AFImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = F874B5CA13E0AA6500B28E3E /* AFImageCache.m */; }; F874B5DB13E0AA6500B28E3E /* AFImageRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = F874B5CB13E0AA6500B28E3E /* AFImageRequestOperation.m */; }; @@ -34,8 +33,6 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - F85CE2DA13EC4A4200BFAE01 /* NSMutableURLRequest+AFNetworking.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSMutableURLRequest+AFNetworking.h"; path = "../AFNetworking/NSMutableURLRequest+AFNetworking.h"; sourceTree = ""; }; - F85CE2DB13EC4A4200BFAE01 /* NSMutableURLRequest+AFNetworking.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSMutableURLRequest+AFNetworking.m"; path = "../AFNetworking/NSMutableURLRequest+AFNetworking.m"; sourceTree = ""; }; F874B5C913E0AA6500B28E3E /* AFHTTPRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AFHTTPRequestOperation.m; path = ../AFNetworking/AFHTTPRequestOperation.m; sourceTree = ""; }; F874B5CA13E0AA6500B28E3E /* AFImageCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AFImageCache.m; path = ../AFNetworking/AFImageCache.m; sourceTree = ""; }; F874B5CB13E0AA6500B28E3E /* AFImageRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AFImageRequestOperation.m; path = ../AFNetworking/AFImageRequestOperation.m; sourceTree = ""; }; @@ -101,8 +98,6 @@ F85CE2D613EC47BC00BFAE01 /* Categories */ = { isa = PBXGroup; children = ( - F85CE2DA13EC4A4200BFAE01 /* NSMutableURLRequest+AFNetworking.h */, - F85CE2DB13EC4A4200BFAE01 /* NSMutableURLRequest+AFNetworking.m */, F874B5D713E0AA6500B28E3E /* UIImage+AFNetworking.h */, F874B5CF13E0AA6500B28E3E /* UIImage+AFNetworking.m */, F874B5D813E0AA6500B28E3E /* UIImageView+AFNetworking.h */, @@ -348,7 +343,6 @@ F874B5DE13E0AA6500B28E3E /* AFRestClient.m in Sources */, F874B5DF13E0AA6500B28E3E /* UIImage+AFNetworking.m in Sources */, F874B5E013E0AA6500B28E3E /* UIImageView+AFNetworking.m in Sources */, - F85CE2DC13EC4A4200BFAE01 /* NSMutableURLRequest+AFNetworking.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };