aboutsummaryrefslogtreecommitdiff
path: root/Foundation/GTMHTTPFetcher.m
diff options
context:
space:
mode:
Diffstat (limited to 'Foundation/GTMHTTPFetcher.m')
-rw-r--r--Foundation/GTMHTTPFetcher.m1978
1 files changed, 0 insertions, 1978 deletions
diff --git a/Foundation/GTMHTTPFetcher.m b/Foundation/GTMHTTPFetcher.m
deleted file mode 100644
index 1c6720a..0000000
--- a/Foundation/GTMHTTPFetcher.m
+++ /dev/null
@@ -1,1978 +0,0 @@
-//
-// GTMHTTPFetcher.m
-//
-// Copyright 2007-2008 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.
-//
-
-#define GTMHTTPFETCHER_DEFINE_GLOBALS 1
-
-#import "GTMHTTPFetcher.h"
-#import "GTMDebugSelectorValidation.h"
-#import "GTMGarbageCollection.h"
-#import "GTMSystemVersion.h"
-
-@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal)
-- (void)logFetchWithError:(NSError *)error;
-- (void)logCapturePostStream;
-@end
-
-// Make sure that if logging is disabled, the InputStream logging is also
-// diabled.
-#if !GTM_HTTPFETCHER_ENABLE_LOGGING
-# undef GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
-# define GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING 0
-#endif // GTM_HTTPFETCHER_ENABLE_LOGGING
-
-#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
-#import "GTMProgressMonitorInputStream.h"
-@interface GTMInputStreamLogger : GTMProgressMonitorInputStream
-// GTMInputStreamLogger wraps any NSInputStream used for uploading so we can
-// capture a copy of the data for the log
-@end
-#endif // !GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
-
-#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
-@interface NSURLConnection (LeopardMethodsOnTigerBuilds)
-- (id)initWithRequest:(NSURLRequest *)request
- delegate:(id)delegate
- startImmediately:(BOOL)startImmediately;
-- (void)start;
-- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
-@end
-#endif
-
-NSString* const kGTMLastModifiedHeader = @"Last-Modified";
-NSString* const kGTMIfModifiedSinceHeader = @"If-Modified-Since";
-
-
-static NSMutableArray* gGTMFetcherStaticCookies = nil;
-static Class gGTMFetcherConnectionClass = nil;
-static NSArray *gGTMFetcherDefaultRunLoopModes = nil;
-
-const NSTimeInterval kDefaultMaxRetryInterval = 60. * 10.; // 10 minutes
-
-@interface GTMHTTPFetcher (PrivateMethods)
-- (void)setCookies:(NSArray *)newCookies
- inArray:(NSMutableArray *)cookieStorageArray;
-- (NSArray *)cookiesForURL:(NSURL *)theURL
- inArray:(NSMutableArray *)cookieStorageArray;
-- (void)handleCookiesForResponse:(NSURLResponse *)response;
-- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error;
-- (void)retryTimerFired:(NSTimer *)timer;
-- (void)destroyRetryTimer;
-- (void)beginRetryTimer;
-- (void)primeTimerWithNewTimeInterval:(NSTimeInterval)secs;
-- (void)retryFetch;
-@end
-
-@implementation GTMHTTPFetcher
-
-+ (GTMHTTPFetcher *)httpFetcherWithRequest:(NSURLRequest *)request {
- return [[[GTMHTTPFetcher alloc] initWithRequest:request] autorelease];
-}
-
-+ (void)initialize {
- if (!gGTMFetcherStaticCookies) {
- gGTMFetcherStaticCookies = [[NSMutableArray alloc] init];
- GTMNSMakeUncollectable(gGTMFetcherStaticCookies);
- }
-}
-
-- (id)init {
- return [self initWithRequest:nil];
-}
-
-- (id)initWithRequest:(NSURLRequest *)request {
- if ((self = [super init]) != nil) {
-
- request_ = [request mutableCopy];
-
- [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic];
- }
- return self;
-}
-
-// TODO: do we need finalize to call stopFetching?
-
-- (void)dealloc {
- [self stopFetching]; // releases connection_
-
- [request_ release];
- [downloadedData_ release];
- [credential_ release];
- [proxyCredential_ release];
- [postData_ release];
- [postStream_ release];
- [loggedStreamData_ release];
- [response_ release];
- [userData_ release];
- [properties_ release];
- [runLoopModes_ release];
- [fetchHistory_ release];
- [self destroyRetryTimer];
-
- [super dealloc];
-}
-
-#pragma mark -
-
-// Begin fetching the URL. |delegate| is not retained
-// The delegate must provide and implement the finished and failed selectors.
-//
-// finishedSEL has a signature like:
-// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data
-// failedSEL has a signature like:
-// - (void)fetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error
-
-- (BOOL)beginFetchWithDelegate:(id)delegate
- didFinishSelector:(SEL)finishedSEL
- didFailSelector:(SEL)failedSEL {
-
- GTMAssertSelectorNilOrImplementedWithArguments(delegate, finishedSEL,
- @encode(GTMHTTPFetcher *),
- @encode(NSData *), NULL);
- GTMAssertSelectorNilOrImplementedWithArguments(delegate, failedSEL,
- @encode(GTMHTTPFetcher *),
- @encode(NSError *), NULL);
- GTMAssertSelectorNilOrImplementedWithArguments(delegate, receivedDataSEL_,
- @encode(GTMHTTPFetcher *),
- @encode(NSData *), NULL);
- GTMAssertSelectorNilOrImplementedWithReturnTypeAndArguments(delegate, retrySEL_,
- @encode(BOOL),
- @encode(GTMHTTPFetcher *),
- @encode(BOOL),
- @encode(NSError *),
- NULL);
-
- if (connection_ != nil) {
- // COV_NF_START - since we want the assert, we can't really test this
- _GTMDevAssert(connection_ != nil,
- @"fetch object %@ being reused; this should never happen",
- self);
- goto CannotBeginFetch;
- // COV_NF_END
- }
-
- if (request_ == nil) {
- _GTMDevLog(@"beginFetchWithDelegate requires a request");
- goto CannotBeginFetch;
- }
-
- [downloadedData_ release];
- downloadedData_ = nil;
-
- [self setDelegate:delegate];
- finishedSEL_ = finishedSEL;
- failedSEL_ = failedSEL;
-
- if (postData_ || postStream_) {
- if ([request_ HTTPMethod] == nil ||
- [[request_ HTTPMethod] isEqual:@"GET"]) {
- [request_ setHTTPMethod:@"POST"];
- }
-
- if (postData_) {
- [request_ setHTTPBody:postData_];
- } else {
-
- // if logging is enabled, it needs a buffer to accumulate data from any
- // NSInputStream used for uploading. Logging will wrap the input
- // stream with a stream that lets us keep a copy the data being read.
- if ([GTMHTTPFetcher isLoggingEnabled] && postStream_ != nil) {
- loggedStreamData_ = [[NSMutableData alloc] init];
- [self logCapturePostStream];
- }
-
- [request_ setHTTPBodyStream:postStream_];
- }
- }
-
- if (fetchHistory_) {
-
- // If this URL is in the history, set the Last-Modified header field
-
- // if we have a history, we're tracking across fetches, so we don't
- // want to pull results from a cache
- [request_ setCachePolicy:NSURLRequestReloadIgnoringCacheData];
-
- NSDictionary* lastModifiedDict
- = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryLastModifiedKey];
- NSString* urlString = [[request_ URL] absoluteString];
- NSString* lastModifiedStr = [lastModifiedDict objectForKey:urlString];
-
- // servers don't want last-modified-ifs on POSTs, so check for a body
- if (lastModifiedStr
- && [request_ HTTPBody] == nil
- && [request_ HTTPBodyStream] == nil) {
-
- [request_ addValue:lastModifiedStr
- forHTTPHeaderField:kGTMIfModifiedSinceHeader];
- }
- }
-
- // get cookies for this URL from our storage array, if
- // we have a storage array
- if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault) {
-
- NSArray *cookies = [self cookiesForURL:[request_ URL]];
-
- if ([cookies count]) {
-
- NSDictionary *headerFields
- = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
- NSString *cookieHeader
- = [headerFields objectForKey:@"Cookie"]; // key used in header dict
- if (cookieHeader) {
- [request_ addValue:cookieHeader
- forHTTPHeaderField:@"Cookie"]; // header name
- }
- }
- }
-
- // finally, start the connection
-
- Class connectionClass = [[self class] connectionClass];
-
- NSArray *runLoopModes = nil;
-
- if ([[self class] doesSupportRunLoopModes]) {
-
- // use the connection-specific run loop modes, if they were provided,
- // or else use the GTMHTTPFetcher default run loop modes, if any
- if (runLoopModes_) {
- runLoopModes = runLoopModes_;
- } else {
- runLoopModes = gGTMFetcherDefaultRunLoopModes;
- }
- }
-
- if ([runLoopModes count] == 0) {
-
- // if no run loop modes were specified, then we'll start the connection
- // on the current run loop in the current mode
- connection_ = [[connectionClass connectionWithRequest:request_
- delegate:self] retain];
- } else {
-
- // schedule on current run loop in the specified modes
- connection_ = [[connectionClass alloc] initWithRequest:request_
- delegate:self
- startImmediately:NO];
-
- for (NSUInteger idx = 0; idx < [runLoopModes count]; idx++) {
- NSString *mode = [runLoopModes objectAtIndex:idx];
- [connection_ scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:mode];
- }
- [connection_ start];
- }
-
- if (!connection_) {
- // COV_NF_START - can't really create this case
- _GTMDevLog(@"beginFetchWithDelegate could not create a connection");
- goto CannotBeginFetch;
- // COV_NF_END
- }
-
- // we'll retain the delegate only during the outstanding connection (similar
- // to what Cocoa does with performSelectorOnMainThread:) since we'd crash
- // if the delegate was released in the interim. We don't retain the selector
- // at other times, to avoid vicious retain loops. This retain is balanced in
- // the -stopFetch method.
- [delegate_ retain];
-
- downloadedData_ = [[NSMutableData alloc] init];
- return YES;
-
-CannotBeginFetch:
-
- if (failedSEL) {
-
- NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
- code:kGTMHTTPFetcherErrorDownloadFailed
- userInfo:nil];
-
- [[self retain] autorelease]; // in case the callback releases us
-
- [delegate performSelector:failedSEL
- withObject:self
- withObject:error];
- }
-
- return NO;
-}
-
-// Returns YES if this is in the process of fetching a URL, or waiting to
-// retry
-- (BOOL)isFetching {
- return (connection_ != nil || retryTimer_ != nil);
-}
-
-// 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;
-}
-
-// Cancel the fetch of the URL that's currently in progress.
-- (void)stopFetching {
- [self destroyRetryTimer];
-
- 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;
-
- // this may be called in a callback from the connection, so use autorelease
- [oldConnection cancel];
- [oldConnection autorelease];
-
- // balance the retain done when the connection was opened
- [delegate_ release];
- }
-}
-
-- (void)retryFetch {
-
- id holdDelegate = [[delegate_ retain] autorelease];
-
- [self stopFetching];
-
- [self beginFetchWithDelegate:holdDelegate
- didFinishSelector:finishedSEL_
- didFailSelector:failedSEL_];
-}
-
-#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 {
-
- if (redirectRequest && 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];
- if (redirectHeaders) {
- NSString *key;
- GTM_FOREACH_KEY(key, redirectHeaders) {
- NSString *value = [redirectHeaders objectForKey:key];
- [newRequest setValue:value forHTTPHeaderField:key];
- }
- }
- redirectRequest = newRequest;
-
- // save cookies from the response
- [self handleCookiesForResponse:redirectResponse];
-
- // log the response we just received
- [self setResponse:redirectResponse];
- [self logFetchWithError:nil];
-
- // update the request for future logging
- [self setRequest:redirectRequest];
- }
- return redirectRequest;
-}
-
-- (void)connection:(NSURLConnection *)connection
- didReceiveResponse:(NSURLResponse *)response {
-
- // 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];
-
- [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) {
-
- // do nothing special for NSURLConnection's default storage mechanism
-
- } 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) {
-
- NSMutableArray *cookieArray = nil;
-
- // static cookies are stored in gGTMFetcherStaticCookies; fetchHistory
- // cookies are stored in fetchHistory_'s kGTMHTTPFetcherHistoryCookiesKey
-
- if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodStatic) {
-
- cookieArray = gGTMFetcherStaticCookies;
-
- } else if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory
- && fetchHistory_ != nil) {
-
- cookieArray
- = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryCookiesKey];
- if (cookieArray == nil) {
- cookieArray = [NSMutableArray array];
- [fetchHistory_ setObject:cookieArray
- forKey:kGTMHTTPFetcherHistoryCookiesKey];
- }
- }
-
- if (cookieArray) {
- @synchronized(cookieArray) {
- [self setCookies:cookies inArray:cookieArray];
- }
- }
- }
- }
- }
-}
-
--(void)connection:(NSURLConnection *)connection
- didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
-
- 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;
- }
- }
-
- // If we don't have credentials, or we've already failed auth 3x, give up and
- // report the error, putting the challenge as a value in the userInfo
- // dictionary
- // Store the challenge first to ensure that it lives past being cancelled.
- NSDictionary *userInfo
- = [NSDictionary dictionaryWithObject:challenge
- forKey:kGTMHTTPFetcherErrorChallengeKey];
- [[challenge sender] cancelAuthenticationChallenge:challenge];
-
- NSError *error
- = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
- code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed
- userInfo:userInfo];
-
- [self connection:connection didFailWithError:error];
-}
-
-
-
-- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
- [downloadedData_ appendData:data];
-
- if (receivedDataSEL_) {
- [delegate_ performSelector:receivedDataSEL_
- withObject:self
- withObject:downloadedData_];
- }
-}
-
-- (void)updateFetchHistory {
-
- if (fetchHistory_) {
-
- NSString* urlString = [[request_ URL] absoluteString];
- if ([response_ respondsToSelector:@selector(allHeaderFields)]) {
- NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields];
- NSString* lastModifiedStr = [headers objectForKey:kGTMLastModifiedHeader];
-
- // get the dictionary mapping URLs to last-modified dates
- NSMutableDictionary* lastModifiedDict
- = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryLastModifiedKey];
- if (!lastModifiedDict) {
- lastModifiedDict = [NSMutableDictionary dictionary];
- [fetchHistory_ setObject:lastModifiedDict
- forKey:kGTMHTTPFetcherHistoryLastModifiedKey];
- }
-
- NSMutableDictionary* datedDataCache = nil;
- if (shouldCacheDatedData_) {
- // get the dictionary mapping URLs to cached, dated data
- datedDataCache
- = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryDatedDataKey];
- if (!datedDataCache) {
- datedDataCache = [NSMutableDictionary dictionary];
- [fetchHistory_ setObject:datedDataCache
- forKey:kGTMHTTPFetcherHistoryDatedDataKey];
- }
- }
-
- NSInteger statusCode = [self statusCode];
- if (statusCode != kGTMHTTPFetcherStatusNotModified) {
-
- // save this last modified date string for successful results (<300)
- // If there's no last modified string, clear the dictionary
- // entry for this URL. Also cache or delete the data, if appropriate
- // (when datedDataCache is non-nil.)
- if (lastModifiedStr && statusCode < 300) {
- [lastModifiedDict setObject:lastModifiedStr forKey:urlString];
- if (downloadedData_) {
- [datedDataCache setObject:downloadedData_ forKey:urlString];
- } else {
- [datedDataCache removeObjectForKey:urlString];
- }
- } else {
- [lastModifiedDict removeObjectForKey:urlString];
- [datedDataCache removeObjectForKey:urlString];
- }
- }
- }
- }
-}
-
-// 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 to downloadedData_.
-// For other errors or if there's no cached data, just return the actual status.
-- (NSInteger)statusAfterHandlingNotModifiedError {
-
- NSInteger status = [self statusCode];
- if (status == kGTMHTTPFetcherStatusNotModified && shouldCacheDatedData_) {
-
- // get the dictionary of URLs and data
- NSString* urlString = [[request_ URL] absoluteString];
-
- NSDictionary* datedDataCache
- = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryDatedDataKey];
- NSData* cachedData = [datedDataCache objectForKey:urlString];
-
- if (cachedData) {
- // copy our stored data, and forge the status to pass on to the delegate
- [downloadedData_ setData:cachedData];
- status = 200;
- }
- }
- return status;
-}
-
-- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
-
- [self updateFetchHistory];
-
- [[self retain] autorelease]; // in case the callback releases us
-
- [self logFetchWithError:nil];
-
- NSInteger status = [self statusAfterHandlingNotModifiedError];
-
- if (status >= 300) {
-
- if ([self shouldRetryNowForStatus:status error:nil]) {
-
- [self beginRetryTimer];
-
- } else {
- // not retrying
-
- // did they want failure notifications?
- if (failedSEL_) {
-
- NSDictionary *userInfo =
- [NSDictionary dictionaryWithObject:downloadedData_
- forKey:kGTMHTTPFetcherStatusDataKey];
- NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
- code:status
- userInfo:userInfo];
-
- [delegate_ performSelector:failedSEL_
- withObject:self
- withObject:error];
- }
- // we're done fetching
- [self stopFetching];
- }
-
- } else if (finishedSEL_) {
-
- // successful http status (under 300)
- [delegate_ performSelector:finishedSEL_
- withObject:self
- withObject:downloadedData_];
- [self stopFetching];
- }
-
-}
-
-- (void)connection:(NSURLConnection *)connection
- didFailWithError:(NSError *)error {
-
- [self logFetchWithError:error];
-
- if ([self shouldRetryNowForStatus:0 error:error]) {
-
- [self beginRetryTimer];
-
- } else {
-
- if (failedSEL_) {
- [[self retain] autorelease]; // in case the callback releases us
-
- [delegate_ performSelector:failedSEL_
- withObject:self
- withObject:error];
- }
-
- [self stopFetching];
- }
-}
-
-#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 }
- };
-
- BOOL isGood = NO;
- // 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) {
-
- isGood = YES;
- break;
- }
- }
- return isGood;
-}
-
-
-// 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.
-- (BOOL)shouldRetryNowForStatus:(NSInteger)status
- error:(NSError *)error {
-
- if ([self isRetryEnabled]) {
-
- if ([self nextRetryInterval] < [self maxRetryInterval]) {
-
- if (error == nil) {
- // make an error for the status
- error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
- code:status
- userInfo:nil];
- }
-
- BOOL willRetry = [self isRetryError:error];
-
- if (retrySEL_) {
- NSMethodSignature *signature = [delegate_ methodSignatureForSelector:retrySEL_];
- NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
- [invocation setSelector:retrySEL_];
- [invocation setTarget:delegate_];
- [invocation setArgument:&self atIndex:2];
- [invocation setArgument:&willRetry atIndex:3];
- [invocation setArgument:&error atIndex:4];
- [invocation invoke];
-
- [invocation getReturnValue:&willRetry];
- }
-
- return willRetry;
- }
- }
-
- return NO;
-}
-
-- (void)beginRetryTimer {
-
- NSTimeInterval nextInterval = [self nextRetryInterval];
- NSTimeInterval maxInterval = [self maxRetryInterval];
-
- NSTimeInterval newInterval = MIN(nextInterval, maxInterval);
-
- [self primeTimerWithNewTimeInterval:newInterval];
-}
-
-- (void)primeTimerWithNewTimeInterval:(NSTimeInterval)secs {
-
- [self destroyRetryTimer];
-
- lastRetryInterval_ = secs;
-
- retryTimer_ = [NSTimer scheduledTimerWithTimeInterval:secs
- target:self
- selector:@selector(retryTimerFired:)
- userInfo:nil
- repeats:NO];
- [retryTimer_ retain];
-}
-
-- (void)retryTimerFired:(NSTimer *)timer {
-
- [self destroyRetryTimer];
-
- retryCount_++;
-
- [self retryFetch];
-}
-
-- (void)destroyRetryTimer {
-
- [retryTimer_ invalidate];
- [retryTimer_ autorelease];
- retryTimer_ = nil;
-}
-
-- (unsigned int)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)setIsRetryEnabled:(BOOL)flag {
-
- if (flag && !isRetryEnabled_) {
- // We defer initializing these until the user calls setIsRetryEnabled
- // to avoid seeding the random number generator if it's not needed.
- // However, it means min and max intervals for this fetcher are reset
- // as a side effect of calling setIsRetryEnabled.
- //
- // seed the random value, and make an initial retry interval
- // random between 1.0 and 2.0 seconds
- srandomdev();
- [self setMinRetryInterval:0.0];
- [self setMaxRetryInterval:kDefaultMaxRetryInterval];
- [self setRetryFactor:2.0];
- lastRetryInterval_ = 0.0;
- }
- isRetryEnabled_ = flag;
-};
-
-- (SEL)retrySelector {
- return retrySEL_;
-}
-
-- (void)setRetrySelector:(SEL)theSelector {
- retrySEL_ = theSelector;
-}
-
-- (NSTimeInterval)maxRetryInterval {
- return maxRetryInterval_;
-}
-
-- (void)setMaxRetryInterval:(NSTimeInterval)secs {
- if (secs > 0) {
- maxRetryInterval_ = secs;
- } else {
- maxRetryInterval_ = kDefaultMaxRetryInterval;
- }
-}
-
-- (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)(random() & 0x0FFFF) / (double) 0x0FFFF);
- }
-}
-
-- (double)retryFactor {
- return retryFactor_;
-}
-
-- (void)setRetryFactor:(double)multiplier {
- retryFactor_ = multiplier;
-}
-
-#pragma mark Getters and Setters
-
-- (NSMutableURLRequest *)request {
- return request_;
-}
-
-- (void)setRequest:(NSURLRequest *)theRequest {
- [request_ autorelease];
- request_ = [theRequest mutableCopy];
-}
-
-- (NSURLCredential *)credential {
- return credential_;
-}
-
-- (void)setCredential:(NSURLCredential *)theCredential {
- [credential_ autorelease];
- credential_ = [theCredential retain];
-}
-
-- (NSURLCredential *)proxyCredential {
- return proxyCredential_;
-}
-
-- (void)setProxyCredential:(NSURLCredential *)theCredential {
- [proxyCredential_ autorelease];
- proxyCredential_ = [theCredential retain];
-}
-
-- (NSData *)postData {
- return postData_;
-}
-
-- (void)setPostData:(NSData *)theData {
- [postData_ autorelease];
- postData_ = [theData retain];
-}
-
-- (NSInputStream *)postStream {
- return postStream_;
-}
-
-- (void)setPostStream:(NSInputStream *)theStream {
- [postStream_ autorelease];
- postStream_ = [theStream retain];
-}
-
-- (GTMHTTPFetcherCookieStorageMethod)cookieStorageMethod {
- return cookieStorageMethod_;
-}
-
-- (void)setCookieStorageMethod:(GTMHTTPFetcherCookieStorageMethod)method {
-
- cookieStorageMethod_ = method;
-
- BOOL handleCookies
- = (method == kGTMHTTPFetcherCookieStorageMethodSystemDefault) ? YES : NO;
- [request_ setHTTPShouldHandleCookies:handleCookies];
-}
-
-- (id)delegate {
- return delegate_;
-}
-
-- (void)setDelegate:(id)theDelegate {
-
- // we retain delegate_ only during the life of the connection
- if (connection_) {
- [delegate_ autorelease];
- delegate_ = [theDelegate retain];
- } else {
- delegate_ = theDelegate;
- }
-}
-
-- (SEL)receivedDataSelector {
- return receivedDataSEL_;
-}
-
-- (void)setReceivedDataSelector:(SEL)theSelector {
- receivedDataSEL_ = theSelector;
-}
-
-- (NSURLResponse *)response {
- return response_;
-}
-
-- (void)setResponse:(NSURLResponse *)response {
- [response_ autorelease];
- response_ = [response retain];
-}
-
-- (NSMutableDictionary *)fetchHistory {
- return fetchHistory_;
-}
-
-- (void)setFetchHistory:(NSMutableDictionary *)fetchHistory {
- [fetchHistory_ autorelease];
- fetchHistory_ = [fetchHistory retain];
- GTMHTTPFetcherCookieStorageMethod method
- = fetchHistory_ ? kGTMHTTPFetcherCookieStorageMethodFetchHistory
- : kGTMHTTPFetcherCookieStorageMethodStatic;
- [self setCookieStorageMethod:method];
-}
-
-- (void)setShouldCacheDatedData:(BOOL)flag {
- shouldCacheDatedData_ = flag;
- if (!flag) {
- [self clearDatedDataHistory];
- }
-}
-
-- (BOOL)shouldCacheDatedData {
- return shouldCacheDatedData_;
-}
-
-// delete last-modified dates and cached data from the fetch history
-- (void)clearDatedDataHistory {
- [fetchHistory_ removeObjectForKey:kGTMHTTPFetcherHistoryLastModifiedKey];
- [fetchHistory_ removeObjectForKey:kGTMHTTPFetcherHistoryDatedDataKey];
-}
-
-- (id)userData {
- return userData_;
-}
-
-- (void)setUserData:(id)theObj {
- [userData_ autorelease];
- userData_ = [theObj retain];
-}
-
-- (void)setProperties:(NSDictionary *)dict {
- [properties_ autorelease];
- properties_ = [dict mutableCopy];
-}
-
-- (NSDictionary *)properties {
- return properties_;
-}
-
-- (void)setProperty:(id)obj forKey:(NSString *)key {
-
- if (properties_ == nil && obj != nil) {
- properties_ = [[NSMutableDictionary alloc] init];
- }
-
- [properties_ setValue:obj forKey:key];
-}
-
-- (id)propertyForKey:(NSString *)key {
- return [properties_ objectForKey:key];
-}
-
-- (NSArray *)runLoopModes {
- return runLoopModes_;
-}
-
-- (void)setRunLoopModes:(NSArray *)modes {
- [runLoopModes_ autorelease];
- runLoopModes_ = [modes retain];
-}
-
-+ (BOOL)doesSupportRunLoopModes {
- SEL sel = @selector(initWithRequest:delegate:startImmediately:);
- return [NSURLConnection instancesRespondToSelector:sel];
-}
-
-+ (NSArray *)defaultRunLoopModes {
- return gGTMFetcherDefaultRunLoopModes;
-}
-
-+ (void)setDefaultRunLoopModes:(NSArray *)modes {
- [gGTMFetcherDefaultRunLoopModes autorelease];
- gGTMFetcherDefaultRunLoopModes = [modes retain];
- GTMNSMakeUncollectable(gGTMFetcherDefaultRunLoopModes);
-}
-
-+ (Class)connectionClass {
- if (gGTMFetcherConnectionClass == nil) {
- gGTMFetcherConnectionClass = [NSURLConnection class];
- }
- return gGTMFetcherConnectionClass;
-}
-
-+ (void)setConnectionClass:(Class)theClass {
- gGTMFetcherConnectionClass = theClass;
-}
-
-#pragma mark Cookies
-
-// return a cookie from the array with the same name, domain, and path as the
-// given cookie, or else return nil if none found
-//
-// Both the cookie being tested and all cookies in cookieStorageArray should
-// be valid (non-nil name, domains, paths)
-- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie
- inArray:(NSArray *)cookieStorageArray {
-
- NSUInteger numberOfCookies = [cookieStorageArray count];
- NSString *name = [cookie name];
- NSString *domain = [cookie domain];
- NSString *path = [cookie path];
-
- _GTMDevAssert(name && domain && path,
- @"Invalid cookie (name:%@ domain:%@ path:%@)",
- name, domain, path);
-
- for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
-
- NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx];
-
- if ([[storedCookie name] isEqual:name]
- && [[storedCookie domain] isEqual:domain]
- && [[storedCookie path] isEqual:path]) {
-
- return storedCookie;
- }
- }
- return nil;
-}
-
-// remove any expired cookies from the array, excluding cookies with nil
-// expirations
-- (void)removeExpiredCookiesInArray:(NSMutableArray *)cookieStorageArray {
-
- // count backwards since we're deleting items from the array
- for (NSInteger idx = [cookieStorageArray count] - 1; idx >= 0; idx--) {
-
- NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx];
-
- NSDate *expiresDate = [storedCookie expiresDate];
- if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
- [cookieStorageArray removeObjectAtIndex:idx];
- }
- }
-}
-
-
-// retrieve all cookies appropriate for the given URL, considering
-// domain, path, cookie name, expiration, security setting.
-// Side effect: removed expired cookies from the storage array
-- (NSArray *)cookiesForURL:(NSURL *)theURL
- inArray:(NSMutableArray *)cookieStorageArray {
-
- [self removeExpiredCookiesInArray:cookieStorageArray];
-
- NSMutableArray *foundCookies = [NSMutableArray array];
-
- // we'll prepend "." to the desired domain, since we want the
- // actual domain "nytimes.com" to still match the cookie domain ".nytimes.com"
- // when we check it below with hasSuffix
- NSString *host = [theURL host];
- NSString *path = [theURL path];
- NSString *scheme = [theURL scheme];
-
- NSString *domain = nil;
-
- if (host) {
- domain = [[@"." stringByAppendingString:host] lowercaseString];
- }
-
- NSUInteger numberOfCookies = [cookieStorageArray count];
- for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
-
- NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx];
-
- NSString *cookieDomain = [[storedCookie domain] lowercaseString];
- NSString *cookiePath = [storedCookie path];
- BOOL cookieIsSecure = [storedCookie isSecure];
-
- BOOL domainIsOK = [domain hasSuffix:cookieDomain];
- if (!domainIsOK && [domain hasSuffix:@"localhost"]) {
- // On Leopard and below, localhost Cookies always come back
- // with a domain of localhost.local. On SnowLeopard they come
- // back as just localhost.
- domainIsOK = [@".localhost.local" hasSuffix:cookieDomain];
- }
- BOOL pathIsOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
- BOOL secureIsOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
-
- if (domainIsOK && pathIsOK && secureIsOK) {
- [foundCookies addObject:storedCookie];
- }
- }
- return foundCookies;
-}
-
-// return cookies for the given URL using the current cookie storage method
-- (NSArray *)cookiesForURL:(NSURL *)theURL {
-
- NSArray *cookies = nil;
- NSMutableArray *cookieStorageArray = nil;
-
- if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodStatic) {
- cookieStorageArray = gGTMFetcherStaticCookies;
- } else if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory) {
- cookieStorageArray
- = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryCookiesKey];
- } else {
- // kGTMHTTPFetcherCookieStorageMethodSystemDefault
- NSHTTPCookieStorage *storage
- = [NSHTTPCookieStorage sharedHTTPCookieStorage];
- cookies = [storage cookiesForURL:theURL];
- }
-
- if (cookieStorageArray) {
-
- @synchronized(cookieStorageArray) {
-
- // cookiesForURL returns a new array of immutable NSCookie objects
- // from cookieStorageArray
- cookies = [self cookiesForURL:theURL
- inArray:cookieStorageArray];
- }
- }
- return cookies;
-}
-
-
-// add all cookies in the array |newCookies| to the storage array,
-// replacing cookies in the storage array as appropriate
-// Side effect: removes expired cookies from the storage array
-- (void)setCookies:(NSArray *)newCookies
- inArray:(NSMutableArray *)cookieStorageArray {
-
- [self removeExpiredCookiesInArray:cookieStorageArray];
-
- NSHTTPCookie *newCookie;
-
- GTM_FOREACH_OBJECT(newCookie, newCookies) {
-
- if ([[newCookie name] length] > 0
- && [[newCookie domain] length] > 0
- && [[newCookie path] length] > 0) {
-
- // remove the cookie if it's currently in the array
- NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie
- inArray:cookieStorageArray];
- if (oldCookie) {
- [cookieStorageArray removeObject:oldCookie];
- }
-
- // make sure the cookie hasn't already expired
- NSDate *expiresDate = [newCookie expiresDate];
- if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
- [cookieStorageArray addObject:newCookie];
- }
-
- } else {
- _GTMDevAssert(NO, @"Cookie incomplete: %@", newCookie); // COV_NF_LINE
- }
- }
-}
-@end
-
-#pragma mark Logging
-
-// NOTE: Threads and Logging
-//
-// All the NSURLConnection callbacks happen on one thread, so we don't have
-// to put any synchronization into the logging code. Yes, the state around
-// logging (it's directory, etc.) could use it, but for now, that's punted.
-
-
-// We don't invoke Leopard methods on 10.4, because we check if the methods are
-// implemented before invoking it, but we need to be able to compile without
-// warnings.
-// This declaration means if you target <=10.4, this method will compile
-// without complaint in this source, so you must test with
-// -respondsToSelector:, too.
-#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
-@interface NSFileManager (LeopardMethodsOnTigerBuilds)
-- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error;
-@end
-#endif
-// The iPhone Foundation removes the deprecated removeFileAtPath:handler:
-#if GTM_IPHONE_SDK
-@interface NSFileManager (TigerMethodsOniPhoneBuilds)
-- (BOOL)removeFileAtPath:(NSString *)path handler:(id)handler;
-@end
-#endif
-
-@implementation GTMHTTPFetcher (GTMHTTPFetcherLogging)
-
-// if GTM_HTTPFETCHER_ENABLE_LOGGING is defined by the user's project then
-// logging code will be compiled into the framework
-
-#if !GTM_HTTPFETCHER_ENABLE_LOGGING
-- (void)logFetchWithError:(NSError *)error {}
-
-+ (void)setLoggingDirectory:(NSString *)path {}
-+ (NSString *)loggingDirectory {return nil;}
-
-+ (void)setIsLoggingEnabled:(BOOL)flag {}
-+ (BOOL)isLoggingEnabled {return NO;}
-
-+ (void)setLoggingProcessName:(NSString *)str {}
-+ (NSString *)loggingProcessName {return nil;}
-
-+ (void)setLoggingDateStamp:(NSString *)str {}
-+ (NSString *)loggingDateStamp {return nil;}
-
-- (void)appendLoggedStreamData:(NSData *)newData {}
-- (void)logCapturePostStream {}
-#else // GTM_HTTPFETCHER_ENABLE_LOGGING
-
-// fetchers come and fetchers go, but statics are forever
-static BOOL gIsLoggingEnabled = NO;
-static NSString *gLoggingDirectoryPath = nil;
-static NSString *gLoggingDateStamp = nil;
-static NSString* gLoggingProcessName = nil;
-
-+ (void)setLoggingDirectory:(NSString *)path {
- [gLoggingDirectoryPath autorelease];
- gLoggingDirectoryPath = [path copy];
-}
-
-+ (NSString *)loggingDirectory {
-
- if (!gLoggingDirectoryPath) {
-
-#if GTM_IPHONE_SDK
- // default to a directory called GTMHTTPDebugLogs into a sandbox-safe
- // directory that a devloper can find easily, the application home
- NSArray *arr = [NSArray arrayWithObject:NSHomeDirectory()];
-#else
- // default to a directory called GTMHTTPDebugLogs in the desktop folder
- NSArray *arr = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory,
- NSUserDomainMask, YES);
-#endif
-
- if ([arr count] > 0) {
- NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs";
-
- NSString *desktopPath = [arr objectAtIndex:0];
- NSString *logsFolderPath
- = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName];
-
- BOOL doesFolderExist;
- BOOL isDir = NO;
- NSFileManager *fileManager = [NSFileManager defaultManager];
- doesFolderExist = [fileManager fileExistsAtPath:logsFolderPath
- isDirectory:&isDir];
-
- if (!doesFolderExist) {
- // make the directory
- doesFolderExist = [fileManager createDirectoryAtPath:logsFolderPath
- attributes:nil];
- }
-
- if (doesFolderExist) {
- // it's there; store it in the global
- gLoggingDirectoryPath = [logsFolderPath copy];
- }
- }
- }
- return gLoggingDirectoryPath;
-}
-
-+ (void)setIsLoggingEnabled:(BOOL)flag {
- gIsLoggingEnabled = flag;
-}
-
-+ (BOOL)isLoggingEnabled {
- return gIsLoggingEnabled;
-}
-
-+ (void)setLoggingProcessName:(NSString *)str {
- [gLoggingProcessName release];
- gLoggingProcessName = [str copy];
-}
-
-+ (NSString *)loggingProcessName {
-
- // get the process name (once per run) replacing spaces with underscores
- if (!gLoggingProcessName) {
-
- NSString *procName = [[NSProcessInfo processInfo] processName];
- NSMutableString *loggingProcessName;
- loggingProcessName = [[NSMutableString alloc] initWithString:procName];
- NSRange nameRange = NSMakeRange(0, [gLoggingProcessName length]);
- [loggingProcessName replaceOccurrencesOfString:@" "
- withString:@"_"
- options:0
- range:nameRange];
- gLoggingProcessName = loggingProcessName;
- }
- return gLoggingProcessName;
-}
-
-+ (void)setLoggingDateStamp:(NSString *)str {
- [gLoggingDateStamp release];
- gLoggingDateStamp = [str copy];
-}
-
-+ (NSString *)loggingDateStamp {
- // we'll pick one date stamp per run, so a run that starts at a later second
- // will get a unique results html file
- if (!gLoggingDateStamp) {
- // produce a string like 08-21_01-41-23PM
-
- NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease];
- [formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
- [formatter setDateFormat:@"M-dd_hh-mm-ssa"];
-
- gLoggingDateStamp = [[formatter stringFromDate:[NSDate date]] retain] ;
- }
- return gLoggingDateStamp;
-}
-
-- (NSString *)cleanParameterFollowing:(NSString *)paramName
- fromString:(NSString *)originalStr {
- // We don't want the password written to disk
- //
- // find "&Passwd=" in the string, and replace it and the stuff that
- // follows it with "Passwd=_snip_"
-
- NSRange passwdRange = [originalStr rangeOfString:@"&Passwd="];
- if (passwdRange.location != NSNotFound) {
-
- // we found Passwd=; find the & that follows the parameter
- NSUInteger origLength = [originalStr length];
- NSRange restOfString = NSMakeRange(passwdRange.location+1,
- origLength - passwdRange.location - 1);
- NSRange rangeOfFollowingAmp = [originalStr rangeOfString:@"&"
- options:0
- range:restOfString];
- NSRange replaceRange;
- if (rangeOfFollowingAmp.location == NSNotFound) {
- // found no other & so replace to end of string
- replaceRange = NSMakeRange(passwdRange.location,
- rangeOfFollowingAmp.location - passwdRange.location);
- } else {
- // another parameter after &Passwd=foo
- replaceRange = NSMakeRange(passwdRange.location,
- rangeOfFollowingAmp.location - passwdRange.location);
- }
-
- NSMutableString *result = [NSMutableString stringWithString:originalStr];
- NSString *replacement = [NSString stringWithFormat:@"%@_snip_", paramName];
-
- [result replaceCharactersInRange:replaceRange withString:replacement];
- return result;
- }
- return originalStr;
-}
-
-// stringFromStreamData creates a string given the supplied data
-//
-// If NSString can create a UTF-8 string from the data, then that is returned.
-//
-// Otherwise, this routine tries to find a MIME boundary at the beginning of
-// the data block, and uses that to break up the data into parts. Each part
-// will be used to try to make a UTF-8 string. For parts that fail, a
-// replacement string showing the part header and <<n bytes>> is supplied
-// in place of the binary data.
-
-- (NSString *)stringFromStreamData:(NSData *)data {
-
- if (data == nil) return nil;
-
- // optimistically, see if the whole data block is UTF-8
- NSString *streamDataStr
- = [[[NSString alloc] initWithData:data
- encoding:NSUTF8StringEncoding] autorelease];
- if (streamDataStr) return streamDataStr;
-
- // Munge a buffer by replacing non-ASCII bytes with underscores,
- // and turn that munged buffer an NSString. That gives us a string
- // we can use with NSScanner.
- NSMutableData *mutableData = [NSMutableData dataWithData:data];
- unsigned char *bytes = [mutableData mutableBytes];
-
- for (NSUInteger idx = 0; idx < [mutableData length]; idx++) {
- if (bytes[idx] > 0x7F || bytes[idx] == 0) {
- bytes[idx] = '_';
- }
- }
-
- NSString *mungedStr
- = [[[NSString alloc] initWithData:mutableData
- encoding:NSUTF8StringEncoding] autorelease];
- if (mungedStr != nil) {
-
- // scan for the boundary string
- NSString *boundary = nil;
- NSScanner *scanner = [NSScanner scannerWithString:mungedStr];
-
- if ([scanner scanUpToString:@"\r\n" intoString:&boundary]
- && [boundary hasPrefix:@"--"]) {
-
- // we found a boundary string; use it to divide the string into parts
- NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary];
-
- // look at each of the munged parts in the original string, and try to
- // convert those into UTF-8
- NSMutableArray *origParts = [NSMutableArray array];
- NSUInteger offset = 0;
- for (NSUInteger partIdx = 0; partIdx < [mungedParts count]; partIdx++) {
-
- NSString *mungedPart = [mungedParts objectAtIndex:partIdx];
- NSUInteger partSize = [mungedPart length];
-
- NSRange range = NSMakeRange(offset, partSize);
- NSData *origPartData = [data subdataWithRange:range];
-
- NSString *origPartStr
- = [[[NSString alloc] initWithData:origPartData
- encoding:NSUTF8StringEncoding] autorelease];
- if (origPartStr) {
- // we could make this original part into UTF-8; use the string
- [origParts addObject:origPartStr];
- } else {
- // this part can't be made into UTF-8; scan the header, if we can
- NSString *header = nil;
- NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart];
- if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) {
- // we couldn't find a header
- header = @"";
- }
-
- // make a part string with the header and <<n bytes>>
- NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%u bytes>>\r",
- header, partSize - [header length]];
- [origParts addObject:binStr];
- }
- offset += partSize + [boundary length];
- }
-
- // rejoin the original parts
- streamDataStr = [origParts componentsJoinedByString:boundary];
- }
- }
-
- if (!streamDataStr) {
- // give up; just make a string showing the uploaded bytes
- streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", [data length]];
- }
- return streamDataStr;
-}
-
-// logFetchWithError is called following a successful or failed fetch attempt
-//
-// This method does all the work for appending to and creating log files
-
-- (void)logFetchWithError:(NSError *)error {
-
- if (![[self class] isLoggingEnabled]) return;
-
- NSFileManager *fileManager = [NSFileManager defaultManager];
-
- // TODO: add Javascript to display response data formatted in hex
-
- NSString *logDirectory = [[self class] loggingDirectory];
- NSString *processName = [[self class] loggingProcessName];
- NSString *dateStamp = [[self class] loggingDateStamp];
-
- // each response's NSData goes into its own xml or txt file, though all
- // responses for this run of the app share a main html file. This
- // counter tracks all fetch responses for this run of the app.
- static int zResponseCounter = 0;
- zResponseCounter++;
-
- // file name for the html file containing plain text in a <textarea>
- NSString *responseDataUnformattedFileName = nil;
-
- // file name for the "formatted" (raw) data file
- NSString *responseDataFormattedFileName = nil;
- NSUInteger responseDataLength = [downloadedData_ length];
-
- NSURLResponse *response = [self response];
- NSString *responseBaseName = nil;
-
- // if there's response data, decide what kind of file to put it in based
- // on the first bytes of the file or on the mime type supplied by the server
- if (responseDataLength) {
- NSString *responseDataExtn = nil;
-
- // generate a response file base name like
- // SyncProto_http_response_10-16_01-56-58PM_3
- responseBaseName = [NSString stringWithFormat:@"%@_http_response_%@_%d",
- processName, dateStamp, zResponseCounter];
-
- NSString *dataStr
- = [[[NSString alloc] initWithData:downloadedData_
- encoding:NSUTF8StringEncoding] autorelease];
- if (dataStr) {
- // we were able to make a UTF-8 string from the response data
-
- NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet];
- dataStr = [dataStr stringByTrimmingCharactersInSet:whitespaceSet];
-
- // save a plain-text version of the response data in an html cile
- // containing a wrapped, scrollable <textarea>
- //
- // we'll use <textarea rows="33" cols="108" readonly=true wrap=soft>
- // </textarea> to fit inside our iframe
- responseDataUnformattedFileName
- = [responseBaseName stringByAppendingPathExtension:@"html"];
- NSString *textFilePath
- = [logDirectory stringByAppendingPathComponent:responseDataUnformattedFileName];
-
- NSString* wrapFmt = @"<textarea rows=\"33\" cols=\"108\" readonly=true"
- " wrap=soft>\n%@\n</textarea>";
- NSString* wrappedStr = [NSString stringWithFormat:wrapFmt, dataStr];
- [wrappedStr writeToFile:textFilePath
- atomically:NO
- encoding:NSUTF8StringEncoding
- error:nil];
-
- // now determine the extension for the "formatted" file, which is really
- // the raw data written with an appropriate extension
-
- // for known file types, we'll write the data to a file with the
- // appropriate extension
- if ([dataStr hasPrefix:@"<?xml"]) {
- responseDataExtn = @"xml";
- } else if ([dataStr hasPrefix:@"<html"]) {
- responseDataExtn = @"html";
- } else {
- // add more types of identifiable text here
- }
-
- } else if ([[response MIMEType] isEqual:@"image/jpeg"]) {
- responseDataExtn = @"jpg";
- } else if ([[response MIMEType] isEqual:@"image/gif"]) {
- responseDataExtn = @"gif";
- } else if ([[response MIMEType] isEqual:@"image/png"]) {
- responseDataExtn = @"png";
- } else {
- // add more non-text types here
- }
-
- // if we have an extension, save the raw data in a file with that
- // extension to be our "formatted" display file
- if (responseDataExtn) {
- responseDataFormattedFileName
- = [responseBaseName stringByAppendingPathExtension:responseDataExtn];
- NSString *formattedFilePath
- = [logDirectory stringByAppendingPathComponent:responseDataFormattedFileName];
-
- [downloadedData_ writeToFile:formattedFilePath atomically:NO];
- }
- }
-
- // we'll have one main html file per run of the app
- NSString *htmlName = [NSString stringWithFormat:@"%@_http_log_%@.html",
- processName, dateStamp];
- NSString *htmlPath =[logDirectory stringByAppendingPathComponent:htmlName];
-
- // if the html file exists (from logging previous fetches) we don't need
- // to re-write the header or the scripts
- BOOL didFileExist = [fileManager fileExistsAtPath:htmlPath];
-
- NSMutableString* outputHTML = [NSMutableString string];
- NSURLRequest *request = [self request];
-
- // we need file names for the various div's that we're going to show and hide,
- // names unique to this response's bundle of data, so we format our div
- // names with the counter that we incremented earlier
- NSString *requestHeadersName
- = [NSString stringWithFormat:@"RequestHeaders%d", zResponseCounter];
- NSString *postDataName
- = [NSString stringWithFormat:@"PostData%d", zResponseCounter];
-
- NSString *responseHeadersName
- = [NSString stringWithFormat:@"ResponseHeaders%d", zResponseCounter];
- NSString *responseDataDivName
- = [NSString stringWithFormat:@"ResponseData%d", zResponseCounter];
- NSString *dataIFrameID
- = [NSString stringWithFormat:@"DataIFrame%d", zResponseCounter];
-
- // we need a header to say we'll have UTF-8 text
- if (!didFileExist) {
- [outputHTML appendFormat:@"<html><head><meta http-equiv=\"content-type\" "
- @"content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>",
- processName, dateStamp];
- }
-
- // write style sheets for each hideable element; each style sheet is
- // customized with our current response number, since they'll share
- // the html page with other responses
- NSString *styleFormat = @"<style type=\"text/css\">div#%@ "
- @"{ margin: 0px 20px 0px 20px; display: none; }</style>\n";
-
- [outputHTML appendFormat:styleFormat, requestHeadersName];
- [outputHTML appendFormat:styleFormat, postDataName];
- [outputHTML appendFormat:styleFormat, responseHeadersName];
- [outputHTML appendFormat:styleFormat, responseDataDivName];
-
- if (!didFileExist) {
- // write javascript functions. The first one shows/hides the layer
- // containing the iframe.
- NSString *scriptFormat = @"<script type=\"text/javascript\"> "
- @"function toggleLayer(whichLayer){ "
- @"var style2 = document.getElementById(whichLayer).style; "
- @"style2.display = style2.display ? \"\":\"block\";}</script>\n";
- [outputHTML appendFormat:scriptFormat];
-
- // the second function is passed the src file; if it's what's shown, it
- // toggles the iframe's visibility. If some other src is shown, it shows
- // the iframe and loads the new source. Note we want to load the source
- // whenever we show the iframe too since Firefox seems to format it wrong
- // when showing it if we don't reload it.
- NSString *toggleIFScriptFormat = @"<script type=\"text/javascript\"> "
- @"function toggleIFrame(whichLayer,iFrameID,newsrc)"
- @"{ \n var iFrameElem=document.getElementById(iFrameID); "
- @"if (iFrameElem.src.indexOf(newsrc) != -1) { toggleLayer(whichLayer); } "
- @"else { document.getElementById(whichLayer).style.display=\"block\"; } "
- @"iFrameElem.src=newsrc; }</script>\n</head>\n<body>\n";
- [outputHTML appendFormat:toggleIFScriptFormat];
- }
-
- // now write the visible html elements
-
- // write the date & time
- [outputHTML appendFormat:@"<b>%@</b><br>", [[NSDate date] description]];
-
- // write the request URL
- [outputHTML appendFormat:@"<b>request:</b> %@ <i>URL:</i> <code>%@</code><br>\n",
- [request HTTPMethod], [request URL]];
-
- // write the request headers, toggleable
- NSDictionary *requestHeaders = [request allHTTPHeaderFields];
- if ([requestHeaders count]) {
- NSString *requestHeadersFormat = @"<a href=\"javascript:toggleLayer('%@');\">"
- @"request headers (%d)</a><div id=\"%@\"><pre>%@</pre></div><br>\n";
- [outputHTML appendFormat:requestHeadersFormat,
- requestHeadersName, // layer name
- [requestHeaders count],
- requestHeadersName,
- [requestHeaders description]]; // description gives a human-readable dump
- } else {
- [outputHTML appendString:@"<i>Request headers: none</i><br>"];
- }
-
- // write the request post data, toggleable
- NSData *postData = postData_;
- if (loggedStreamData_) {
- postData = loggedStreamData_;
- }
-
- if ([postData length]) {
- NSString *postDataFormat = @"<a href=\"javascript:toggleLayer('%@');\">"
- "posted data (%d bytes)</a><div id=\"%@\">%@</div><br>\n";
- NSString *postDataStr = [self stringFromStreamData:postData];
- if (postDataStr) {
- NSString *postDataTextAreaFmt = @"<pre>%@</pre>";
- if ([postDataStr rangeOfString:@"<"].location != NSNotFound) {
- postDataTextAreaFmt = @"<textarea rows=\"15\" cols=\"100\""
- " readonly=true wrap=soft>\n%@\n</textarea>";
- }
- NSString *cleanedPostData = [self cleanParameterFollowing:@"&Passwd="
- fromString:postDataStr];
- NSString *postDataTextArea = [NSString stringWithFormat:
- postDataTextAreaFmt, cleanedPostData];
-
- [outputHTML appendFormat:postDataFormat,
- postDataName, // layer name
- [postData length],
- postDataName,
- postDataTextArea];
- }
- } else {
- // no post data
- }
-
- // write the response status, MIME type, URL
- if (response) {
- NSString *statusString = @"";
- if ([response respondsToSelector:@selector(statusCode)]) {
- NSInteger status = [(NSHTTPURLResponse *)response statusCode];
- statusString = @"200";
- if (status != 200) {
- // purple for errors
- statusString
- = [NSString stringWithFormat:@"<FONT COLOR=\"#FF00FF\">%d</FONT>",
- status];
- }
- }
-
- // show the response URL only if it's different from the request URL
- NSString *responseURLStr = @"";
- NSURL *responseURL = [response URL];
-
- if (responseURL && ![responseURL isEqual:[request URL]]) {
- NSString *responseURLFormat = @"<br><FONT COLOR=\"#FF00FF\">response URL:"
- @"</FONT> <code>%@</code>";
- responseURLStr = [NSString stringWithFormat:responseURLFormat,
- [responseURL absoluteString]];
- }
-
- NSDictionary *responseHeaders = nil;
- if ([response respondsToSelector:@selector(allHeaderFields)]) {
- responseHeaders = [(NSHTTPURLResponse *)response allHeaderFields];
- }
- [outputHTML appendFormat:@"<b>response:</b> <i>status:</i> %@ <i> "
- @"&nbsp;&nbsp;&nbsp;MIMEType:</i><code> %@</code>%@<br>\n",
- statusString,
- [response MIMEType],
- responseURLStr,
- responseHeaders ? [responseHeaders description] : @""];
-
- // write the response headers, toggleable
- if ([responseHeaders count]) {
-
- NSString *cookiesSet = [responseHeaders objectForKey:@"Set-Cookie"];
-
- NSString *responseHeadersFormat = @"<a href=\"javascript:toggleLayer("
- @"'%@');\">response headers (%d) %@</a><div id=\"%@\"><pre>%@</pre>"
- @"</div><br>\n";
- [outputHTML appendFormat:responseHeadersFormat,
- responseHeadersName,
- [responseHeaders count],
- (cookiesSet ? @"<i>sets cookies</i>" : @""),
- responseHeadersName,
- [responseHeaders description]];
-
- } else {
- [outputHTML appendString:@"<i>Response headers: none</i><br>\n"];
- }
- }
-
- // error
- if (error) {
- [outputHTML appendFormat:@"<b>error:</b> %@ <br>\n", [error description]];
- }
-
- // write the response data. We have links to show formatted and text
- // versions, but they both show it in the same iframe, and both
- // links also toggle visible/hidden
- if (responseDataFormattedFileName || responseDataUnformattedFileName) {
-
- // response data, toggleable links -- formatted and text versions
- if (responseDataFormattedFileName) {
- [outputHTML appendFormat:@"response data (%d bytes) formatted <b>%@</b> ",
- responseDataLength,
- [responseDataFormattedFileName pathExtension]];
-
- // inline (iframe) link
- NSString *responseInlineFormattedDataNameFormat = @"&nbsp;&nbsp;<a "
- @"href=\"javascript:toggleIFrame('%@','%@','%@');\">inline</a>\n";
- [outputHTML appendFormat:responseInlineFormattedDataNameFormat,
- responseDataDivName, // div ID
- dataIFrameID, // iframe ID (for reloading)
- responseDataFormattedFileName]; // src to reload
-
- // plain link (so the user can command-click it into another tab)
- [outputHTML appendFormat:@"&nbsp;&nbsp;<a href=\"%@\">stand-alone</a><br>\n",
- [responseDataFormattedFileName
- stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
- }
- if (responseDataUnformattedFileName) {
- [outputHTML appendFormat:@"response data (%d bytes) plain text ",
- responseDataLength];
-
- // inline (iframe) link
- NSString *responseInlineDataNameFormat = @"&nbsp;&nbsp;<a href=\""
- @"javascript:toggleIFrame('%@','%@','%@');\">inline</a> \n";
- [outputHTML appendFormat:responseInlineDataNameFormat,
- responseDataDivName, // div ID
- dataIFrameID, // iframe ID (for reloading)
- responseDataUnformattedFileName]; // src to reload
-
- // plain link (so the user can command-click it into another tab)
- [outputHTML appendFormat:@"&nbsp;&nbsp;<a href=\"%@\">stand-alone</a><br>\n",
- [responseDataUnformattedFileName
- stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
- }
-
- // make the iframe
- NSString *divHTMLFormat = @"<div id=\"%@\">%@</div><br>\n";
- NSString *src = responseDataFormattedFileName ?
- responseDataFormattedFileName : responseDataUnformattedFileName;
- NSString *escapedSrc
- = [src stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
- NSString *iframeFmt = @" <iframe src=\"%@\" id=\"%@\" width=800 height=400>"
- @"\n<a href=\"%@\">%@</a>\n </iframe>\n";
- NSString *dataIFrameHTML = [NSString stringWithFormat:iframeFmt,
- escapedSrc, dataIFrameID, escapedSrc, src];
- [outputHTML appendFormat:divHTMLFormat,
- responseDataDivName, dataIFrameHTML];
- } else {
- // could not parse response data; just show the length of it
- [outputHTML appendFormat:@"<i>Response data: %d bytes </i>\n",
- responseDataLength];
- }
-
- [outputHTML appendString:@"<br><hr><p>"];
-
- // append the HTML to the main output file
- const char* htmlBytes = [outputHTML UTF8String];
- NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath
- append:YES];
- [stream open];
- [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)];
- [stream close];
-
- // make a symlink to the latest html
- NSString *symlinkName = [NSString stringWithFormat:@"%@_http_log_newest.html",
- processName];
- NSString *symlinkPath
- = [logDirectory stringByAppendingPathComponent:symlinkName];
-
- // removeFileAtPath might be going away, but removeItemAtPath does not exist
- // in 10.4
- if ([fileManager respondsToSelector:@selector(removeFileAtPath:handler:)]) {
- [fileManager removeFileAtPath:symlinkPath handler:nil];
- } else if ([fileManager respondsToSelector:@selector(removeItemAtPath:error:)]) {
- // To make the next line compile when targeting 10.4, we declare
- // removeItemAtPath:error: in an @interface above
- [fileManager removeItemAtPath:symlinkPath error:NULL];
- }
-
- [fileManager createSymbolicLinkAtPath:symlinkPath pathContent:htmlPath];
-}
-
-- (void)logCapturePostStream {
-
-#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
- // This is called when beginning a fetch. The caller should have already
- // verified that logging is enabled, and should have allocated
- // loggedStreamData_ as a mutable object.
-
- // If we're logging, we need to wrap the upload stream with our monitor
- // stream subclass that will call us back with the bytes being read from the
- // stream
-
- // our wrapper will retain the old post stream
- [postStream_ autorelease];
-
- // length can be
- postStream_ = [GTMInputStreamLogger inputStreamWithStream:postStream_
- length:0];
- [postStream_ retain];
-
- // we don't really want monitoring callbacks; our subclass will be
- // calling our appendLoggedStreamData: method at every read instead
- [(GTMInputStreamLogger *)postStream_ setMonitorDelegate:self
- selector:nil];
-#endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
-}
-
-- (void)appendLoggedStreamData:(NSData *)newData {
- [loggedStreamData_ appendData:newData];
-}
-
-#endif // GTM_HTTPFETCHER_ENABLE_LOGGING
-@end
-
-#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
-@implementation GTMInputStreamLogger
-- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
-
- // capture the read stream data, and pass it to the delegate to append to
- NSInteger result = [super read:buffer maxLength:len];
- if (result >= 0) {
- NSData *data = [NSData dataWithBytes:buffer length:result];
- [monitorDelegate_ appendLoggedStreamData:data];
- }
- return result;
-}
-@end
-#endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING