2 // ASIFormDataRequest.m
3 // Part of ASIHTTPRequest -> http://allseeing-i.com/ASIHTTPRequest
5 // Created by Ben Copsey on 07/11/2008.
6 // Copyright 2008-2009 All-Seeing Interactive. All rights reserved.
9 #import "ASIFormDataRequest.h"
13 @interface ASIFormDataRequest ()
14 - (void)buildMultipartFormDataPostBody;
15 - (void)buildURLEncodedPostBody;
16 - (void)appendPostString:(NSString *)string;
18 @property (retain) NSMutableArray *postData;
19 @property (retain) NSMutableArray *fileData;
21 #if DEBUG_FORM_DATA_REQUEST
22 - (void)addToDebugBody:(NSString *)string;
23 @property (retain, nonatomic) NSString *debugBodyString;
28 @implementation ASIFormDataRequest
30 #pragma mark utilities
31 - (NSString*)encodeURL:(NSString *)string
33 NSString *newString = NSMakeCollectable([(NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)string, NULL, CFSTR(":/?#[]@!$ &'()*+,;=\"<>%{}|\\^~`"), CFStringConvertNSStringEncodingToEncoding([self stringEncoding])) autorelease]);
40 #pragma mark init / dealloc
42 + (id)requestWithURL:(NSURL *)newURL
44 return [[[self alloc] initWithURL:newURL] autorelease];
47 - (id)initWithURL:(NSURL *)newURL
49 self = [super initWithURL:newURL];
50 [self setPostFormat:ASIURLEncodedPostFormat];
51 [self setStringEncoding:NSUTF8StringEncoding];
57 #if DEBUG_FORM_DATA_REQUEST
58 [debugBodyString release];
66 #pragma mark setup request
68 - (void)addPostValue:(id <NSObject>)value forKey:(NSString *)key
70 if (![self postData]) {
71 [self setPostData:[NSMutableArray array]];
73 [[self postData] addObject:[NSDictionary dictionaryWithObjectsAndKeys:[value description],@"value",key,@"key",nil]];
76 - (void)setPostValue:(id <NSObject>)value forKey:(NSString *)key
78 // Remove any existing value
80 for (i=0; i<[[self postData] count]; i++) {
81 NSDictionary *val = [[self postData] objectAtIndex:i];
82 if ([[val objectForKey:@"key"] isEqualToString:key]) {
83 [[self postData] removeObjectAtIndex:i];
87 [self addPostValue:value forKey:key];
91 - (void)addFile:(NSString *)filePath forKey:(NSString *)key
93 [self addFile:filePath withFileName:nil andContentType:nil forKey:key];
96 - (void)addFile:(id)data withFileName:(NSString *)fileName andContentType:(NSString *)contentType forKey:(NSString *)key
98 if (![self fileData]) {
99 [self setFileData:[NSMutableArray array]];
102 // If data is a path to a local file
103 if ([data isKindOfClass:[NSString class]]) {
104 BOOL isDirectory = NO;
105 BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:(NSString *)data isDirectory:&isDirectory];
106 if (!fileExists || isDirectory) {
107 [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIInternalErrorWhileBuildingRequestType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"No file exists at %@",data],NSLocalizedDescriptionKey,nil]]];
110 // If the caller didn't specify a custom file name, we'll use the file name of the file we were passed
112 fileName = [(NSString *)data lastPathComponent];
115 // If we were given the path to a file, and the user didn't specify a mime type, we can detect it from the file extension
117 contentType = [ASIHTTPRequest mimeTypeForFileAtPath:data];
121 NSDictionary *fileInfo = [NSDictionary dictionaryWithObjectsAndKeys:data, @"data", contentType, @"contentType", fileName, @"fileName", key, @"key", nil];
122 [[self fileData] addObject:fileInfo];
126 - (void)setFile:(NSString *)filePath forKey:(NSString *)key
128 [self setFile:filePath withFileName:nil andContentType:nil forKey:key];
131 - (void)setFile:(id)data withFileName:(NSString *)fileName andContentType:(NSString *)contentType forKey:(NSString *)key
133 // Remove any existing value
135 for (i=0; i<[[self fileData] count]; i++) {
136 NSDictionary *val = [[self fileData] objectAtIndex:i];
137 if ([[val objectForKey:@"key"] isEqualToString:key]) {
138 [[self fileData] removeObjectAtIndex:i];
142 [self addFile:data withFileName:fileName andContentType:contentType forKey:key];
145 - (void)addData:(NSData *)data forKey:(NSString *)key
147 [self addData:data withFileName:@"file" andContentType:nil forKey:key];
150 - (void)addData:(id)data withFileName:(NSString *)fileName andContentType:(NSString *)contentType forKey:(NSString *)key
152 if (![self fileData]) {
153 [self setFileData:[NSMutableArray array]];
156 contentType = @"application/octet-stream";
159 NSDictionary *fileInfo = [NSDictionary dictionaryWithObjectsAndKeys:data, @"data", contentType, @"contentType", fileName, @"fileName", key, @"key", nil];
160 [[self fileData] addObject:fileInfo];
163 - (void)setData:(NSData *)data forKey:(NSString *)key
165 [self setData:data withFileName:@"file" andContentType:nil forKey:key];
168 - (void)setData:(id)data withFileName:(NSString *)fileName andContentType:(NSString *)contentType forKey:(NSString *)key
170 // Remove any existing value
172 for (i=0; i<[[self fileData] count]; i++) {
173 NSDictionary *val = [[self fileData] objectAtIndex:i];
174 if ([[val objectForKey:@"key"] isEqualToString:key]) {
175 [[self fileData] removeObjectAtIndex:i];
179 [self addData:data withFileName:fileName andContentType:contentType forKey:key];
182 - (void)buildPostBody
184 if ([self haveBuiltPostBody]) {
188 #if DEBUG_FORM_DATA_REQUEST
189 [self setDebugBodyString:@""];
192 if (![self postData] && ![self fileData]) {
193 [super buildPostBody];
196 if ([[self fileData] count] > 0) {
197 [self setShouldStreamPostDataFromDisk:YES];
200 if ([self postFormat] == ASIURLEncodedPostFormat) {
201 [self buildURLEncodedPostBody];
203 [self buildMultipartFormDataPostBody];
206 [super buildPostBody];
208 #if DEBUG_FORM_DATA_REQUEST
209 NSLog(@"%@",[self debugBodyString]);
210 [self setDebugBodyString:nil];
215 - (void)buildMultipartFormDataPostBody
217 #if DEBUG_FORM_DATA_REQUEST
218 [self addToDebugBody:@"\r\n==== Building a multipart/form-data body ====\r\n"];
221 NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding([self stringEncoding]));
223 // Set your own boundary string only if really obsessive. We don't bother to check if post data contains the boundary, since it's pretty unlikely that it does.
224 NSString *stringBoundary = @"0xKhTmLbOuNdArY";
226 [self addRequestHeader:@"Content-Type" value:[NSString stringWithFormat:@"multipart/form-data; charset=%@; boundary=%@", charset, stringBoundary]];
228 [self appendPostString:[NSString stringWithFormat:@"--%@\r\n",stringBoundary]];
231 NSString *endItemBoundary = [NSString stringWithFormat:@"\r\n--%@\r\n",stringBoundary];
233 for (NSDictionary *val in [self postData]) {
234 [self appendPostString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n",[val objectForKey:@"key"]]];
235 [self appendPostString:[val objectForKey:@"value"]];
237 if (i != [[self postData] count] || [[self fileData] count] > 0) { //Only add the boundary if this is not the last item in the post body
238 [self appendPostString:endItemBoundary];
242 // Adds files to upload
244 for (NSDictionary *val in [self fileData]) {
246 [self appendPostString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", [val objectForKey:@"key"], [val objectForKey:@"fileName"]]];
247 [self appendPostString:[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", [val objectForKey:@"contentType"]]];
249 id data = [val objectForKey:@"data"];
250 if ([data isKindOfClass:[NSString class]]) {
251 [self appendPostDataFromFile:data];
253 [self appendPostData:data];
256 // Only add the boundary if this is not the last item in the post body
257 if (i != [[self fileData] count]) {
258 [self appendPostString:endItemBoundary];
262 [self appendPostString:[NSString stringWithFormat:@"\r\n--%@--\r\n",stringBoundary]];
264 #if DEBUG_FORM_DATA_REQUEST
265 [self addToDebugBody:@"==== End of multipart/form-data body ====\r\n"];
269 - (void)buildURLEncodedPostBody
272 // We can't post binary data using application/x-www-form-urlencoded
273 if ([[self fileData] count] > 0) {
274 [self setPostFormat:ASIMultipartFormDataPostFormat];
275 [self buildMultipartFormDataPostBody];
279 #if DEBUG_FORM_DATA_REQUEST
280 [self addToDebugBody:@"\r\n==== Building an application/x-www-form-urlencoded body ====\r\n"];
284 NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding([self stringEncoding]));
286 [self addRequestHeader:@"Content-Type" value:[NSString stringWithFormat:@"application/x-www-form-urlencoded; charset=%@",charset]];
290 NSUInteger count = [[self postData] count]-1;
291 for (NSDictionary *val in [self postData]) {
292 NSString *data = [NSString stringWithFormat:@"%@=%@%@", [self encodeURL:[val objectForKey:@"key"]], [self encodeURL:[val objectForKey:@"value"]],(i<count ? @"&" : @"")];
293 [self appendPostString:data];
296 #if DEBUG_FORM_DATA_REQUEST
297 [self addToDebugBody:@"\r\n==== End of application/x-www-form-urlencoded body ====\r\n"];
301 - (void)appendPostString:(NSString *)string
303 #if DEBUG_FORM_DATA_REQUEST
304 [self addToDebugBody:string];
306 [super appendPostData:[string dataUsingEncoding:[self stringEncoding]]];
309 #if DEBUG_FORM_DATA_REQUEST
310 - (void)appendPostData:(NSData *)data
312 [self addToDebugBody:[NSString stringWithFormat:@"[%lu bytes of data]",(unsigned long)[data length]]];
313 [super appendPostData:data];
316 - (void)appendPostDataFromFile:(NSString *)file
319 unsigned long long fileSize = [[[[NSFileManager defaultManager] attributesOfItemAtPath:file error:&err] objectForKey:NSFileSize] unsignedLongLongValue];
321 [self addToDebugBody:[NSString stringWithFormat:@"[Error: Failed to obtain the size of the file at '%@']",file]];
323 [self addToDebugBody:[NSString stringWithFormat:@"[%llu bytes of data from file '%@']",fileSize,file]];
326 [super appendPostDataFromFile:file];
329 - (void)addToDebugBody:(NSString *)string
331 [self setDebugBodyString:[[self debugBodyString] stringByAppendingString:string]];
335 #pragma mark NSCopying
337 - (id)copyWithZone:(NSZone *)zone
339 ASIFormDataRequest *newRequest = [super copyWithZone:zone];
340 [newRequest setPostData:[[[self postData] mutableCopyWithZone:zone] autorelease]];
341 [newRequest setFileData:[[[self fileData] mutableCopyWithZone:zone] autorelease]];
342 [newRequest setPostFormat:[self postFormat]];
343 [newRequest setStringEncoding:[self stringEncoding]];
344 [newRequest setRequestMethod:[self requestMethod]];
348 @synthesize postData;
349 @synthesize fileData;
350 @synthesize postFormat;
351 @synthesize stringEncoding;
352 #if DEBUG_FORM_DATA_REQUEST
353 @synthesize debugBodyString;