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