| /* 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 relies on NSURLConnection, which is unavailable for apps built with minimum |
| // targets of iOS 9 and OS X 10.11. |
| // |
| // This fetcher class is deprecated under the OS X 10.12/iOS 10 SDKs unless targeting |
| // OS X 10.8 or iOS 6. |
| #if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_12) \ |
| && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_12 \ |
| && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_9) |
| #error GTMHTTPFetcher is deprecated; use GTMSessionFetcher instead |
| #endif |
| |
| #if (TARGET_OS_IPHONE && defined(__IPHONE_10_0) \ |
| && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 \ |
| && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0) |
| #error GTMHTTPFetcher is deprecated; use GTMSessionFetcher instead |
| #endif |
| |
| #import "GTMHTTPFetcher.h" |
| |
| #if GTM_BACKGROUND_FETCHING |
| #import <UIKit/UIKit.h> |
| #endif |
| |
| #import <sys/utsname.h> |
| |
| 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 |
| |
| NSString *const kGTMHTTPFetcherCompletionInvokedNotification = @"kGTMHTTPFetcherCompletionInvokedNotification"; |
| NSString *const kGTMHTTPFetcherCompletionDataKey = @"data"; |
| NSString *const kGTMHTTPFetcherCompletionErrorKey = @"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 kCallbackTarget = @"target"; |
| static NSString *const kCallbackSelector = @"sel"; |
| static NSString *const kCallbackBlock = @"block"; |
| 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; |
| @property (copy) void (^completionBlock)(NSData *, NSError *); |
| |
| - (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)invokeFetchCallbacksWithTarget:(id)target |
| selector:(SEL)sel |
| block:(id)block |
| data:(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]; |
| } |
| |
| #if DEBUG |
| #if (!TARGET_OS_IPHONE && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_9) \ |
| || (TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0) |
| NSLog(@"GTMHTTPFetcher is deprecated and should be updated to GTMSessionFetcher" |
| @" (inside bundle %@)", |
| [NSBundle bundleForClass:self].bundlePath.lastPathComponent); |
| #endif |
| #endif |
| } |
| } |
| |
| - (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]; |
| [bodyData_ release]; |
| [postStream_ release]; |
| [loggedStreamData_ release]; |
| [response_ release]; |
| [completionBlock_ release]; |
| [receivedDataBlock_ release]; |
| [sentDataBlock_ release]; |
| [retryBlock_ release]; |
| [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]; |
| [allowedInsecureSchemes_ release]; |
| // releaseCallbacks nilifies delegate_ making this release a no-op if releaseCallbacks was called |
| [delegate_ 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; |
| } |
| |
| NSURL *requestURL = [request_ URL]; |
| if (request_ == nil || requestURL == nil) { |
| NSAssert(request_ != nil, @"beginFetchWithDelegate requires a request with a URL"); |
| goto CannotBeginFetch; |
| } |
| |
| #if !GTM_ALLOW_INSECURE_REQUESTS |
| if (requestURL != nil) { |
| // Allow https only for requests, unless overridden by the client. |
| // |
| // Non-https requests may too easily be snooped, so we disallow them by default. |
| // |
| // file: and data: schemes are usually safe if they are hardcoded in the client or provided |
| // by a trusted source, but since it's fairly rare to need them, it's safest to make clients |
| // explicitly allow them. |
| NSString *requestScheme = [requestURL scheme]; |
| BOOL isSecure = ([requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame); |
| if (!isSecure) { |
| BOOL allowRequest = NO; |
| NSString *host = [requestURL host]; |
| BOOL isLocalhost = ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame |
| || [host isEqual:@"::1"] |
| || [host isEqual:@"127.0.0.1"]); |
| if (isLocalhost) { |
| if (allowLocalhostRequest_) { |
| allowRequest = YES; |
| } else { |
| // To fetch from localhost, the fetcher must specifically have the allowLocalhostRequest |
| // property set. |
| #if DEBUG |
| NSAssert(NO, @"Fetch request for localhost but fetcher allowLocalhostRequest" |
| @" is not set: %@", requestURL); |
| #else |
| NSLog(@"Localhost fetch disallowed for %@", requestURL); |
| #endif |
| } |
| } else { |
| // Not localhost; check schemes. |
| for (NSString *allowedScheme in allowedInsecureSchemes_) { |
| if ([requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) { |
| allowRequest = YES; |
| break; |
| } |
| } |
| if (!allowRequest) { |
| // To make a request other than https:, the client must specify an array for the |
| // allowedInsecureSchemes property. |
| #if DEBUG |
| NSAssert(NO, @"Insecure fetch request has a scheme (%@)" |
| @" not found in fetcher allowedInsecureSchemes (%@): %@", |
| requestScheme, allowedInsecureSchemes_, requestURL); |
| #else |
| NSLog(@"Fetch disallowed for %@", requestURL); |
| #endif |
| } |
| } |
| if (!allowRequest) { |
| error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain |
| code:kGTMHTTPFetcherErrorInsecureRequest |
| userInfo:nil]; |
| goto CannotBeginFetch; |
| } |
| } // !isSecure |
| } // requestURL != nil |
| #endif // GTM_ALLOW_INSECURE_REQUESTS |
| |
| self.downloadedData = nil; |
| downloadedLength_ = 0; |
| |
| if (servicePriority_ == NSIntegerMin) { |
| mayDelay = NO; |
| } |
| 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 (bodyData_ || postStream_) { |
| if (isEffectiveHTTPGet) { |
| [request_ setHTTPMethod:@"POST"]; |
| isEffectiveHTTPGet = NO; |
| } |
| |
| if (bodyData_) { |
| [request_ setHTTPBody:bodyData_]; |
| } 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; |
| } else if (delegateQueue == nil && runLoopModes_ == nil && ![NSThread isMainThread]) { |
| // Neither a delegate queue nor runLoopModes were supplied, and we're not on the |
| // main thread, so assume the user really wants callbacks and provide a queue. |
| // |
| // We don't have a way to verify that this thread has a run loop spinning, but |
| // it's fairly rare that a background thread does have one. A client that |
| // does want to rely on spinning a run loop should specify run loop modes. |
| delegateQueue = [NSOperationQueue mainQueue]; |
| self.delegateQueue = delegateQueue; |
| } |
| |
| #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]; |
| } |
| |
| #if DEBUG |
| // For testing only, look for a property indicating the fetch should immediately fail. |
| if ([self propertyForKey:@"_CannotBeginFetch"] != nil) { |
| goto CannotBeginFetch; |
| } |
| #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]; |
| } |
| } |
| |
| - (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]; |
| } |
| |
| - (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; |
| |
| self.completionBlock = nil; |
| self.sentDataBlock = nil; |
| self.receivedDataBlock = nil; |
| self.retryBlock = nil; |
| } |
| |
| // 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_ |
| || completionBlock_ != nil |
| || 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; |
| void (^block)(NSData *, NSError *); |
| |
| // 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_; |
| block = [[completionBlock_ retain] autorelease]; |
| } |
| [self invokeFetchCallbacksWithTarget:target |
| selector:sel |
| block:block |
| data:data |
| error:error]; |
| } |
| |
| - (void)invokeFetchCallbacksWithTarget:(id)target |
| selector:(SEL)sel |
| block:(id)block |
| data:(NSData *)data |
| error:(NSError *)error { |
| [[self retain] autorelease]; // In case the callback releases us |
| |
| [self invokeFetchCallback:sel |
| target:target |
| data:data |
| error:error]; |
| |
| if (block) { |
| ((void (^)(NSData *, NSError *))block)(data, error); |
| } |
| |
| // Post a notification, primarily to allow code to collect responses for |
| // testing. |
| // |
| // The observing code is not likely on the fetcher's callback |
| // queue, so this posts explicitly to the main queue. |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; |
| if (data) { |
| [userInfo setObject:data forKey:kGTMHTTPFetcherCompletionDataKey]; |
| } |
| if (error) { |
| [userInfo setObject:error forKey:kGTMHTTPFetcherCompletionErrorKey]; |
| } |
| NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; |
| [nc postNotificationName:kGTMHTTPFetcherCompletionInvokedNotification |
| object:self |
| userInfo:userInfo]; |
| }); |
| } |
| |
| - (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]; |
| |
| // 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) { |
| id target = delegate_; |
| NSString *sel = finishedSel_ ? NSStringFromSelector(finishedSel_) : nil; |
| void (^block)(NSData *, NSError *) = completionBlock_; |
| |
| [dict setValue:target forKey:kCallbackTarget]; |
| [dict setValue:sel forKey:kCallbackSelector]; |
| [dict setValue:block forKey:kCallbackBlock]; |
| } |
| |
| NSInvocationOperation *op = |
| [[[NSInvocationOperation alloc] initWithTarget:self |
| selector:@selector(invokeOnQueueWithDictionary:) |
| object:dict] autorelease]; |
| [delegateQueue_ addOperation:op]; |
| } |
| |
| - (void)invokeOnQueueWithDictionary:(NSDictionary *)dict { |
| id target = [dict objectForKey:kCallbackTarget]; |
| NSString *selStr = [dict objectForKey:kCallbackSelector]; |
| SEL sel = selStr ? NSSelectorFromString(selStr) : NULL; |
| id block = [dict objectForKey:kCallbackBlock]; |
| |
| NSData *data = [dict objectForKey:kCallbackData]; |
| NSError *error = [dict objectForKey:kCallbackError]; |
| |
| [self invokeFetchCallbacksWithTarget:target |
| selector:sel |
| block:block |
| data: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 (sentDataBlock_) { |
| sentDataBlock_(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); |
| } |
| } |
| } |
| |
| - (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 (receivedDataBlock_) { |
| receivedDataBlock_(downloadedData_); |
| } |
| } // @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 (retryBlock_) { |
| willRetry = retryBlock_(willRetry, error); |
| } |
| } |
| 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_, |
| allowedInsecureSchemes = allowedInsecureSchemes_, |
| allowLocalhostRequest = allowLocalhostRequest_, |
| credential = credential_, |
| proxyCredential = proxyCredential_, |
| bodyData = bodyData_, |
| 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_; |
| |
| @synthesize completionBlock = completionBlock_, |
| sentDataBlock = sentDataBlock_, |
| receivedDataBlock = receivedDataBlock_, |
| retryBlock = retryBlock_; |
| |
| @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_ retain] autorelease]; |
| } |
| } |
| |
| - (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_ retain] autorelease]; |
| } |
| } |
| |
| - (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] retain] autorelease]; |
| } |
| } |
| |
| - (void)addPropertiesFromDictionary:(NSDictionary *)dict { |
| @synchronized(self) { |
| if (properties_ == nil && dict != nil) { |
| [self setProperties:[[dict mutableCopy] autorelease]]; |
| } else { |
| [properties_ addEntriesFromDictionary:dict]; |
| } |
| } |
| } |
| |
| - (NSData *)postData { |
| #if DEBUG |
| NSLog(@"*** GTMHTTPFetcher: postData is deprecated; use bodyData property instead ***"); |
| #endif |
| return self.bodyData; |
| } |
| |
| - (void)setPostData:(NSData *)data { |
| #if DEBUG |
| NSLog(@"*** GTMHTTPFetcher: postData is deprecated; use bodyData property instead ***"); |
| #endif |
| self.bodyData = data; |
| } |
| |
| - (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 { |
| } |
| |
| + (BOOL)isLoggingEnabled { |
| return NO; |
| } |
| #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 and commas with underscores |
| [result replaceOccurrencesOfString:@" " |
| withString:@"_" |
| options:0 |
| range:NSMakeRange(0, [result length])]; |
| [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 *model = [currentDevice model]; |
| NSString *cleanedModel = GTMCleanedUserAgentString(model); |
| NSString *systemVersion = [currentDevice systemVersion]; |
| |
| #if TARGET_IPHONE_SIMULATOR |
| NSString *hardwareModel = @"sim"; |
| #else |
| NSString *hardwareModel; |
| struct utsname unameRecord; |
| if (uname(&unameRecord) == 0) { |
| NSString *machineName = [NSString stringWithCString:unameRecord.machine |
| encoding:NSUTF8StringEncoding]; |
| hardwareModel = GTMCleanedUserAgentString(machineName); |
| } else { |
| hardwareModel = @"unk"; |
| } |
| #endif |
| savedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@", |
| cleanedModel, systemVersion, hardwareModel]; |
| // Example: iPod_Touch/2.2 hw/iPod1_1 |
| } |
| 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; |
| } |
| } |