/* Copyright (c) 2011 Google Inc. * * Licensed 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. */ // // GTMHTTPFetcher.m // #define GTMHTTPFETCHER_DEFINE_GLOBALS 1 #import "GTMHTTPFetcher.h" #if GTM_BACKGROUND_FETCHING #import #endif static id gGTMFetcherStaticCookieStorage = nil; static Class gGTMFetcherConnectionClass = nil; // The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH), // 1 minute for downloads. static const NSTimeInterval kUnsetMaxRetryInterval = -1; static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0; static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.; // delegateQueue callback parameters static NSString *const kCallbackData = @"data"; static NSString *const kCallbackError = @"error"; // // GTMHTTPFetcher // @interface GTMHTTPFetcher () @property (copy) NSString *temporaryDownloadPath; @property (retain) id cookieStorage; @property (readwrite, retain) NSData *downloadedData; #if NS_BLOCKS_AVAILABLE @property (copy) void (^completionBlock)(NSData *, NSError *); #endif - (BOOL)beginFetchMayDelay:(BOOL)mayDelay mayAuthorize:(BOOL)mayAuthorize; - (void)failToBeginFetchWithError:(NSError *)error; - (void)failToBeginFetchDeferWithError:(NSError *)error; #if GTM_BACKGROUND_FETCHING - (void)endBackgroundTask; - (void)backgroundFetchExpired; #endif - (BOOL)authorizeRequest; - (void)authorizer:(id )auth request:(NSMutableURLRequest *)request finishedWithError:(NSError *)error; - (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath; - (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks; - (BOOL)shouldReleaseCallbacksUponCompletion; - (void)addCookiesToRequest:(NSMutableURLRequest *)request; - (void)handleCookiesForResponse:(NSURLResponse *)response; - (void)invokeFetchCallbacksWithData:(NSData *)data error:(NSError *)error; - (void)invokeFetchCallback:(SEL)sel target:(id)target data:(NSData *)data error:(NSError *)error; - (void)invokeFetchCallbacksOnDelegateQueueWithData:(NSData *)data error:(NSError *)error; - (void)releaseCallbacks; - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; - (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error; - (void)destroyRetryTimer; - (void)beginRetryTimer; - (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs; - (void)sendStopNotificationIfNeeded; - (void)retryFetch; - (void)retryTimerFired:(NSTimer *)timer; @end @interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal) - (void)setupStreamLogging; - (void)logFetchWithError:(NSError *)error; @end @implementation GTMHTTPFetcher + (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request { return [[[[self class] alloc] initWithRequest:request] autorelease]; } + (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL { return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]]; } + (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString { return [self fetcherWithURL:[NSURL URLWithString:requestURLString]]; } + (void)initialize { // initialize is guaranteed by the runtime to be called in a // thread-safe manner if (!gGTMFetcherStaticCookieStorage) { Class cookieStorageClass = NSClassFromString(@"GTMCookieStorage"); if (cookieStorageClass) { gGTMFetcherStaticCookieStorage = [[cookieStorageClass alloc] init]; } } } - (id)init { return [self initWithRequest:nil]; } - (id)initWithRequest:(NSURLRequest *)request { self = [super init]; if (self) { request_ = [request mutableCopy]; if (gGTMFetcherStaticCookieStorage != nil) { // The user has compiled with the cookie storage class available; // default to static cookie storage, so our cookies are independent // of the cookies of other apps. [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; } else { // Default to system default cookie storage [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodSystemDefault]; } #if !STRIP_GTM_FETCH_LOGGING // Encourage developers to set the comment property or use // setCommentWithFormat: by providing a default string. comment_ = @"(No fetcher comment set)"; #endif } return self; } - (id)copyWithZone:(NSZone *)zone { // disallow use of fetchers in a copy property [self doesNotRecognizeSelector:_cmd]; return nil; } - (NSString *)description { return [NSString stringWithFormat:@"%@ %p (%@)", [self class], self, [self.mutableRequest URL]]; } #if !GTM_IPHONE - (void)finalize { [self stopFetchReleasingCallbacks:YES]; // releases connection_, destroys timers [super finalize]; } #endif - (void)dealloc { #if DEBUG NSAssert(!isStopNotificationNeeded_, @"unbalanced fetcher notification for %@", [request_ URL]); #endif // Note: if a connection or a retry timer was pending, then this instance // would be retained by those so it wouldn't be getting dealloc'd, // hence we don't need to stopFetch here [request_ release]; [connection_ release]; [downloadedData_ release]; [downloadPath_ release]; [temporaryDownloadPath_ release]; [downloadFileHandle_ release]; [credential_ release]; [proxyCredential_ release]; [postData_ release]; [postStream_ release]; [loggedStreamData_ release]; [response_ release]; #if NS_BLOCKS_AVAILABLE [completionBlock_ release]; [receivedDataBlock_ release]; [sentDataBlock_ release]; [retryBlock_ release]; #endif [userData_ release]; [properties_ release]; [delegateQueue_ release]; [runLoopModes_ release]; [fetchHistory_ release]; [cookieStorage_ release]; [authorizer_ release]; [service_ release]; [serviceHost_ release]; [thread_ release]; [retryTimer_ release]; [comment_ release]; [log_ release]; #if !STRIP_GTM_FETCH_LOGGING [redirectedFromURL_ release]; [logRequestBody_ release]; [logResponseBody_ release]; #endif [super dealloc]; } #pragma mark - // Begin fetching the URL (or begin a retry fetch). The delegate is retained // for the duration of the fetch connection. - (BOOL)beginFetchWithDelegate:(id)delegate didFinishSelector:(SEL)finishedSelector { GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, @encode(GTMHTTPFetcher *), @encode(NSData *), @encode(NSError *), 0); GTMAssertSelectorNilOrImplementedWithArgs(delegate, receivedDataSel_, @encode(GTMHTTPFetcher *), @encode(NSData *), 0); GTMAssertSelectorNilOrImplementedWithArgs(delegate, retrySel_, @encode(GTMHTTPFetcher *), @encode(BOOL), @encode(NSError *), 0); // We'll retain the delegate only during the outstanding connection (similar // to what Cocoa does with performSelectorOnMainThread:) and during // authorization or delays, since the app would crash // if the delegate was released before the fetch calls back [self setDelegate:delegate]; finishedSel_ = finishedSelector; return [self beginFetchMayDelay:YES mayAuthorize:YES]; } - (BOOL)beginFetchMayDelay:(BOOL)mayDelay mayAuthorize:(BOOL)mayAuthorize { // This is the internal entry point for re-starting fetches NSError *error = nil; if (connection_ != nil) { NSAssert1(connection_ != nil, @"fetch object %@ being reused; this should never happen", self); goto CannotBeginFetch; } if (request_ == nil || [request_ URL] == nil) { NSAssert(request_ != nil, @"beginFetchWithDelegate requires a request with a URL"); goto CannotBeginFetch; } self.downloadedData = nil; downloadedLength_ = 0; if (mayDelay && service_) { BOOL shouldFetchNow = [service_ fetcherShouldBeginFetching:self]; if (!shouldFetchNow) { // the fetch is deferred, but will happen later return YES; } } NSString *effectiveHTTPMethod = [request_ valueForHTTPHeaderField:@"X-HTTP-Method-Override"]; if (effectiveHTTPMethod == nil) { effectiveHTTPMethod = [request_ HTTPMethod]; } BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil || [effectiveHTTPMethod isEqual:@"GET"]); if (postData_ || postStream_) { if (isEffectiveHTTPGet) { [request_ setHTTPMethod:@"POST"]; isEffectiveHTTPGet = NO; } if (postData_) { [request_ setHTTPBody:postData_]; } else { if ([self respondsToSelector:@selector(setupStreamLogging)]) { [self performSelector:@selector(setupStreamLogging)]; } [request_ setHTTPBodyStream:postStream_]; } } // We authorize after setting up the http method and body in the request // because OAuth 1 may need to sign the request body if (mayAuthorize && authorizer_) { BOOL isAuthorized = [authorizer_ isAuthorizedRequest:request_]; if (!isAuthorized) { // authorization needed return [self authorizeRequest]; } } [fetchHistory_ updateRequest:request_ isHTTPGet:isEffectiveHTTPGet]; // set the default upload or download retry interval, if necessary if (isRetryEnabled_ && maxRetryInterval_ <= kUnsetMaxRetryInterval) { if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) { [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval]; } else { [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval]; } } [self addCookiesToRequest:request_]; if (downloadPath_ != nil) { // downloading to a path, so create a temporary file and a file handle for // downloading NSString *tempPath = [self createTempDownloadFilePathForPath:downloadPath_]; BOOL didCreate = [[NSData data] writeToFile:tempPath options:0 error:&error]; if (!didCreate) goto CannotBeginFetch; [self setTemporaryDownloadPath:tempPath]; NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:tempPath]; if (fh == nil) goto CannotBeginFetch; [self setDownloadFileHandle:fh]; } // finally, start the connection Class connectionClass = [[self class] connectionClass]; NSOperationQueue *delegateQueue = delegateQueue_; if (delegateQueue && ![connectionClass instancesRespondToSelector:@selector(setDelegateQueue:)]) { // NSURLConnection has no setDelegateQueue: on iOS 4 and Mac OS X 10.5. delegateQueue = nil; self.delegateQueue = nil; } #if DEBUG && TARGET_OS_IPHONE BOOL isPreIOS6 = (NSFoundationVersionNumber <= 890.1); if (isPreIOS6 && delegateQueue) { NSLog(@"GTMHTTPFetcher delegateQueue not safe in iOS 5"); } #endif if (downloadFileHandle_ != nil) { // Downloading to a file, so downloadedData_ remains nil. } else { self.downloadedData = [NSMutableData data]; } hasConnectionEnded_ = NO; if ([runLoopModes_ count] == 0 && delegateQueue == nil) { // No custom callback modes or queue were specified, so start the connection // on the current run loop in the current mode connection_ = [[connectionClass connectionWithRequest:request_ delegate:self] retain]; } else { // Specify callbacks be on an operation queue or on the current run loop // in the specified modes connection_ = [[connectionClass alloc] initWithRequest:request_ delegate:self startImmediately:NO]; if (delegateQueue) { [connection_ performSelector:@selector(setDelegateQueue:) withObject:delegateQueue]; } else if (runLoopModes_) { NSRunLoop *rl = [NSRunLoop currentRunLoop]; for (NSString *mode in runLoopModes_) { [connection_ scheduleInRunLoop:rl forMode:mode]; } } [connection_ start]; } if (!connection_) { NSAssert(connection_ != nil, @"beginFetchWithDelegate could not create a connection"); self.downloadedData = nil; goto CannotBeginFetch; } #if GTM_BACKGROUND_FETCHING backgroundTaskIdentifer_ = 0; // UIBackgroundTaskInvalid is 0 on iOS 4 if (shouldFetchInBackground_) { // For iOS 3 compatibility, ensure that UIApp supports backgrounding UIApplication *app = [UIApplication sharedApplication]; if ([app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) { // Tell UIApplication that we want to continue even when the app is in the // background. NSThread *thread = delegateQueue_ ? nil : [NSThread currentThread]; backgroundTaskIdentifer_ = [app beginBackgroundTaskWithExpirationHandler:^{ // Background task expiration callback - this block is always invoked by // UIApplication on the main thread. if (thread) { // Run the user's callbacks on the thread used to start the // fetch. [self performSelector:@selector(backgroundFetchExpired) onThread:thread withObject:nil waitUntilDone:YES]; } else { // backgroundFetchExpired invokes callbacks on the provided delegate // queue. [self backgroundFetchExpired]; } }]; } } #endif // Once connection_ is non-nil we can send the start notification isStopNotificationNeeded_ = YES; NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; [defaultNC postNotificationName:kGTMHTTPFetcherStartedNotification object:self]; return YES; CannotBeginFetch: [self failToBeginFetchDeferWithError:error]; return NO; } - (void)failToBeginFetchDeferWithError:(NSError *)error { if (delegateQueue_) { // Deferring will happen by the callback being invoked on the specified // queue. [self failToBeginFetchWithError:error]; } else { // No delegate queue has been specified, so put the callback // on an appropriate run loop. NSArray *modes = (runLoopModes_ ? runLoopModes_ : [NSArray arrayWithObject:NSRunLoopCommonModes]); [self performSelector:@selector(failToBeginFetchWithError:) onThread:[NSThread currentThread] withObject:error waitUntilDone:NO modes:modes]; } } - (void)failToBeginFetchWithError:(NSError *)error { if (error == nil) { error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain code:kGTMHTTPFetcherErrorDownloadFailed userInfo:nil]; } [[self retain] autorelease]; // In case the callback releases us [self invokeFetchCallbacksOnDelegateQueueWithData:nil error:error]; [self releaseCallbacks]; [service_ fetcherDidStop:self]; self.authorizer = nil; if (temporaryDownloadPath_) { [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_ error:NULL]; self.temporaryDownloadPath = nil; } } #if GTM_BACKGROUND_FETCHING - (void)backgroundFetchExpired { // On background expiration, we stop the fetch and invoke the callbacks NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain code:kGTMHTTPFetcherErrorBackgroundExpiration userInfo:nil]; [self invokeFetchCallbacksOnDelegateQueueWithData:nil error:error]; @synchronized(self) { // Stopping the fetch here will indirectly call endBackgroundTask [self stopFetchReleasingCallbacks:NO]; [self releaseCallbacks]; self.authorizer = nil; } } - (void)endBackgroundTask { @synchronized(self) { // Whenever the connection stops or background execution expires, // we need to tell UIApplication we're done if (backgroundTaskIdentifer_) { // If backgroundTaskIdentifer_ is non-zero, we know we're on iOS 4 UIApplication *app = [UIApplication sharedApplication]; [app endBackgroundTask:backgroundTaskIdentifer_]; backgroundTaskIdentifer_ = 0; } } } #endif // GTM_BACKGROUND_FETCHING - (BOOL)authorizeRequest { id authorizer = self.authorizer; SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:); if ([authorizer respondsToSelector:asyncAuthSel]) { SEL callbackSel = @selector(authorizer:request:finishedWithError:); [authorizer authorizeRequest:request_ delegate:self didFinishSelector:callbackSel]; return YES; } else { NSAssert(authorizer == nil, @"invalid authorizer for fetch"); // No authorizing possible, and authorizing happens only after any delay; // just begin fetching return [self beginFetchMayDelay:NO mayAuthorize:NO]; } } - (void)authorizer:(id )auth request:(NSMutableURLRequest *)request finishedWithError:(NSError *)error { if (error != nil) { // We can't fetch without authorization [self failToBeginFetchDeferWithError:error]; } else { [self beginFetchMayDelay:NO mayAuthorize:NO]; } } #if NS_BLOCKS_AVAILABLE - (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler { self.completionBlock = handler; // The user may have called setDelegate: earlier if they want to use other // delegate-style callbacks during the fetch; otherwise, the delegate is nil, // which is fine. return [self beginFetchWithDelegate:[self delegate] didFinishSelector:nil]; } #endif - (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath { NSString *tempDir = nil; #if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED >= 1060)) // Find an appropriate directory for the download, ideally on the same disk // as the final target location so the temporary file won't have to be moved // to a different disk. // // Available in SDKs for 10.6 and iOS 4 // // Oct 2011: We previously also used URLForDirectory for // (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000)) // but that is returning a non-temporary directory for iOS, unfortunately SEL sel = @selector(URLForDirectory:inDomain:appropriateForURL:create:error:); if ([NSFileManager instancesRespondToSelector:sel]) { NSError *error = nil; NSURL *targetURL = [NSURL fileURLWithPath:targetPath]; NSFileManager *fileMgr = [NSFileManager defaultManager]; NSURL *tempDirURL = [fileMgr URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:targetURL create:YES error:&error]; tempDir = [tempDirURL path]; } #endif if (tempDir == nil) { tempDir = NSTemporaryDirectory(); } static unsigned int counter = 0; NSString *name = [NSString stringWithFormat:@"gtmhttpfetcher_%u_%u", ++counter, (unsigned int) arc4random()]; NSString *result = [tempDir stringByAppendingPathComponent:name]; return result; } - (void)addCookiesToRequest:(NSMutableURLRequest *)request { // Get cookies for this URL from our storage array, if // we have a storage array if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault && cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodNone) { NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]]; if ([cookies count] > 0) { NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; NSString *cookieHeader = [headerFields objectForKey:@"Cookie"]; // key used in header dictionary if (cookieHeader) { [request addValue:cookieHeader forHTTPHeaderField:@"Cookie"]; // header name } } } } // Returns YES if this is in the process of fetching a URL, or waiting to // retry, or waiting for authorization, or waiting to be issued by the // service object - (BOOL)isFetching { if (connection_ != nil || retryTimer_ != nil) return YES; BOOL isAuthorizing = [authorizer_ isAuthorizingRequest:request_]; if (isAuthorizing) return YES; BOOL isDelayed = [service_ isDelayingFetcher:self]; return isDelayed; } // Returns the status code set in connection:didReceiveResponse: - (NSInteger)statusCode { NSInteger statusCode; if (response_ != nil && [response_ respondsToSelector:@selector(statusCode)]) { statusCode = [(NSHTTPURLResponse *)response_ statusCode]; } else { // Default to zero, in hopes of hinting "Unknown" (we can't be // sure that things are OK enough to use 200). statusCode = 0; } return statusCode; } - (NSDictionary *)responseHeaders { if (response_ != nil && [response_ respondsToSelector:@selector(allHeaderFields)]) { NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields]; return headers; } return nil; } - (void)releaseCallbacks { [delegate_ autorelease]; delegate_ = nil; [delegateQueue_ autorelease]; delegateQueue_ = nil; #if NS_BLOCKS_AVAILABLE self.completionBlock = nil; self.sentDataBlock = nil; self.receivedDataBlock = nil; self.retryBlock = nil; #endif } // Cancel the fetch of the URL that's currently in progress. - (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks { id service; // if the connection or the retry timer is all that's retaining the fetcher, // we want to be sure this instance survives stopping at least long enough for // the stack to unwind [[self retain] autorelease]; [self destroyRetryTimer]; @synchronized(self) { service = [[service_ retain] autorelease]; if (connection_) { // in case cancelling the connection calls this recursively, we want // to ensure that we'll only release the connection and delegate once, // so first set connection_ to nil NSURLConnection* oldConnection = connection_; connection_ = nil; if (!hasConnectionEnded_) { [oldConnection cancel]; } // this may be called in a callback from the connection, so use autorelease [oldConnection autorelease]; } } // @synchronized(self) // send the stopped notification [self sendStopNotificationIfNeeded]; @synchronized(self) { [authorizer_ stopAuthorizationForRequest:request_]; if (shouldReleaseCallbacks) { [self releaseCallbacks]; self.authorizer = nil; } if (temporaryDownloadPath_) { [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_ error:NULL]; self.temporaryDownloadPath = nil; } } // @synchronized(self) [service fetcherDidStop:self]; #if GTM_BACKGROUND_FETCHING [self endBackgroundTask]; #endif } // External stop method - (void)stopFetching { [self stopFetchReleasingCallbacks:YES]; } - (void)sendStopNotificationIfNeeded { BOOL sendNow = NO; @synchronized(self) { if (isStopNotificationNeeded_) { isStopNotificationNeeded_ = NO; sendNow = YES; } } if (sendNow) { NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; [defaultNC postNotificationName:kGTMHTTPFetcherStoppedNotification object:self]; } } - (void)retryFetch { [self stopFetchReleasingCallbacks:NO]; [self beginFetchWithDelegate:delegate_ didFinishSelector:finishedSel_]; } - (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds { NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; // Loop until the callbacks have been called and released, and until // the connection is no longer pending, or until the timeout has expired BOOL isMainThread = [NSThread isMainThread]; while ((!hasConnectionEnded_ #if NS_BLOCKS_AVAILABLE || completionBlock_ != nil #endif || delegate_ != nil) && [giveUpDate timeIntervalSinceNow] > 0) { // Run the current run loop 1/1000 of a second to give the networking // code a chance to work if (isMainThread || delegateQueue_ == nil) { NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001]; [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; } else { [NSThread sleepForTimeInterval:0.001]; } } } #pragma mark NSURLConnection Delegate Methods // // NSURLConnection Delegate Methods // // This method just says "follow all redirects", which _should_ be the default behavior, // According to file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Conceptual/URLLoadingSystem // but the redirects were not being followed until I added this method. May be // a bug in the NSURLConnection code, or the documentation. // // In OS X 10.4.8 and earlier, the redirect request doesn't // get the original's headers and body. This causes POSTs to fail. // So we construct a new request, a copy of the original, with overrides from the // redirect. // // Docs say that if redirectResponse is nil, just return the redirectRequest. - (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)redirectRequest redirectResponse:(NSURLResponse *)redirectResponse { @synchronized(self) { if (redirectRequest && redirectResponse) { // save cookies from the response [self handleCookiesForResponse:redirectResponse]; NSMutableURLRequest *newRequest = [[request_ mutableCopy] autorelease]; // copy the URL NSURL *redirectURL = [redirectRequest URL]; NSURL *url = [newRequest URL]; // disallow scheme changes (say, from https to http) NSString *redirectScheme = [url scheme]; NSString *newScheme = [redirectURL scheme]; NSString *newResourceSpecifier = [redirectURL resourceSpecifier]; if ([redirectScheme caseInsensitiveCompare:@"http"] == NSOrderedSame && newScheme != nil && [newScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { // allow the change from http to https redirectScheme = newScheme; } NSString *newUrlString = [NSString stringWithFormat:@"%@:%@", redirectScheme, newResourceSpecifier]; NSURL *newURL = [NSURL URLWithString:newUrlString]; [newRequest setURL:newURL]; // any headers in the redirect override headers in the original. NSDictionary *redirectHeaders = [redirectRequest allHTTPHeaderFields]; for (NSString *key in redirectHeaders) { NSString *value = [redirectHeaders objectForKey:key]; [newRequest setValue:value forHTTPHeaderField:key]; } [self addCookiesToRequest:newRequest]; redirectRequest = newRequest; // log the response we just received [self setResponse:redirectResponse]; [self logNowWithError:nil]; // update the request for future logging NSMutableURLRequest *mutable = [[redirectRequest mutableCopy] autorelease]; [self setMutableRequest:mutable]; } return redirectRequest; } // @synchronized(self) } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { @synchronized(self) { // This method is called when the server has determined that it // has enough information to create the NSURLResponse // it can be called multiple times, for example in the case of a // redirect, so each time we reset the data. [downloadedData_ setLength:0]; [downloadFileHandle_ truncateFileAtOffset:0]; downloadedLength_ = 0; [self setResponse:response]; // Save cookies from the response [self handleCookiesForResponse:response]; } } // handleCookiesForResponse: handles storage of cookies for responses passed to // connection:willSendRequest:redirectResponse: and connection:didReceiveResponse: - (void)handleCookiesForResponse:(NSURLResponse *)response { if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodSystemDefault || cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodNone) { // do nothing special for NSURLConnection's default storage mechanism // or when we're ignoring cookies } else if ([response respondsToSelector:@selector(allHeaderFields)]) { // grab the cookies from the header as NSHTTPCookies and store them either // into our static array or into the fetchHistory NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields]; if (responseHeaderFields) { NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaderFields forURL:[response URL]]; if ([cookies count] > 0) { [cookieStorage_ setCookies:cookies]; } } } } -(void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { @synchronized(self) { if ([challenge previousFailureCount] <= 2) { NSURLCredential *credential = credential_; if ([[challenge protectionSpace] isProxy] && proxyCredential_ != nil) { credential = proxyCredential_; } // Here, if credential is still nil, then we *could* try to get it from // NSURLCredentialStorage's defaultCredentialForProtectionSpace:. // We don't, because we're assuming: // // - for server credentials, we only want ones supplied by the program // calling http fetcher // - for proxy credentials, if one were necessary and available in the // keychain, it would've been found automatically by NSURLConnection // and this challenge delegate method never would've been called // anyway if (credential) { // try the credential [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; return; } } // @synchronized(self) // If we don't have credentials, or we've already failed auth 3x, // report the error, putting the challenge as a value in the userInfo // dictionary. #if DEBUG NSAssert(!isCancellingChallenge_, @"isCancellingChallenge_ unexpected"); #endif NSDictionary *userInfo = [NSDictionary dictionaryWithObject:challenge forKey:kGTMHTTPFetcherErrorChallengeKey]; NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed userInfo:userInfo]; // cancelAuthenticationChallenge seems to indirectly call // connection:didFailWithError: now, though that isn't documented // // We'll use an ivar to make the indirect invocation of the // delegate method do nothing. isCancellingChallenge_ = YES; [[challenge sender] cancelAuthenticationChallenge:challenge]; isCancellingChallenge_ = NO; [self connection:connection didFailWithError:error]; } } - (void)invokeFetchCallbacksWithData:(NSData *)data error:(NSError *)error { // To avoid deadlocks, this should not be called inside of @synchronized(self) id target; SEL sel; #if NS_BLOCKS_AVAILABLE void (^block)(NSData *, NSError *); #endif @synchronized(self) { target = delegate_; sel = finishedSel_; block = completionBlock_; } [[self retain] autorelease]; // In case the callback releases us [self invokeFetchCallback:sel target:target data:data error:error]; #if NS_BLOCKS_AVAILABLE if (block) { block(data, error); } #endif } - (void)invokeFetchCallback:(SEL)sel target:(id)target data:(NSData *)data error:(NSError *)error { // This method is available to subclasses which may provide a customized // target pointer. if (target && sel) { NSMethodSignature *sig = [target methodSignatureForSelector:sel]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; [invocation setSelector:sel]; [invocation setTarget:target]; [invocation setArgument:&self atIndex:2]; [invocation setArgument:&data atIndex:3]; [invocation setArgument:&error atIndex:4]; [invocation invoke]; } } - (void)invokeFetchCallbacksOnDelegateQueueWithData:(NSData *)data error:(NSError *)error { // This is called by methods that are not already on the delegateQueue // (as NSURLConnection callbacks should already be, but other failures // are not.) if (!delegateQueue_) { [self invokeFetchCallbacksWithData:data error:error]; } // Values may be nil. NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:2]; [dict setValue:data forKey:kCallbackData]; [dict setValue:error forKey:kCallbackError]; NSInvocationOperation *op = [[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invokeOnQueueWithDictionary:) object:dict] autorelease]; [delegateQueue_ addOperation:op]; } - (void)invokeOnQueueWithDictionary:(NSDictionary *)dict { NSData *data = [dict objectForKey:kCallbackData]; NSError *error = [dict objectForKey:kCallbackError]; [self invokeFetchCallbacksWithData:data error:error]; } - (void)invokeSentDataCallback:(SEL)sel target:(id)target didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { if (target && sel) { NSMethodSignature *sig = [target methodSignatureForSelector:sel]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; [invocation setSelector:sel]; [invocation setTarget:target]; [invocation setArgument:&self atIndex:2]; [invocation setArgument:&bytesWritten atIndex:3]; [invocation setArgument:&totalBytesWritten atIndex:4]; [invocation setArgument:&totalBytesExpectedToWrite atIndex:5]; [invocation invoke]; } } - (BOOL)invokeRetryCallback:(SEL)sel target:(id)target willRetry:(BOOL)willRetry error:(NSError *)error { if (target && sel) { NSMethodSignature *sig = [target methodSignatureForSelector:sel]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; [invocation setSelector:sel]; [invocation setTarget:target]; [invocation setArgument:&self atIndex:2]; [invocation setArgument:&willRetry atIndex:3]; [invocation setArgument:&error atIndex:4]; [invocation invoke]; [invocation getReturnValue:&willRetry]; } return willRetry; } - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { @synchronized(self) { SEL sel = [self sentDataSelector]; [self invokeSentDataCallback:sel target:delegate_ didSendBodyData:bytesWritten totalBytesWritten:totalBytesWritten totalBytesExpectedToWrite:totalBytesExpectedToWrite]; #if NS_BLOCKS_AVAILABLE if (sentDataBlock_) { sentDataBlock_(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); } #endif } } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { @synchronized(self) { #if DEBUG NSAssert(!hasConnectionEnded_, @"Connection received data after ending"); // The download file handle should be set or the data object allocated // before the fetch is started. NSAssert((downloadFileHandle_ == nil) != (downloadedData_ == nil), @"received data accumulates as either NSData (%d) or" @" NSFileHandle (%d)", (downloadedData_ != nil), (downloadFileHandle_ != nil)); #endif // Hopefully, we'll never see this execute out-of-order, receiving data // after we've received the finished or failed callback. if (hasConnectionEnded_) return; if (downloadFileHandle_ != nil) { // Append to file @try { [downloadFileHandle_ writeData:data]; downloadedLength_ = [downloadFileHandle_ offsetInFile]; } @catch (NSException *exc) { // Couldn't write to file, probably due to a full disk NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[exc reason] forKey:NSLocalizedDescriptionKey]; NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain code:kGTMHTTPFetcherErrorFileHandleException userInfo:userInfo]; [self connection:connection didFailWithError:error]; return; } } else { // append to mutable data [downloadedData_ appendData:data]; downloadedLength_ = [downloadedData_ length]; } if (receivedDataSel_) { [delegate_ performSelector:receivedDataSel_ withObject:self withObject:downloadedData_]; } #if NS_BLOCKS_AVAILABLE if (receivedDataBlock_) { receivedDataBlock_(downloadedData_); } #endif } // @synchronized(self) } // For error 304's ("Not Modified") where we've cached the data, return // status 200 ("OK") to the caller (but leave the fetcher status as 304) // and copy the cached data. // // For other errors or if there's no cached data, just return the actual status. - (NSData *)cachedDataForStatus { if ([self statusCode] == kGTMHTTPFetcherStatusNotModified && [fetchHistory_ shouldCacheETaggedData]) { NSData *cachedData = [fetchHistory_ cachedDataForRequest:request_]; return cachedData; } return nil; } - (NSInteger)statusAfterHandlingNotModifiedError { NSInteger status = [self statusCode]; NSData *cachedData = [self cachedDataForStatus]; if (cachedData) { // Forge the status to pass on to the delegate status = 200; // Copy our stored data if (downloadFileHandle_ != nil) { @try { // Downloading to a file handle won't save to the cache (the data is // likely inappropriately large for caching), but will still read from // the cache, on the unlikely chance that the response was Not Modified // and the URL response was indeed present in the cache. [downloadFileHandle_ truncateFileAtOffset:0]; [downloadFileHandle_ writeData:cachedData]; downloadedLength_ = [downloadFileHandle_ offsetInFile]; } @catch (NSException *) { // Failed to write data, likely due to lack of disk space status = kGTMHTTPFetcherErrorFileHandleException; } } else { [downloadedData_ setData:cachedData]; downloadedLength_ = [cachedData length]; } } return status; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { BOOL shouldStopFetching = YES; BOOL shouldSendStopNotification = NO; NSError *error = nil; NSData *downloadedData; #if !STRIP_GTM_FETCH_LOGGING BOOL shouldDeferLogging = NO; #endif BOOL shouldBeginRetryTimer = NO; BOOL hasLogged = NO; @synchronized(self) { // We no longer need to cancel the connection hasConnectionEnded_ = YES; // Skip caching ETagged results when the data is being saved to a file if (downloadFileHandle_ == nil) { [fetchHistory_ updateFetchHistoryWithRequest:request_ response:response_ downloadedData:downloadedData_]; } else { [fetchHistory_ removeCachedDataForRequest:request_]; } [[self retain] autorelease]; // in case the callback releases us NSInteger status = [self statusCode]; if ([self cachedDataForStatus] != nil) { // Log the pre-cache response. [self logNowWithError:nil]; hasLogged = YES; status = [self statusAfterHandlingNotModifiedError]; } shouldSendStopNotification = YES; if (status >= 0 && status < 300) { // success if (downloadPath_) { // Avoid deleting the downloaded file when the fetch stops [downloadFileHandle_ closeFile]; self.downloadFileHandle = nil; NSFileManager *fileMgr = [NSFileManager defaultManager]; [fileMgr removeItemAtPath:downloadPath_ error:NULL]; if ([fileMgr moveItemAtPath:temporaryDownloadPath_ toPath:downloadPath_ error:&error]) { self.temporaryDownloadPath = nil; } } } else { // unsuccessful if (!hasLogged) { [self logNowWithError:nil]; hasLogged = YES; } // Status over 300; retry or notify the delegate of failure if ([self shouldRetryNowForStatus:status error:nil]) { // retrying shouldBeginRetryTimer = YES; shouldStopFetching = NO; } else { NSDictionary *userInfo = nil; if ([downloadedData_ length] > 0) { userInfo = [NSDictionary dictionaryWithObject:downloadedData_ forKey:kGTMHTTPFetcherStatusDataKey]; } error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain code:status userInfo:userInfo]; } } downloadedData = downloadedData_; #if !STRIP_GTM_FETCH_LOGGING shouldDeferLogging = shouldDeferResponseBodyLogging_; #endif } // @synchronized(self) if (shouldBeginRetryTimer) { [self beginRetryTimer]; } if (shouldSendStopNotification) { // We want to send the stop notification before calling the delegate's // callback selector, since the callback selector may release all of // the fetcher properties that the client is using to track the fetches. // // We'll also stop now so that, to any observers watching the notifications, // it doesn't look like our wait for a retry (which may be long, // 30 seconds or more) is part of the network activity. [self sendStopNotificationIfNeeded]; } if (shouldStopFetching) { // Call the callbacks (outside of the @synchronized to avoid deadlocks.) [self invokeFetchCallbacksWithData:downloadedData error:error]; BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion]; [self stopFetchReleasingCallbacks:shouldRelease]; } @synchronized(self) { BOOL shouldLogNow = !hasLogged; #if !STRIP_GTM_FETCH_LOGGING if (shouldDeferLogging) shouldLogNow = NO; #endif if (shouldLogNow) { [self logNowWithError:nil]; } } } - (BOOL)shouldReleaseCallbacksUponCompletion { // A subclass can override this to keep callbacks around after the // connection has finished successfully return YES; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { @synchronized(self) { // Prevent the failure callback from being called twice, since the stopFetch // call below (either the explicit one at the end of this method, or the // implicit one when the retry occurs) will release the delegate. if (connection_ == nil) return; // If this method was invoked indirectly by cancellation of an authentication // challenge, defer this until it is called again with the proper error object if (isCancellingChallenge_) return; // We no longer need to cancel the connection hasConnectionEnded_ = YES; [self logNowWithError:error]; } // See comment about sendStopNotificationIfNeeded // in connectionDidFinishLoading: [self sendStopNotificationIfNeeded]; if ([self shouldRetryNowForStatus:0 error:error]) { [self beginRetryTimer]; } else { [[self retain] autorelease]; // in case the callback releases us [self invokeFetchCallbacksWithData:nil error:error]; [self stopFetchReleasingCallbacks:YES]; } } - (void)logNowWithError:(NSError *)error { // If the logging category is available, then log the current request, // response, data, and error if ([self respondsToSelector:@selector(logFetchWithError:)]) { [self performSelector:@selector(logFetchWithError:) withObject:error]; } } #pragma mark Retries - (BOOL)isRetryError:(NSError *)error { struct retryRecord { NSString *const domain; int code; }; struct retryRecord retries[] = { { kGTMHTTPFetcherStatusDomain, 408 }, // request timeout { kGTMHTTPFetcherStatusDomain, 503 }, // service unavailable { kGTMHTTPFetcherStatusDomain, 504 }, // request timeout { NSURLErrorDomain, NSURLErrorTimedOut }, { NSURLErrorDomain, NSURLErrorNetworkConnectionLost }, { nil, 0 } }; // NSError's isEqual always returns false for equal but distinct instances // of NSError, so we have to compare the domain and code values explicitly for (int idx = 0; retries[idx].domain != nil; idx++) { if ([[error domain] isEqual:retries[idx].domain] && [error code] == retries[idx].code) { return YES; } } return NO; } // shouldRetryNowForStatus:error: returns YES if the user has enabled retries // and the status or error is one that is suitable for retrying. "Suitable" // means either the isRetryError:'s list contains the status or error, or the // user's retrySelector: is present and returns YES when called, or the // authorizer may be able to fix. - (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error { // Determine if a refreshed authorizer may avoid an authorization error BOOL shouldRetryForAuthRefresh = NO; BOOL isFirstAuthError = (authorizer_ != nil) && !hasAttemptedAuthRefresh_ && (status == kGTMHTTPFetcherStatusUnauthorized); // 401 if (isFirstAuthError) { if ([authorizer_ respondsToSelector:@selector(primeForRefresh)]) { BOOL hasPrimed = [authorizer_ primeForRefresh]; if (hasPrimed) { shouldRetryForAuthRefresh = YES; hasAttemptedAuthRefresh_ = YES; [request_ setValue:nil forHTTPHeaderField:@"Authorization"]; } } } // Determine if we're doing exponential backoff retries BOOL shouldDoIntervalRetry = [self isRetryEnabled] && ([self nextRetryInterval] < [self maxRetryInterval]); BOOL willRetry = NO; BOOL canRetry = shouldRetryForAuthRefresh || shouldDoIntervalRetry; if (canRetry) { // Check if this is a retryable error if (error == nil) { // Make an error for the status NSDictionary *userInfo = nil; if ([downloadedData_ length] > 0) { userInfo = [NSDictionary dictionaryWithObject:downloadedData_ forKey:kGTMHTTPFetcherStatusDataKey]; } error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain code:status userInfo:userInfo]; } willRetry = shouldRetryForAuthRefresh || [self isRetryError:error]; // If the user has installed a retry callback, consult that willRetry = [self invokeRetryCallback:retrySel_ target:delegate_ willRetry:willRetry error:error]; #if NS_BLOCKS_AVAILABLE if (retryBlock_) { willRetry = retryBlock_(willRetry, error); } #endif } return willRetry; } - (void)beginRetryTimer { @synchronized(self) { if (delegateQueue_ != nil && ![NSThread isMainThread]) { // A delegate queue is set, so the thread we're running on may not // have a run loop. We'll defer creating and starting the timer // until we're on the main thread to ensure it has a run loop. // (If we weren't supporting 10.5, we could use dispatch_after instead // of an NSTimer.) [self performSelectorOnMainThread:_cmd withObject:nil waitUntilDone:NO]; return; } } NSTimeInterval nextInterval = [self nextRetryInterval]; NSTimeInterval maxInterval = [self maxRetryInterval]; NSTimeInterval newInterval = MIN(nextInterval, maxInterval); [self primeRetryTimerWithNewTimeInterval:newInterval]; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc postNotificationName:kGTMHTTPFetcherRetryDelayStartedNotification object:self]; } - (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs { [self destroyRetryTimer]; @synchronized(self) { lastRetryInterval_ = secs; retryTimer_ = [NSTimer timerWithTimeInterval:secs target:self selector:@selector(retryTimerFired:) userInfo:nil repeats:NO]; [retryTimer_ retain]; NSRunLoop *timerRL = (self.delegateQueue ? [NSRunLoop mainRunLoop] : [NSRunLoop currentRunLoop]); [timerRL addTimer:retryTimer_ forMode:NSDefaultRunLoopMode]; } } - (void)retryTimerFired:(NSTimer *)timer { [self destroyRetryTimer]; @synchronized(self) { retryCount_++; [self retryFetch]; } } - (void)destroyRetryTimer { BOOL shouldNotify = NO; @synchronized(self) { if (retryTimer_) { [retryTimer_ invalidate]; [retryTimer_ autorelease]; retryTimer_ = nil; shouldNotify = YES; } } // @synchronized(self) if (shouldNotify) { NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; [defaultNC postNotificationName:kGTMHTTPFetcherRetryDelayStoppedNotification object:self]; } } - (NSUInteger)retryCount { return retryCount_; } - (NSTimeInterval)nextRetryInterval { // The next wait interval is the factor (2.0) times the last interval, // but never less than the minimum interval. NSTimeInterval secs = lastRetryInterval_ * retryFactor_; secs = MIN(secs, maxRetryInterval_); secs = MAX(secs, minRetryInterval_); return secs; } - (BOOL)isRetryEnabled { return isRetryEnabled_; } - (void)setRetryEnabled:(BOOL)flag { if (flag && !isRetryEnabled_) { // We defer initializing these until the user calls setRetryEnabled // to avoid using the random number generator if it's not needed. // However, this means min and max intervals for this fetcher are reset // as a side effect of calling setRetryEnabled. // // Make an initial retry interval random between 1.0 and 2.0 seconds [self setMinRetryInterval:0.0]; [self setMaxRetryInterval:kUnsetMaxRetryInterval]; [self setRetryFactor:2.0]; lastRetryInterval_ = 0.0; } isRetryEnabled_ = flag; }; - (NSTimeInterval)maxRetryInterval { return maxRetryInterval_; } - (void)setMaxRetryInterval:(NSTimeInterval)secs { if (secs > 0) { maxRetryInterval_ = secs; } else { maxRetryInterval_ = kUnsetMaxRetryInterval; } } - (double)minRetryInterval { return minRetryInterval_; } - (void)setMinRetryInterval:(NSTimeInterval)secs { if (secs > 0) { minRetryInterval_ = secs; } else { // Set min interval to a random value between 1.0 and 2.0 seconds // so that if multiple clients start retrying at the same time, they'll // repeat at different times and avoid overloading the server minRetryInterval_ = 1.0 + ((double)(arc4random() & 0x0FFFF) / (double) 0x0FFFF); } } #pragma mark Getters and Setters @dynamic cookieStorageMethod, retryEnabled, maxRetryInterval, minRetryInterval, retryCount, nextRetryInterval, statusCode, responseHeaders, fetchHistory, userData, properties; @synthesize mutableRequest = request_, credential = credential_, proxyCredential = proxyCredential_, postData = postData_, postStream = postStream_, delegate = delegate_, authorizer = authorizer_, service = service_, serviceHost = serviceHost_, servicePriority = servicePriority_, thread = thread_, sentDataSelector = sentDataSel_, receivedDataSelector = receivedDataSel_, retrySelector = retrySel_, retryFactor = retryFactor_, response = response_, downloadedLength = downloadedLength_, downloadedData = downloadedData_, downloadPath = downloadPath_, temporaryDownloadPath = temporaryDownloadPath_, downloadFileHandle = downloadFileHandle_, delegateQueue = delegateQueue_, runLoopModes = runLoopModes_, comment = comment_, log = log_, cookieStorage = cookieStorage_; #if NS_BLOCKS_AVAILABLE @synthesize completionBlock = completionBlock_, sentDataBlock = sentDataBlock_, receivedDataBlock = receivedDataBlock_, retryBlock = retryBlock_; #endif @synthesize shouldFetchInBackground = shouldFetchInBackground_; - (NSInteger)cookieStorageMethod { return cookieStorageMethod_; } - (void)setCookieStorageMethod:(NSInteger)method { cookieStorageMethod_ = method; if (method == kGTMHTTPFetcherCookieStorageMethodSystemDefault) { // System default [request_ setHTTPShouldHandleCookies:YES]; // No need for a cookie storage object self.cookieStorage = nil; } else { // Not system default [request_ setHTTPShouldHandleCookies:NO]; if (method == kGTMHTTPFetcherCookieStorageMethodStatic) { // Store cookies in the static array NSAssert(gGTMFetcherStaticCookieStorage != nil, @"cookie storage requires GTMHTTPFetchHistory"); self.cookieStorage = gGTMFetcherStaticCookieStorage; } else if (method == kGTMHTTPFetcherCookieStorageMethodFetchHistory) { // store cookies in the fetch history self.cookieStorage = [fetchHistory_ cookieStorage]; } else { // kGTMHTTPFetcherCookieStorageMethodNone - ignore cookies self.cookieStorage = nil; } } } + (id )staticCookieStorage { return gGTMFetcherStaticCookieStorage; } + (BOOL)doesSupportSentDataCallback { #if GTM_IPHONE // NSURLConnection's didSendBodyData: delegate support appears to be // available starting in iPhone OS 3.0 return (NSFoundationVersionNumber >= 678.47); #else // Per WebKit's MaxFoundationVersionWithoutdidSendBodyDataDelegate // // Indicates if NSURLConnection will invoke the didSendBodyData: delegate // method return (NSFoundationVersionNumber > 677.21); #endif } - (id )fetchHistory { return fetchHistory_; } - (void)setFetchHistory:(id )fetchHistory { [fetchHistory_ autorelease]; fetchHistory_ = [fetchHistory retain]; if (fetchHistory_ != nil) { // set the fetch history's cookie array to be the cookie store [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodFetchHistory]; } else { // The fetch history was removed if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory) { // Fall back to static storage [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic]; } } } - (id)userData { @synchronized(self) { return userData_; } } - (void)setUserData:(id)theObj { @synchronized(self) { [userData_ autorelease]; userData_ = [theObj retain]; } } - (void)setProperties:(NSMutableDictionary *)dict { @synchronized(self) { [properties_ autorelease]; // This copies rather than retains the parameter for compatiblity with // an earlier version that took an immutable parameter and copied it. properties_ = [dict mutableCopy]; } } - (NSMutableDictionary *)properties { @synchronized(self) { return properties_; } } - (void)setProperty:(id)obj forKey:(NSString *)key { @synchronized(self) { if (properties_ == nil && obj != nil) { [self setProperties:[NSMutableDictionary dictionary]]; } [properties_ setValue:obj forKey:key]; } } - (id)propertyForKey:(NSString *)key { @synchronized(self) { return [properties_ objectForKey:key]; } } - (void)addPropertiesFromDictionary:(NSDictionary *)dict { @synchronized(self) { if (properties_ == nil && dict != nil) { [self setProperties:[[dict mutableCopy] autorelease]]; } else { [properties_ addEntriesFromDictionary:dict]; } } } - (void)setCommentWithFormat:(id)format, ... { #if !STRIP_GTM_FETCH_LOGGING NSString *result = format; if (format) { va_list argList; va_start(argList, format); result = [[[NSString alloc] initWithFormat:format arguments:argList] autorelease]; va_end(argList); } [self setComment:result]; #endif } + (Class)connectionClass { if (gGTMFetcherConnectionClass == nil) { gGTMFetcherConnectionClass = [NSURLConnection class]; } return gGTMFetcherConnectionClass; } + (void)setConnectionClass:(Class)theClass { gGTMFetcherConnectionClass = theClass; } #if STRIP_GTM_FETCH_LOGGING + (void)setLoggingEnabled:(BOOL)flag { } #endif // STRIP_GTM_FETCH_LOGGING @end void GTMAssertSelectorNilOrImplementedWithArgs(id obj, SEL sel, ...) { // Verify that the object's selector is implemented with the proper // number and type of arguments #if DEBUG va_list argList; va_start(argList, sel); if (obj && sel) { // Check that the selector is implemented if (![obj respondsToSelector:sel]) { NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed", NSStringFromClass([obj class]), NSStringFromSelector(sel)); NSCAssert(0, @"callback selector unimplemented or misnamed"); } else { const char *expectedArgType; unsigned int argCount = 2; // skip self and _cmd NSMethodSignature *sig = [obj methodSignatureForSelector:sel]; // Check that each expected argument is present and of the correct type while ((expectedArgType = va_arg(argList, const char*)) != 0) { if ([sig numberOfArguments] > argCount) { const char *foundArgType = [sig getArgumentTypeAtIndex:argCount]; if(0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) { NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s", NSStringFromClass([obj class]), NSStringFromSelector(sel), (argCount - 2), expectedArgType); NSCAssert(0, @"callback selector argument type mistake"); } } argCount++; } // Check that the proper number of arguments are present in the selector if (argCount != [sig numberOfArguments]) { NSLog( @"\"%@\" selector \"%@\" should have %d arguments", NSStringFromClass([obj class]), NSStringFromSelector(sel), (argCount - 2)); NSCAssert(0, @"callback selector arguments incorrect"); } } } va_end(argList); #endif } NSString *GTMCleanedUserAgentString(NSString *str) { // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html // and http://www-archive.mozilla.org/build/user-agent-strings.html if (str == nil) return nil; NSMutableString *result = [NSMutableString stringWithString:str]; // Replace spaces with underscores [result replaceOccurrencesOfString:@" " withString:@"_" options:0 range:NSMakeRange(0, [result length])]; // Delete http token separators and remaining whitespace static NSCharacterSet *charsToDelete = nil; if (charsToDelete == nil) { // Make a set of unwanted characters NSString *const kSeparators = @"()<>@,;:\\\"/[]?={}"; NSMutableCharacterSet *mutableChars; mutableChars = [[[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy] autorelease]; [mutableChars addCharactersInString:kSeparators]; charsToDelete = [mutableChars copy]; // hang on to an immutable copy } while (1) { NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete]; if (separatorRange.location == NSNotFound) break; [result deleteCharactersInRange:separatorRange]; }; return result; } NSString *GTMSystemVersionString(void) { NSString *systemString = @""; #if TARGET_OS_MAC && !TARGET_OS_IPHONE // Mac build static NSString *savedSystemString = nil; if (savedSystemString == nil) { // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading // the system plist file. NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist"; NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath]; NSString *versString = [plist objectForKey:@"ProductVersion"]; if ([versString length] == 0) { versString = @"10.?.?"; } savedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString]; } systemString = savedSystemString; #elif TARGET_OS_IPHONE // Compiling against the iPhone SDK static NSString *savedSystemString = nil; if (savedSystemString == nil) { // Avoid the slowness of calling currentDevice repeatedly on the iPhone UIDevice* currentDevice = [UIDevice currentDevice]; NSString *rawModel = [currentDevice model]; NSString *model = GTMCleanedUserAgentString(rawModel); NSString *systemVersion = [currentDevice systemVersion]; savedSystemString = [[NSString alloc] initWithFormat:@"%@/%@", model, systemVersion]; // "iPod_Touch/2.2" } systemString = savedSystemString; #elif (GTL_IPHONE || GDATA_IPHONE) // Compiling iOS libraries against the Mac SDK systemString = @"iPhone/x.x"; #elif defined(_SYS_UTSNAME_H) // Foundation-only build struct utsname unameRecord; uname(&unameRecord); systemString = [NSString stringWithFormat:@"%s/%s", unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1" #endif return systemString; } // Return a generic name and version for the current application; this avoids // anonymous server transactions. NSString *GTMApplicationIdentifier(NSBundle *bundle) { static NSString *sAppID = nil; if (sAppID != nil) return sAppID; // If there's a bundle ID, use that; otherwise, use the process name if (bundle == nil) { bundle = [NSBundle mainBundle]; } NSString *identifier; NSString *bundleID = [bundle bundleIdentifier]; if ([bundleID length] > 0) { identifier = bundleID; } else { // Fall back on the procname, prefixed by "proc" to flag that it's // autogenerated and perhaps unreliable NSString *procName = [[NSProcessInfo processInfo] processName]; identifier = [NSString stringWithFormat:@"proc_%@", procName]; } // Clean up whitespace and special characters identifier = GTMCleanedUserAgentString(identifier); // If there's a version number, append that NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; if ([version length] == 0) { version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"]; } // Clean up whitespace and special characters version = GTMCleanedUserAgentString(version); // Glue the two together (cleanup done above or else cleanup would strip the // slash) if ([version length] > 0) { identifier = [identifier stringByAppendingFormat:@"/%@", version]; } sAppID = [identifier copy]; return sAppID; }