/core/externals/update-engine/externals/gdata-objectivec-client/Source/HTTPFetcher/GTMHTTPFetcher.m
Objective C | 2007 lines | 1387 code | 322 blank | 298 comment | 288 complexity | 88f4f5798e8f1a7d6dabe2653776642e MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, GPL-2.0
- /* 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
- //
- #import "GTMHTTPFetcher.h"
- #if GTM_BACKGROUND_FETCHING
- #import <UIKit/UIKit.h>
- #endif
- static id <GTMCookieStorageProtocol> gGTMFetcherStaticCookieStorage = nil;
- static Class gGTMFetcherConnectionClass = nil;
- NSString *const kGTMHTTPFetcherStartedNotification = @"kGTMHTTPFetcherStartedNotification";
- NSString *const kGTMHTTPFetcherStoppedNotification = @"kGTMHTTPFetcherStoppedNotification";
- NSString *const kGTMHTTPFetcherRetryDelayStartedNotification = @"kGTMHTTPFetcherRetryDelayStartedNotification";
- NSString *const kGTMHTTPFetcherRetryDelayStoppedNotification = @"kGTMHTTPFetcherRetryDelayStoppedNotification";
- NSString *const kGTMHTTPFetcherErrorDomain = @"com.google.GTMHTTPFetcher";
- NSString *const kGTMHTTPFetcherStatusDomain = @"com.google.HTTPStatus";
- NSString *const kGTMHTTPFetcherErrorChallengeKey = @"challenge";
- NSString *const kGTMHTTPFetcherStatusDataKey = @"data"; // data returned with a kGTMHTTPFetcherStatusDomain error
- // 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)invokeOnQueueWithDictionary:(NSDictionary *)dict;
- - (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;
- - (void)logNowWithError:(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];
- [initialRequestDate_ 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
- if (!initialRequestDate_) {
- initialRequestDate_ = [[NSDate alloc] init];
- }
- // 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
- // If -stopFetching is called in another thread directly after this @synchronized stanza finishes
- // on this thread, then target and block could be released before being used in this method. So
- // retain each until this method is done with them.
- @synchronized(self) {
- target = [[delegate_ retain] autorelease];
- sel = finishedSel_;
- #if NS_BLOCKS_AVAILABLE
- block = [[completionBlock_ retain] autorelease];
- #endif
- }
- [[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;
- @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) {
- #if !STRIP_GTM_FETCH_LOGGING
- // Log the pre-cache response.
- [self logNowWithError:nil];
- hasLoggedError_ = YES;
- #endif
- 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 !STRIP_GTM_FETCH_LOGGING
- if (!hasLoggedError_) {
- [self logNowWithError:nil];
- hasLoggedError_ = YES;
- }
- #endif
- // 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];
- }
- #if !STRIP_GTM_FETCH_LOGGING
- @synchronized(self) {
- if (!shouldDeferLogging && !hasLoggedError_) {
- [self logNowWithError:nil];
- }
- }
- #endif
- }
- - (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]);
- if (shouldDoIntervalRetry) {
- // If an explicit max retry interval was set, we expect repeated backoffs to take
- // up to roughly twice that for repeated fast failures. If the initial attempt is
- // already more than 3 times the max retry interval, then failures have taken a long time
- // (such as from network timeouts) so don't retry again to avoid the app becoming
- // unexpectedly unresponsive.
- if (maxRetryInterval_ > kUnsetMaxRetryInterval) {
- NSTimeInterval maxAllowedIntervalBeforeRetry = maxRetryInterval_ * 3;
- NSTimeInterval timeSinceInitialRequest = -[initialRequestDate_ timeIntervalSinceNow];
- if (timeSinceInitialRequest > maxAllowedIntervalBeforeRetry) {
- shouldDoIntervalRetry = NO;
- }
- }
- }
- 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) {
- @synchronized([GTMHTTPFetcher class]) {
- static NSMutableDictionary *sAppIDMap = nil;
- // If there's a bundle ID, use that; otherwise, use the process name
- if (bundle == nil) {
- bundle = [NSBundle mainBundle];
- }
- NSString *bundleID = [bundle bundleIdentifier];
- if (bundleID == nil) {
- bundleID = @"";
- }
- NSString *identifier = [sAppIDMap objectForKey:bundleID];
- if (identifier) return identifier;
- // Apps may add a string to the info.plist to uniquely identify different builds.
- identifier = [bundle objectForInfoDictionaryKey:@"GTMUserAgentID"];
- if ([identifier length] == 0) {
- 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];
- }
- if (sAppIDMap == nil) {
- sAppIDMap = [[NSMutableDictionary alloc] init];
- }
- [sAppIDMap setObject:identifier forKey:bundleID];
- return identifier;
- }
- }