aboutsummaryrefslogtreecommitdiffhomepage
path: root/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m
diff options
context:
space:
mode:
Diffstat (limited to 'example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m')
-rwxr-xr-xexample/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m1956
1 files changed, 1956 insertions, 0 deletions
diff --git a/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m
new file mode 100755
index 00000000..ebbdd0de
--- /dev/null
+++ b/example/common/gtm-oauth2/HTTPFetcher/GTMHTTPFetcher.m
@@ -0,0 +1,1956 @@
+/* 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 <UIKit/UIKit.h>
+#endif
+
+static id <GTMCookieStorageProtocol> 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 <GTMCookieStorageProtocol> 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 <GTMFetcherAuthorizationProtocol>)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 <GTMFetcherAuthorizationProtocol>)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 <GTMHTTPFetcherServiceProtocol> 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 <GTMCookieStorageProtocol>)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 <GTMHTTPFetchHistoryProtocol>)fetchHistory {
+ return fetchHistory_;
+}
+
+- (void)setFetchHistory:(id <GTMHTTPFetchHistoryProtocol>)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;
+}