PageRenderTime 242ms CodeModel.GetById 21ms app.highlight 208ms RepoModel.GetById 1ms app.codeStats 0ms

/core/externals/update-engine/externals/gdata-objectivec-client/Source/HTTPFetcher/GTMHTTPFetcher.m

http://macfuse.googlecode.com/
Objective C | 2007 lines | 1387 code | 322 blank | 298 comment | 288 complexity | 88f4f5798e8f1a7d6dabe2653776642e MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/* Copyright (c) 2011 Google Inc.
   2 *
   3 * Licensed under the Apache License, Version 2.0 (the "License");
   4 * you may not use this file except in compliance with the License.
   5 * You may obtain a copy of the License at
   6 *
   7 *     http://www.apache.org/licenses/LICENSE-2.0
   8 *
   9 * Unless required by applicable law or agreed to in writing, software
  10 * distributed under the License is distributed on an "AS IS" BASIS,
  11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 * See the License for the specific language governing permissions and
  13 * limitations under the License.
  14 */
  15
  16//
  17//  GTMHTTPFetcher.m
  18//
  19
  20#import "GTMHTTPFetcher.h"
  21
  22#if GTM_BACKGROUND_FETCHING
  23#import <UIKit/UIKit.h>
  24#endif
  25
  26static id <GTMCookieStorageProtocol> gGTMFetcherStaticCookieStorage = nil;
  27static Class gGTMFetcherConnectionClass = nil;
  28
  29
  30NSString *const kGTMHTTPFetcherStartedNotification           = @"kGTMHTTPFetcherStartedNotification";
  31NSString *const kGTMHTTPFetcherStoppedNotification           = @"kGTMHTTPFetcherStoppedNotification";
  32NSString *const kGTMHTTPFetcherRetryDelayStartedNotification = @"kGTMHTTPFetcherRetryDelayStartedNotification";
  33NSString *const kGTMHTTPFetcherRetryDelayStoppedNotification = @"kGTMHTTPFetcherRetryDelayStoppedNotification";
  34
  35NSString *const kGTMHTTPFetcherErrorDomain       = @"com.google.GTMHTTPFetcher";
  36NSString *const kGTMHTTPFetcherStatusDomain      = @"com.google.HTTPStatus";
  37NSString *const kGTMHTTPFetcherErrorChallengeKey = @"challenge";
  38NSString *const kGTMHTTPFetcherStatusDataKey     = @"data";  // data returned with a kGTMHTTPFetcherStatusDomain error
  39
  40// The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH),
  41// 1 minute for downloads.
  42static const NSTimeInterval kUnsetMaxRetryInterval = -1;
  43static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0;
  44static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
  45
  46// delegateQueue callback parameters
  47static NSString *const kCallbackData = @"data";
  48static NSString *const kCallbackError = @"error";
  49
  50//
  51// GTMHTTPFetcher
  52//
  53
  54@interface GTMHTTPFetcher ()
  55
  56@property (copy) NSString *temporaryDownloadPath;
  57@property (retain) id <GTMCookieStorageProtocol> cookieStorage;
  58@property (readwrite, retain) NSData *downloadedData;
  59#if NS_BLOCKS_AVAILABLE
  60@property (copy) void (^completionBlock)(NSData *, NSError *);
  61#endif
  62
  63- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
  64              mayAuthorize:(BOOL)mayAuthorize;
  65- (void)failToBeginFetchWithError:(NSError *)error;
  66- (void)failToBeginFetchDeferWithError:(NSError *)error;
  67
  68#if GTM_BACKGROUND_FETCHING
  69- (void)endBackgroundTask;
  70- (void)backgroundFetchExpired;
  71#endif
  72
  73- (BOOL)authorizeRequest;
  74- (void)authorizer:(id <GTMFetcherAuthorizationProtocol>)auth
  75           request:(NSMutableURLRequest *)request
  76 finishedWithError:(NSError *)error;
  77
  78- (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath;
  79- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks;
  80- (BOOL)shouldReleaseCallbacksUponCompletion;
  81
  82- (void)addCookiesToRequest:(NSMutableURLRequest *)request;
  83- (void)handleCookiesForResponse:(NSURLResponse *)response;
  84
  85- (void)invokeFetchCallbacksWithData:(NSData *)data
  86                               error:(NSError *)error;
  87- (void)invokeFetchCallback:(SEL)sel
  88                     target:(id)target
  89                       data:(NSData *)data
  90                      error:(NSError *)error;
  91- (void)invokeFetchCallbacksOnDelegateQueueWithData:(NSData *)data
  92                                              error:(NSError *)error;
  93- (void)invokeOnQueueWithDictionary:(NSDictionary *)dict;
  94- (void)releaseCallbacks;
  95
  96- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
  97
  98- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error;
  99- (void)destroyRetryTimer;
 100- (void)beginRetryTimer;
 101- (void)primeRetryTimerWithNewTimeInterval:(NSTimeInterval)secs;
 102- (void)sendStopNotificationIfNeeded;
 103- (void)retryFetch;
 104- (void)retryTimerFired:(NSTimer *)timer;
 105@end
 106
 107@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal)
 108- (void)setupStreamLogging;
 109- (void)logFetchWithError:(NSError *)error;
 110- (void)logNowWithError:(NSError *)error;
 111@end
 112
 113@implementation GTMHTTPFetcher
 114
 115+ (GTMHTTPFetcher *)fetcherWithRequest:(NSURLRequest *)request {
 116  return [[[[self class] alloc] initWithRequest:request] autorelease];
 117}
 118
 119+ (GTMHTTPFetcher *)fetcherWithURL:(NSURL *)requestURL {
 120  return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
 121}
 122
 123+ (GTMHTTPFetcher *)fetcherWithURLString:(NSString *)requestURLString {
 124  return [self fetcherWithURL:[NSURL URLWithString:requestURLString]];
 125}
 126
 127+ (void)initialize {
 128  // initialize is guaranteed by the runtime to be called in a
 129  // thread-safe manner
 130  if (!gGTMFetcherStaticCookieStorage) {
 131    Class cookieStorageClass = NSClassFromString(@"GTMCookieStorage");
 132    if (cookieStorageClass) {
 133      gGTMFetcherStaticCookieStorage = [[cookieStorageClass alloc] init];
 134    }
 135  }
 136}
 137
 138- (id)init {
 139  return [self initWithRequest:nil];
 140}
 141
 142- (id)initWithRequest:(NSURLRequest *)request {
 143  self = [super init];
 144  if (self) {
 145    request_ = [request mutableCopy];
 146
 147    if (gGTMFetcherStaticCookieStorage != nil) {
 148      // The user has compiled with the cookie storage class available;
 149      // default to static cookie storage, so our cookies are independent
 150      // of the cookies of other apps.
 151      [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic];
 152    } else {
 153      // Default to system default cookie storage
 154      [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodSystemDefault];
 155    }
 156#if !STRIP_GTM_FETCH_LOGGING
 157    // Encourage developers to set the comment property or use
 158    // setCommentWithFormat: by providing a default string.
 159    comment_ = @"(No fetcher comment set)";
 160#endif
 161  }
 162  return self;
 163}
 164
 165- (id)copyWithZone:(NSZone *)zone {
 166  // disallow use of fetchers in a copy property
 167  [self doesNotRecognizeSelector:_cmd];
 168  return nil;
 169}
 170
 171- (NSString *)description {
 172  return [NSString stringWithFormat:@"%@ %p (%@)",
 173          [self class], self, [self.mutableRequest URL]];
 174}
 175
 176#if !GTM_IPHONE
 177- (void)finalize {
 178  [self stopFetchReleasingCallbacks:YES]; // releases connection_, destroys timers
 179  [super finalize];
 180}
 181#endif
 182
 183- (void)dealloc {
 184#if DEBUG
 185  NSAssert(!isStopNotificationNeeded_,
 186           @"unbalanced fetcher notification for %@", [request_ URL]);
 187#endif
 188
 189  // Note: if a connection or a retry timer was pending, then this instance
 190  // would be retained by those so it wouldn't be getting dealloc'd,
 191  // hence we don't need to stopFetch here
 192  [request_ release];
 193  [connection_ release];
 194  [downloadedData_ release];
 195  [downloadPath_ release];
 196  [temporaryDownloadPath_ release];
 197  [downloadFileHandle_ release];
 198  [credential_ release];
 199  [proxyCredential_ release];
 200  [postData_ release];
 201  [postStream_ release];
 202  [loggedStreamData_ release];
 203  [response_ release];
 204#if NS_BLOCKS_AVAILABLE
 205  [completionBlock_ release];
 206  [receivedDataBlock_ release];
 207  [sentDataBlock_ release];
 208  [retryBlock_ release];
 209#endif
 210  [userData_ release];
 211  [properties_ release];
 212  [delegateQueue_ release];
 213  [runLoopModes_ release];
 214  [fetchHistory_ release];
 215  [cookieStorage_ release];
 216  [authorizer_ release];
 217  [service_ release];
 218  [serviceHost_ release];
 219  [thread_ release];
 220  [retryTimer_ release];
 221  [initialRequestDate_ release];
 222  [comment_ release];
 223  [log_ release];
 224#if !STRIP_GTM_FETCH_LOGGING
 225  [redirectedFromURL_ release];
 226  [logRequestBody_ release];
 227  [logResponseBody_ release];
 228#endif
 229
 230  [super dealloc];
 231}
 232
 233#pragma mark -
 234
 235// Begin fetching the URL (or begin a retry fetch).  The delegate is retained
 236// for the duration of the fetch connection.
 237
 238- (BOOL)beginFetchWithDelegate:(id)delegate
 239             didFinishSelector:(SEL)finishedSelector {
 240  GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, @encode(GTMHTTPFetcher *), @encode(NSData *), @encode(NSError *), 0);
 241  GTMAssertSelectorNilOrImplementedWithArgs(delegate, receivedDataSel_, @encode(GTMHTTPFetcher *), @encode(NSData *), 0);
 242  GTMAssertSelectorNilOrImplementedWithArgs(delegate, retrySel_, @encode(GTMHTTPFetcher *), @encode(BOOL), @encode(NSError *), 0);
 243
 244  // We'll retain the delegate only during the outstanding connection (similar
 245  // to what Cocoa does with performSelectorOnMainThread:) and during
 246  // authorization or delays, since the app would crash
 247  // if the delegate was released before the fetch calls back
 248  [self setDelegate:delegate];
 249  finishedSel_ = finishedSelector;
 250
 251  return [self beginFetchMayDelay:YES
 252                     mayAuthorize:YES];
 253}
 254
 255- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
 256              mayAuthorize:(BOOL)mayAuthorize {
 257  // This is the internal entry point for re-starting fetches
 258  NSError *error = nil;
 259
 260  if (connection_ != nil) {
 261    NSAssert1(connection_ != nil, @"fetch object %@ being reused; this should never happen", self);
 262    goto CannotBeginFetch;
 263  }
 264
 265  if (request_ == nil || [request_ URL] == nil) {
 266    NSAssert(request_ != nil, @"beginFetchWithDelegate requires a request with a URL");
 267    goto CannotBeginFetch;
 268  }
 269
 270  self.downloadedData = nil;
 271  downloadedLength_ = 0;
 272
 273  if (mayDelay && service_) {
 274    BOOL shouldFetchNow = [service_ fetcherShouldBeginFetching:self];
 275    if (!shouldFetchNow) {
 276      // the fetch is deferred, but will happen later
 277      return YES;
 278    }
 279  }
 280
 281  NSString *effectiveHTTPMethod = [request_ valueForHTTPHeaderField:@"X-HTTP-Method-Override"];
 282  if (effectiveHTTPMethod == nil) {
 283    effectiveHTTPMethod = [request_ HTTPMethod];
 284  }
 285  BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil
 286                             || [effectiveHTTPMethod isEqual:@"GET"]);
 287
 288  if (postData_ || postStream_) {
 289    if (isEffectiveHTTPGet) {
 290      [request_ setHTTPMethod:@"POST"];
 291      isEffectiveHTTPGet = NO;
 292    }
 293
 294    if (postData_) {
 295      [request_ setHTTPBody:postData_];
 296    } else {
 297      if ([self respondsToSelector:@selector(setupStreamLogging)]) {
 298        [self performSelector:@selector(setupStreamLogging)];
 299      }
 300
 301      [request_ setHTTPBodyStream:postStream_];
 302    }
 303  }
 304
 305  // We authorize after setting up the http method and body in the request
 306  // because OAuth 1 may need to sign the request body
 307  if (mayAuthorize && authorizer_) {
 308    BOOL isAuthorized = [authorizer_ isAuthorizedRequest:request_];
 309    if (!isAuthorized) {
 310      // authorization needed
 311      return [self authorizeRequest];
 312    }
 313  }
 314
 315  [fetchHistory_ updateRequest:request_ isHTTPGet:isEffectiveHTTPGet];
 316
 317  // set the default upload or download retry interval, if necessary
 318  if (isRetryEnabled_
 319      && maxRetryInterval_ <= kUnsetMaxRetryInterval) {
 320    if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) {
 321      [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval];
 322    } else {
 323      [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval];
 324    }
 325  }
 326
 327  [self addCookiesToRequest:request_];
 328
 329  if (downloadPath_ != nil) {
 330    // downloading to a path, so create a temporary file and a file handle for
 331    // downloading
 332    NSString *tempPath = [self createTempDownloadFilePathForPath:downloadPath_];
 333
 334    BOOL didCreate = [[NSData data] writeToFile:tempPath
 335                                        options:0
 336                                          error:&error];
 337    if (!didCreate) goto CannotBeginFetch;
 338
 339    [self setTemporaryDownloadPath:tempPath];
 340
 341    NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:tempPath];
 342    if (fh == nil) goto CannotBeginFetch;
 343
 344    [self setDownloadFileHandle:fh];
 345  }
 346
 347  // finally, start the connection
 348
 349  Class connectionClass = [[self class] connectionClass];
 350
 351  NSOperationQueue *delegateQueue = delegateQueue_;
 352  if (delegateQueue &&
 353      ![connectionClass instancesRespondToSelector:@selector(setDelegateQueue:)]) {
 354    // NSURLConnection has no setDelegateQueue: on iOS 4 and Mac OS X 10.5.
 355    delegateQueue = nil;
 356    self.delegateQueue = nil;
 357  }
 358
 359#if DEBUG && TARGET_OS_IPHONE
 360  BOOL isPreIOS6 = (NSFoundationVersionNumber <= 890.1);
 361  if (isPreIOS6 && delegateQueue) {
 362    NSLog(@"GTMHTTPFetcher delegateQueue not safe in iOS 5");
 363  }
 364#endif
 365
 366  if (downloadFileHandle_ != nil) {
 367    // Downloading to a file, so downloadedData_ remains nil.
 368  } else {
 369    self.downloadedData = [NSMutableData data];
 370  }
 371
 372  hasConnectionEnded_ = NO;
 373  if ([runLoopModes_ count] == 0 && delegateQueue == nil) {
 374    // No custom callback modes or queue were specified, so start the connection
 375    // on the current run loop in the current mode
 376    connection_ = [[connectionClass connectionWithRequest:request_
 377                                                 delegate:self] retain];
 378  } else {
 379    // Specify callbacks be on an operation queue or on the current run loop
 380    // in the specified modes
 381    connection_ = [[connectionClass alloc] initWithRequest:request_
 382                                                  delegate:self
 383                                          startImmediately:NO];
 384    if (delegateQueue) {
 385      [connection_ performSelector:@selector(setDelegateQueue:)
 386                        withObject:delegateQueue];
 387    } else if (runLoopModes_) {
 388      NSRunLoop *rl = [NSRunLoop currentRunLoop];
 389      for (NSString *mode in runLoopModes_) {
 390        [connection_ scheduleInRunLoop:rl forMode:mode];
 391      }
 392    }
 393    [connection_ start];
 394  }
 395
 396  if (!connection_) {
 397    NSAssert(connection_ != nil, @"beginFetchWithDelegate could not create a connection");
 398    self.downloadedData = nil;
 399    goto CannotBeginFetch;
 400  }
 401
 402#if GTM_BACKGROUND_FETCHING
 403  backgroundTaskIdentifer_ = 0;  // UIBackgroundTaskInvalid is 0 on iOS 4
 404  if (shouldFetchInBackground_) {
 405    // For iOS 3 compatibility, ensure that UIApp supports backgrounding
 406    UIApplication *app = [UIApplication sharedApplication];
 407    if ([app respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) {
 408      // Tell UIApplication that we want to continue even when the app is in the
 409      // background.
 410      NSThread *thread = delegateQueue_ ? nil : [NSThread currentThread];
 411      backgroundTaskIdentifer_ = [app beginBackgroundTaskWithExpirationHandler:^{
 412        // Background task expiration callback - this block is always invoked by
 413        // UIApplication on the main thread.
 414        if (thread) {
 415          // Run the user's callbacks on the thread used to start the
 416          // fetch.
 417          [self performSelector:@selector(backgroundFetchExpired)
 418                       onThread:thread
 419                     withObject:nil
 420                  waitUntilDone:YES];
 421        } else {
 422          // backgroundFetchExpired invokes callbacks on the provided delegate
 423          // queue.
 424          [self backgroundFetchExpired];
 425        }
 426      }];
 427    }
 428  }
 429#endif
 430
 431  if (!initialRequestDate_) {
 432    initialRequestDate_ = [[NSDate alloc] init];
 433  }
 434
 435  // Once connection_ is non-nil we can send the start notification
 436  isStopNotificationNeeded_ = YES;
 437  NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter];
 438  [defaultNC postNotificationName:kGTMHTTPFetcherStartedNotification
 439                           object:self];
 440  return YES;
 441
 442CannotBeginFetch:
 443  [self failToBeginFetchDeferWithError:error];
 444  return NO;
 445}
 446
 447- (void)failToBeginFetchDeferWithError:(NSError *)error {
 448  if (delegateQueue_) {
 449    // Deferring will happen by the callback being invoked on the specified
 450    // queue.
 451    [self failToBeginFetchWithError:error];
 452  } else {
 453    // No delegate queue has been specified, so put the callback
 454    // on an appropriate run loop.
 455    NSArray *modes = (runLoopModes_ ? runLoopModes_ :
 456                      [NSArray arrayWithObject:NSRunLoopCommonModes]);
 457    [self performSelector:@selector(failToBeginFetchWithError:)
 458                 onThread:[NSThread currentThread]
 459               withObject:error
 460            waitUntilDone:NO
 461                    modes:modes];
 462  }
 463}
 464
 465- (void)failToBeginFetchWithError:(NSError *)error {
 466  if (error == nil) {
 467    error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
 468                                code:kGTMHTTPFetcherErrorDownloadFailed
 469                            userInfo:nil];
 470  }
 471
 472  [[self retain] autorelease];  // In case the callback releases us
 473
 474  [self invokeFetchCallbacksOnDelegateQueueWithData:nil
 475                                              error:error];
 476
 477  [self releaseCallbacks];
 478
 479  [service_ fetcherDidStop:self];
 480
 481  self.authorizer = nil;
 482
 483  if (temporaryDownloadPath_) {
 484    [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_
 485                                               error:NULL];
 486    self.temporaryDownloadPath = nil;
 487  }
 488}
 489
 490#if GTM_BACKGROUND_FETCHING
 491- (void)backgroundFetchExpired {
 492  // On background expiration, we stop the fetch and invoke the callbacks
 493  NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
 494                                       code:kGTMHTTPFetcherErrorBackgroundExpiration
 495                                   userInfo:nil];
 496  [self invokeFetchCallbacksOnDelegateQueueWithData:nil
 497                                              error:error];
 498  @synchronized(self) {
 499    // Stopping the fetch here will indirectly call endBackgroundTask
 500    [self stopFetchReleasingCallbacks:NO];
 501
 502    [self releaseCallbacks];
 503    self.authorizer = nil;
 504  }
 505}
 506
 507- (void)endBackgroundTask {
 508  @synchronized(self) {
 509    // Whenever the connection stops or background execution expires,
 510    // we need to tell UIApplication we're done
 511    if (backgroundTaskIdentifer_) {
 512      // If backgroundTaskIdentifer_ is non-zero, we know we're on iOS 4
 513      UIApplication *app = [UIApplication sharedApplication];
 514      [app endBackgroundTask:backgroundTaskIdentifer_];
 515
 516      backgroundTaskIdentifer_ = 0;
 517    }
 518  }
 519}
 520#endif // GTM_BACKGROUND_FETCHING
 521
 522- (BOOL)authorizeRequest {
 523  id authorizer = self.authorizer;
 524  SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:);
 525  if ([authorizer respondsToSelector:asyncAuthSel]) {
 526    SEL callbackSel = @selector(authorizer:request:finishedWithError:);
 527    [authorizer authorizeRequest:request_
 528                        delegate:self
 529               didFinishSelector:callbackSel];
 530    return YES;
 531  } else {
 532    NSAssert(authorizer == nil, @"invalid authorizer for fetch");
 533
 534    // No authorizing possible, and authorizing happens only after any delay;
 535    // just begin fetching
 536    return [self beginFetchMayDelay:NO
 537                       mayAuthorize:NO];
 538  }
 539}
 540
 541- (void)authorizer:(id <GTMFetcherAuthorizationProtocol>)auth
 542           request:(NSMutableURLRequest *)request
 543 finishedWithError:(NSError *)error {
 544  if (error != nil) {
 545    // We can't fetch without authorization
 546    [self failToBeginFetchDeferWithError:error];
 547  } else {
 548    [self beginFetchMayDelay:NO
 549                mayAuthorize:NO];
 550  }
 551}
 552
 553#if NS_BLOCKS_AVAILABLE
 554- (BOOL)beginFetchWithCompletionHandler:(void (^)(NSData *data, NSError *error))handler {
 555  self.completionBlock = handler;
 556
 557  // The user may have called setDelegate: earlier if they want to use other
 558  // delegate-style callbacks during the fetch; otherwise, the delegate is nil,
 559  // which is fine.
 560  return [self beginFetchWithDelegate:[self delegate]
 561                    didFinishSelector:nil];
 562}
 563#endif
 564
 565- (NSString *)createTempDownloadFilePathForPath:(NSString *)targetPath {
 566  NSString *tempDir = nil;
 567
 568#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED >= 1060))
 569  // Find an appropriate directory for the download, ideally on the same disk
 570  // as the final target location so the temporary file won't have to be moved
 571  // to a different disk.
 572  //
 573  // Available in SDKs for 10.6 and iOS 4
 574  //
 575  // Oct 2011: We previously also used URLForDirectory for
 576  //   (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED >= 40000))
 577  // but that is returning a non-temporary directory for iOS, unfortunately
 578
 579  SEL sel = @selector(URLForDirectory:inDomain:appropriateForURL:create:error:);
 580  if ([NSFileManager instancesRespondToSelector:sel]) {
 581    NSError *error = nil;
 582    NSURL *targetURL = [NSURL fileURLWithPath:targetPath];
 583    NSFileManager *fileMgr = [NSFileManager defaultManager];
 584
 585    NSURL *tempDirURL = [fileMgr URLForDirectory:NSItemReplacementDirectory
 586                                        inDomain:NSUserDomainMask
 587                               appropriateForURL:targetURL
 588                                          create:YES
 589                                           error:&error];
 590    tempDir = [tempDirURL path];
 591  }
 592#endif
 593
 594  if (tempDir == nil) {
 595    tempDir = NSTemporaryDirectory();
 596  }
 597
 598  static unsigned int counter = 0;
 599  NSString *name = [NSString stringWithFormat:@"gtmhttpfetcher_%u_%u",
 600                        ++counter, (unsigned int) arc4random()];
 601  NSString *result = [tempDir stringByAppendingPathComponent:name];
 602  return result;
 603}
 604
 605- (void)addCookiesToRequest:(NSMutableURLRequest *)request {
 606  // Get cookies for this URL from our storage array, if
 607  // we have a storage array
 608  if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault
 609      && cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodNone) {
 610
 611    NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]];
 612    if ([cookies count] > 0) {
 613
 614      NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
 615      NSString *cookieHeader = [headerFields objectForKey:@"Cookie"]; // key used in header dictionary
 616      if (cookieHeader) {
 617        [request addValue:cookieHeader forHTTPHeaderField:@"Cookie"]; // header name
 618      }
 619    }
 620  }
 621}
 622
 623// Returns YES if this is in the process of fetching a URL, or waiting to
 624// retry, or waiting for authorization, or waiting to be issued by the
 625// service object
 626- (BOOL)isFetching {
 627  if (connection_ != nil || retryTimer_ != nil) return YES;
 628
 629  BOOL isAuthorizing = [authorizer_ isAuthorizingRequest:request_];
 630  if (isAuthorizing) return YES;
 631
 632  BOOL isDelayed = [service_ isDelayingFetcher:self];
 633  return isDelayed;
 634}
 635
 636// Returns the status code set in connection:didReceiveResponse:
 637- (NSInteger)statusCode {
 638
 639  NSInteger statusCode;
 640
 641  if (response_ != nil
 642    && [response_ respondsToSelector:@selector(statusCode)]) {
 643
 644    statusCode = [(NSHTTPURLResponse *)response_ statusCode];
 645  } else {
 646    //  Default to zero, in hopes of hinting "Unknown" (we can't be
 647    //  sure that things are OK enough to use 200).
 648    statusCode = 0;
 649  }
 650  return statusCode;
 651}
 652
 653- (NSDictionary *)responseHeaders {
 654  if (response_ != nil
 655      && [response_ respondsToSelector:@selector(allHeaderFields)]) {
 656
 657    NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields];
 658    return headers;
 659  }
 660  return nil;
 661}
 662
 663- (void)releaseCallbacks {
 664  [delegate_ autorelease];
 665  delegate_ = nil;
 666
 667  [delegateQueue_ autorelease];
 668  delegateQueue_ = nil;
 669
 670#if NS_BLOCKS_AVAILABLE
 671  self.completionBlock = nil;
 672  self.sentDataBlock = nil;
 673  self.receivedDataBlock = nil;
 674  self.retryBlock = nil;
 675#endif
 676}
 677
 678// Cancel the fetch of the URL that's currently in progress.
 679- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
 680  id <GTMHTTPFetcherServiceProtocol> service;
 681
 682  // if the connection or the retry timer is all that's retaining the fetcher,
 683  // we want to be sure this instance survives stopping at least long enough for
 684  // the stack to unwind
 685  [[self retain] autorelease];
 686
 687  [self destroyRetryTimer];
 688
 689  @synchronized(self) {
 690    service = [[service_ retain] autorelease];
 691
 692    if (connection_) {
 693      // in case cancelling the connection calls this recursively, we want
 694      // to ensure that we'll only release the connection and delegate once,
 695      // so first set connection_ to nil
 696      NSURLConnection* oldConnection = connection_;
 697      connection_ = nil;
 698
 699      if (!hasConnectionEnded_) {
 700        [oldConnection cancel];
 701      }
 702
 703      // this may be called in a callback from the connection, so use autorelease
 704      [oldConnection autorelease];
 705    }
 706  }  // @synchronized(self)
 707
 708  // send the stopped notification
 709  [self sendStopNotificationIfNeeded];
 710
 711  @synchronized(self) {
 712    [authorizer_ stopAuthorizationForRequest:request_];
 713
 714    if (shouldReleaseCallbacks) {
 715      [self releaseCallbacks];
 716
 717      self.authorizer = nil;
 718    }
 719
 720    if (temporaryDownloadPath_) {
 721      [[NSFileManager defaultManager] removeItemAtPath:temporaryDownloadPath_
 722                                                 error:NULL];
 723      self.temporaryDownloadPath = nil;
 724    }
 725  }  // @synchronized(self)
 726
 727  [service fetcherDidStop:self];
 728
 729#if GTM_BACKGROUND_FETCHING
 730  [self endBackgroundTask];
 731#endif
 732}
 733
 734// External stop method
 735- (void)stopFetching {
 736  [self stopFetchReleasingCallbacks:YES];
 737}
 738
 739- (void)sendStopNotificationIfNeeded {
 740  BOOL sendNow = NO;
 741  @synchronized(self) {
 742    if (isStopNotificationNeeded_) {
 743      isStopNotificationNeeded_ = NO;
 744      sendNow = YES;
 745    }
 746  }
 747
 748  if (sendNow) {
 749    NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter];
 750    [defaultNC postNotificationName:kGTMHTTPFetcherStoppedNotification
 751                             object:self];
 752  }
 753}
 754
 755- (void)retryFetch {
 756  [self stopFetchReleasingCallbacks:NO];
 757
 758  [self beginFetchWithDelegate:delegate_
 759             didFinishSelector:finishedSel_];
 760}
 761
 762- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
 763  NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
 764
 765  // Loop until the callbacks have been called and released, and until
 766  // the connection is no longer pending, or until the timeout has expired
 767  BOOL isMainThread = [NSThread isMainThread];
 768
 769  while ((!hasConnectionEnded_
 770#if NS_BLOCKS_AVAILABLE
 771          || completionBlock_ != nil
 772#endif
 773          || delegate_ != nil)
 774         && [giveUpDate timeIntervalSinceNow] > 0) {
 775
 776    // Run the current run loop 1/1000 of a second to give the networking
 777    // code a chance to work
 778    if (isMainThread || delegateQueue_ == nil) {
 779      NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
 780      [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
 781    } else {
 782      [NSThread sleepForTimeInterval:0.001];
 783    }
 784  }
 785}
 786
 787#pragma mark NSURLConnection Delegate Methods
 788
 789//
 790// NSURLConnection Delegate Methods
 791//
 792
 793// This method just says "follow all redirects", which _should_ be the default behavior,
 794// According to file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Conceptual/URLLoadingSystem
 795// but the redirects were not being followed until I added this method.  May be
 796// a bug in the NSURLConnection code, or the documentation.
 797//
 798// In OS X 10.4.8 and earlier, the redirect request doesn't
 799// get the original's headers and body. This causes POSTs to fail.
 800// So we construct a new request, a copy of the original, with overrides from the
 801// redirect.
 802//
 803// Docs say that if redirectResponse is nil, just return the redirectRequest.
 804
 805- (NSURLRequest *)connection:(NSURLConnection *)connection
 806             willSendRequest:(NSURLRequest *)redirectRequest
 807            redirectResponse:(NSURLResponse *)redirectResponse {
 808  @synchronized(self) {
 809    if (redirectRequest && redirectResponse) {
 810      // save cookies from the response
 811      [self handleCookiesForResponse:redirectResponse];
 812
 813      NSMutableURLRequest *newRequest = [[request_ mutableCopy] autorelease];
 814      // copy the URL
 815      NSURL *redirectURL = [redirectRequest URL];
 816      NSURL *url = [newRequest URL];
 817
 818      // disallow scheme changes (say, from https to http)
 819      NSString *redirectScheme = [url scheme];
 820      NSString *newScheme = [redirectURL scheme];
 821      NSString *newResourceSpecifier = [redirectURL resourceSpecifier];
 822
 823      if ([redirectScheme caseInsensitiveCompare:@"http"] == NSOrderedSame
 824          && newScheme != nil
 825          && [newScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
 826
 827        // allow the change from http to https
 828        redirectScheme = newScheme;
 829      }
 830
 831      NSString *newUrlString = [NSString stringWithFormat:@"%@:%@",
 832        redirectScheme, newResourceSpecifier];
 833
 834      NSURL *newURL = [NSURL URLWithString:newUrlString];
 835      [newRequest setURL:newURL];
 836
 837      // any headers in the redirect override headers in the original.
 838      NSDictionary *redirectHeaders = [redirectRequest allHTTPHeaderFields];
 839      for (NSString *key in redirectHeaders) {
 840        NSString *value = [redirectHeaders objectForKey:key];
 841        [newRequest setValue:value forHTTPHeaderField:key];
 842      }
 843
 844      [self addCookiesToRequest:newRequest];
 845
 846      redirectRequest = newRequest;
 847
 848      // log the response we just received
 849      [self setResponse:redirectResponse];
 850      [self logNowWithError:nil];
 851
 852      // update the request for future logging
 853      NSMutableURLRequest *mutable = [[redirectRequest mutableCopy] autorelease];
 854      [self setMutableRequest:mutable];
 855    }
 856    return redirectRequest;
 857  }  // @synchronized(self)
 858}
 859
 860- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
 861  @synchronized(self) {
 862    // This method is called when the server has determined that it
 863    // has enough information to create the NSURLResponse
 864    // it can be called multiple times, for example in the case of a
 865    // redirect, so each time we reset the data.
 866    [downloadedData_ setLength:0];
 867    [downloadFileHandle_ truncateFileAtOffset:0];
 868    downloadedLength_ = 0;
 869
 870    [self setResponse:response];
 871
 872    // Save cookies from the response
 873    [self handleCookiesForResponse:response];
 874  }
 875}
 876
 877
 878// handleCookiesForResponse: handles storage of cookies for responses passed to
 879// connection:willSendRequest:redirectResponse: and connection:didReceiveResponse:
 880- (void)handleCookiesForResponse:(NSURLResponse *)response {
 881
 882  if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodSystemDefault
 883    || cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodNone) {
 884
 885    // do nothing special for NSURLConnection's default storage mechanism
 886    // or when we're ignoring cookies
 887
 888  } else if ([response respondsToSelector:@selector(allHeaderFields)]) {
 889
 890    // grab the cookies from the header as NSHTTPCookies and store them either
 891    // into our static array or into the fetchHistory
 892
 893    NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields];
 894    if (responseHeaderFields) {
 895
 896      NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaderFields
 897                                                                forURL:[response URL]];
 898      if ([cookies count] > 0) {
 899        [cookieStorage_ setCookies:cookies];
 900      }
 901    }
 902  }
 903}
 904
 905-(void)connection:(NSURLConnection *)connection
 906didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
 907  @synchronized(self) {
 908    if ([challenge previousFailureCount] <= 2) {
 909
 910      NSURLCredential *credential = credential_;
 911
 912      if ([[challenge protectionSpace] isProxy] && proxyCredential_ != nil) {
 913        credential = proxyCredential_;
 914      }
 915
 916      // Here, if credential is still nil, then we *could* try to get it from
 917      // NSURLCredentialStorage's defaultCredentialForProtectionSpace:.
 918      // We don't, because we're assuming:
 919      //
 920      // - for server credentials, we only want ones supplied by the program
 921      //   calling http fetcher
 922      // - for proxy credentials, if one were necessary and available in the
 923      //   keychain, it would've been found automatically by NSURLConnection
 924      //   and this challenge delegate method never would've been called
 925      //   anyway
 926
 927      if (credential) {
 928        // try the credential
 929        [[challenge sender] useCredential:credential
 930               forAuthenticationChallenge:challenge];
 931        return;
 932      }
 933    }  // @synchronized(self)
 934
 935    // If we don't have credentials, or we've already failed auth 3x,
 936    // report the error, putting the challenge as a value in the userInfo
 937    // dictionary.
 938#if DEBUG
 939    NSAssert(!isCancellingChallenge_, @"isCancellingChallenge_ unexpected");
 940#endif
 941    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:challenge
 942                                                         forKey:kGTMHTTPFetcherErrorChallengeKey];
 943    NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
 944                                         code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed
 945                                     userInfo:userInfo];
 946
 947    // cancelAuthenticationChallenge seems to indirectly call
 948    // connection:didFailWithError: now, though that isn't documented
 949    //
 950    // We'll use an ivar to make the indirect invocation of the
 951    // delegate method do nothing.
 952    isCancellingChallenge_ = YES;
 953    [[challenge sender] cancelAuthenticationChallenge:challenge];
 954    isCancellingChallenge_ = NO;
 955
 956    [self connection:connection didFailWithError:error];
 957  }
 958}
 959
 960- (void)invokeFetchCallbacksWithData:(NSData *)data
 961                               error:(NSError *)error {
 962  // To avoid deadlocks, this should not be called inside of @synchronized(self)
 963  id target;
 964  SEL sel;
 965#if NS_BLOCKS_AVAILABLE
 966  void (^block)(NSData *, NSError *);
 967#endif
 968
 969  // If -stopFetching is called in another thread directly after this @synchronized stanza finishes
 970  // on this thread, then target and block could be released before being used in this method. So
 971  // retain each until this method is done with them.
 972  @synchronized(self) {
 973    target = [[delegate_ retain] autorelease];
 974    sel = finishedSel_;
 975#if NS_BLOCKS_AVAILABLE
 976    block = [[completionBlock_ retain] autorelease];
 977#endif
 978  }
 979
 980  [[self retain] autorelease];  // In case the callback releases us
 981
 982  [self invokeFetchCallback:sel
 983                     target:target
 984                       data:data
 985                      error:error];
 986
 987#if NS_BLOCKS_AVAILABLE
 988  if (block) {
 989    block(data, error);
 990  }
 991#endif
 992}
 993
 994- (void)invokeFetchCallback:(SEL)sel
 995                     target:(id)target
 996                       data:(NSData *)data
 997                      error:(NSError *)error {
 998  // This method is available to subclasses which may provide a customized
 999  // target pointer.
1000  if (target && sel) {
1001    NSMethodSignature *sig = [target methodSignatureForSelector:sel];
1002    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
1003    [invocation setSelector:sel];
1004    [invocation setTarget:target];
1005    [invocation setArgument:&self atIndex:2];
1006    [invocation setArgument:&data atIndex:3];
1007    [invocation setArgument:&error atIndex:4];
1008    [invocation invoke];
1009  }
1010}
1011
1012- (void)invokeFetchCallbacksOnDelegateQueueWithData:(NSData *)data
1013                                              error:(NSError *)error {
1014  // This is called by methods that are not already on the delegateQueue
1015  // (as NSURLConnection callbacks should already be, but other failures
1016  // are not.)
1017  if (!delegateQueue_) {
1018    [self invokeFetchCallbacksWithData:data error:error];
1019  }
1020
1021  // Values may be nil.
1022  NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:2];
1023  [dict setValue:data forKey:kCallbackData];
1024  [dict setValue:error forKey:kCallbackError];
1025  NSInvocationOperation *op =
1026    [[[NSInvocationOperation alloc] initWithTarget:self
1027                                          selector:@selector(invokeOnQueueWithDictionary:)
1028                                            object:dict] autorelease];
1029  [delegateQueue_ addOperation:op];
1030}
1031
1032- (void)invokeOnQueueWithDictionary:(NSDictionary *)dict {
1033  NSData *data = [dict objectForKey:kCallbackData];
1034  NSError *error = [dict objectForKey:kCallbackError];
1035
1036  [self invokeFetchCallbacksWithData:data error:error];
1037}
1038
1039
1040- (void)invokeSentDataCallback:(SEL)sel
1041                        target:(id)target
1042               didSendBodyData:(NSInteger)bytesWritten
1043             totalBytesWritten:(NSInteger)totalBytesWritten
1044     totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
1045  if (target && sel) {
1046    NSMethodSignature *sig = [target methodSignatureForSelector:sel];
1047    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
1048    [invocation setSelector:sel];
1049    [invocation setTarget:target];
1050    [invocation setArgument:&self atIndex:2];
1051    [invocation setArgument:&bytesWritten atIndex:3];
1052    [invocation setArgument:&totalBytesWritten atIndex:4];
1053    [invocation setArgument:&totalBytesExpectedToWrite atIndex:5];
1054    [invocation invoke];
1055  }
1056}
1057
1058- (BOOL)invokeRetryCallback:(SEL)sel
1059                     target:(id)target
1060                  willRetry:(BOOL)willRetry
1061                      error:(NSError *)error {
1062  if (target && sel) {
1063    NSMethodSignature *sig = [target methodSignatureForSelector:sel];
1064    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
1065    [invocation setSelector:sel];
1066    [invocation setTarget:target];
1067    [invocation setArgument:&self atIndex:2];
1068    [invocation setArgument:&willRetry atIndex:3];
1069    [invocation setArgument:&error atIndex:4];
1070    [invocation invoke];
1071
1072    [invocation getReturnValue:&willRetry];
1073  }
1074  return willRetry;
1075}
1076
1077- (void)connection:(NSURLConnection *)connection
1078   didSendBodyData:(NSInteger)bytesWritten
1079 totalBytesWritten:(NSInteger)totalBytesWritten
1080totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
1081  @synchronized(self) {
1082    SEL sel = [self sentDataSelector];
1083    [self invokeSentDataCallback:sel
1084                          target:delegate_
1085                 didSendBodyData:bytesWritten
1086               totalBytesWritten:totalBytesWritten
1087       totalBytesExpectedToWrite:totalBytesExpectedToWrite];
1088
1089#if NS_BLOCKS_AVAILABLE
1090    if (sentDataBlock_) {
1091      sentDataBlock_(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
1092    }
1093#endif
1094  }
1095}
1096
1097- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
1098  @synchronized(self) {
1099#if DEBUG
1100    NSAssert(!hasConnectionEnded_, @"Connection received data after ending");
1101
1102    // The download file handle should be set or the data object allocated
1103    // before the fetch is started.
1104    NSAssert((downloadFileHandle_ == nil) != (downloadedData_ == nil),
1105             @"received data accumulates as either NSData (%d) or"
1106             @" NSFileHandle (%d)",
1107             (downloadedData_ != nil), (downloadFileHandle_ != nil));
1108#endif
1109    // Hopefully, we'll never see this execute out-of-order, receiving data
1110    // after we've received the finished or failed callback.
1111    if (hasConnectionEnded_) return;
1112
1113    if (downloadFileHandle_ != nil) {
1114      // Append to file
1115      @try {
1116        [downloadFileHandle_ writeData:data];
1117
1118        downloadedLength_ = [downloadFileHandle_ offsetInFile];
1119      }
1120      @catch (NSException *exc) {
1121        // Couldn't write to file, probably due to a full disk
1122        NSDictionary *userInfo = [NSDictionary dictionaryWithObject:[exc reason]
1123                                                             forKey:NSLocalizedDescriptionKey];
1124        NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
1125                                             code:kGTMHTTPFetcherErrorFileHandleException
1126                                         userInfo:userInfo];
1127        [self connection:connection didFailWithError:error];
1128        return;
1129      }
1130    } else {
1131      // append to mutable data
1132      [downloadedData_ appendData:data];
1133
1134      downloadedLength_ = [downloadedData_ length];
1135    }
1136
1137    if (receivedDataSel_) {
1138      [delegate_ performSelector:receivedDataSel_
1139                      withObject:self
1140                      withObject:downloadedData_];
1141    }
1142
1143#if NS_BLOCKS_AVAILABLE
1144    if (receivedDataBlock_) {
1145      receivedDataBlock_(downloadedData_);
1146    }
1147#endif
1148  }  // @synchronized(self)
1149}
1150
1151// For error 304's ("Not Modified") where we've cached the data, return
1152// status 200 ("OK") to the caller (but leave the fetcher status as 304)
1153// and copy the cached data.
1154//
1155// For other errors or if there's no cached data, just return the actual status.
1156- (NSData *)cachedDataForStatus {
1157  if ([self statusCode] == kGTMHTTPFetcherStatusNotModified
1158      && [fetchHistory_ shouldCacheETaggedData]) {
1159    NSData *cachedData = [fetchHistory_ cachedDataForRequest:request_];
1160    return cachedData;
1161  }
1162  return nil;
1163}
1164
1165- (NSInteger)statusAfterHandlingNotModifiedError {
1166  NSInteger status = [self statusCode];
1167  NSData *cachedData = [self cachedDataForStatus];
1168  if (cachedData) {
1169    // Forge the status to pass on to the delegate
1170    status = 200;
1171
1172    // Copy our stored data
1173    if (downloadFileHandle_ != nil) {
1174      @try {
1175        // Downloading to a file handle won't save to the cache (the data is
1176        // likely inappropriately large for caching), but will still read from
1177        // the cache, on the unlikely chance that the response was Not Modified
1178        // and the URL response was indeed present in the cache.
1179        [downloadFileHandle_ truncateFileAtOffset:0];
1180        [downloadFileHandle_ writeData:cachedData];
1181        downloadedLength_ = [downloadFileHandle_ offsetInFile];
1182      }
1183      @catch (NSException *) {
1184        // Failed to write data, likely due to lack of disk space
1185        status = kGTMHTTPFetcherErrorFileHandleException;
1186      }
1187    } else {
1188      [downloadedData_ setData:cachedData];
1189      downloadedLength_ = [cachedData length];
1190    }
1191  }
1192  return status;
1193}
1194
1195- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
1196  BOOL shouldStopFetching = YES;
1197  BOOL shouldSendStopNotification = NO;
1198  NSError *error = nil;
1199  NSData *downloadedData;
1200#if !STRIP_GTM_FETCH_LOGGING
1201  BOOL shouldDeferLogging = NO;
1202#endif
1203  BOOL shouldBeginRetryTimer = NO;
1204
1205  @synchronized(self) {
1206    // We no longer need to cancel the connection
1207    hasConnectionEnded_ = YES;
1208
1209    // Skip caching ETagged results when the data is being saved to a file
1210    if (downloadFileHandle_ == nil) {
1211      [fetchHistory_ updateFetchHistoryWithRequest:request_
1212                                          response:response_
1213                                    downloadedData:downloadedData_];
1214    } else {
1215      [fetchHistory_ removeCachedDataForRequest:request_];
1216    }
1217
1218    [[self retain] autorelease]; // in case the callback releases us
1219
1220    NSInteger status = [self statusCode];
1221    if ([self cachedDataForStatus] != nil) {
1222#if !STRIP_GTM_FETCH_LOGGING
1223      // Log the pre-cache response.
1224      [self logNowWithError:nil];
1225      hasLoggedError_ = YES;
1226#endif
1227      status = [self statusAfterHandlingNotModifiedError];
1228    }
1229
1230    shouldSendStopNotification = YES;
1231
1232    if (status >= 0 && status < 300) {
1233      // success
1234      if (downloadPath_) {
1235        // Avoid deleting the downloaded file when the fetch stops
1236        [downloadFileHandle_ closeFile];
1237        self.downloadFileHandle = nil;
1238
1239        NSFileManager *fileMgr = [NSFileManager defaultManager];
1240        [fileMgr removeItemAtPath:downloadPath_
1241                            error:NULL];
1242
1243        if ([fileMgr moveItemAtPath:temporaryDownloadPath_
1244                             toPath:downloadPath_
1245                              error:&error]) {
1246          self.temporaryDownloadPath = nil;
1247        }
1248      }
1249    } else {
1250      // unsuccessful
1251#if !STRIP_GTM_FETCH_LOGGING
1252      if (!hasLoggedError_) {
1253        [self logNowWithError:nil];
1254        hasLoggedError_ = YES;
1255      }
1256#endif
1257      // Status over 300; retry or notify the delegate of failure
1258      if ([self shouldRetryNowForStatus:status error:nil]) {
1259        // retrying
1260        shouldBeginRetryTimer = YES;
1261        shouldStopFetching = NO;
1262      } else {
1263        NSDictionary *userInfo = nil;
1264        if ([downloadedData_ length] > 0) {
1265          userInfo = [NSDictionary dictionaryWithObject:downloadedData_
1266                                                 forKey:kGTMHTTPFetcherStatusDataKey];
1267        }
1268        error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
1269                                    code:status
1270                                userInfo:userInfo];
1271      }
1272    }
1273    downloadedData = downloadedData_;
1274#if !STRIP_GTM_FETCH_LOGGING
1275    shouldDeferLogging = shouldDeferResponseBodyLogging_;
1276#endif
1277  }  // @synchronized(self)
1278
1279  if (shouldBeginRetryTimer) {
1280    [self beginRetryTimer];
1281  }
1282
1283  if (shouldSendStopNotification) {
1284    // We want to send the stop notification before calling the delegate's
1285    // callback selector, since the callback selector may release all of
1286    // the fetcher properties that the client is using to track the fetches.
1287    //
1288    // We'll also stop now so that, to any observers watching the notifications,
1289    // it doesn't look like our wait for a retry (which may be long,
1290    // 30 seconds or more) is part of the network activity.
1291    [self sendStopNotificationIfNeeded];
1292  }
1293
1294  if (shouldStopFetching) {
1295    // Call the callbacks (outside of the @synchronized to avoid deadlocks.)
1296    [self invokeFetchCallbacksWithData:downloadedData
1297                                 error:error];
1298    BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion];
1299    [self stopFetchReleasingCallbacks:shouldRelease];
1300  }
1301
1302#if !STRIP_GTM_FETCH_LOGGING
1303  @synchronized(self) {
1304    if (!shouldDeferLogging && !hasLoggedError_) {
1305      [self logNowWithError:nil];
1306    }
1307  }
1308#endif
1309}
1310
1311- (BOOL)shouldReleaseCallbacksUponCompletion {
1312  // A subclass can override this to keep callbacks around after the
1313  // connection has finished successfully
1314  return YES;
1315}
1316
1317- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
1318  @synchronized(self) {
1319    // Prevent the failure callback from being called twice, since the stopFetch
1320    // call below (either the explicit one at the end of this method, or the
1321    // implicit one when the retry occurs) will release the delegate.
1322    if (connection_ == nil) return;
1323
1324    // If this method was invoked indirectly by cancellation of an authentication
1325    // challenge, defer this until it is called again with the proper error object
1326    if (isCancellingChallenge_) return;
1327
1328    // We no longer need to cancel the connection
1329    hasConnectionEnded_ = YES;
1330
1331    [self logNowWithError:error];
1332  }
1333
1334  // See comment about sendStopNotificationIfNeeded
1335  // in connectionDidFinishLoading:
1336  [self sendStopNotificationIfNeeded];
1337
1338  if ([self shouldRetryNowForStatus:0 error:error]) {
1339    [self beginRetryTimer];
1340  } else {
1341    [[self retain] autorelease]; // in case the callback releases us
1342
1343    [self invokeFetchCallbacksWithData:nil
1344                                 error:error];
1345
1346    [self stopFetchReleasingCallbacks:YES];
1347  }
1348}
1349
1350- (void)logNowWithError:(NSError *)error {
1351  // If the logging category is available, then log the current request,
1352  // response, data, and error
1353  if ([self respondsToSelector:@selector(logFetchWithError:)]) {
1354    [self performSelector:@selector(logFetchWithError:) withObject:error];
1355  }
1356}
1357
1358#pragma mark Retries
1359
1360- (BOOL)isRetryError:(NSError *)error {
1361
1362  struct retryRecord {
1363    NSString *const domain;
1364    int code;
1365  };
1366
1367  struct retryRecord retries[] = {
1368    { kGTMHTTPFetcherStatusDomain, 408 }, // request timeout
1369    { kGTMHTTPFetcherStatusDomain, 503 }, // service unavailable
1370    { kGTMHTTPFetcherStatusDomain, 504 }, // request timeout
1371    { NSURLErrorDomain, NSURLErrorTimedOut },
1372    { NSURLErrorDomain, NSURLErrorNetworkConnectionLost },
1373    { nil, 0 }
1374  };
1375
1376  // NSError's isEqual always returns false for equal but distinct instances
1377  // of NSError, so we have to compare the domain and code values explicitly
1378
1379  for (int idx = 0; retries[idx].domain != nil; idx++) {
1380
1381    if ([[error domain] isEqual:retries[idx].domain]
1382        && [error code] == retries[idx].code) {
1383
1384      return YES;
1385    }
1386  }
1387  return NO;
1388}
1389
1390
1391// shouldRetryNowForStatus:error: returns YES if the user has enabled retries
1392// and the status or error is one that is suitable for retrying.  "Suitable"
1393// means either the isRetryError:'s list contains the status or error, or the
1394// user's retrySelector: is present and returns YES when called, or the
1395// authorizer may be able to fix.
1396- (BOOL)shouldRetryNowForStatus:(NSInteger)status
1397                          error:(NSError *)error {
1398  // Determine if a refreshed authorizer may avoid an authorization error
1399  BOOL shouldRetryForAuthRefresh = NO;
1400  BOOL isFirstAuthError = (authorizer_ != nil)
1401    && !hasAttemptedAuthRefresh_
1402    && (status == kGTMHTTPFetcherStatusUnauthorized); // 401
1403
1404  if (isFirstAuthError) {
1405    if ([authorizer_ respondsToSelector:@selector(primeForRefresh)]) {
1406      BOOL hasPrimed = [authorizer_ primeForRefresh];
1407      if (hasPrimed) {
1408        shouldRetryForAuthRefresh = YES;
1409        hasAttemptedAuthRefresh_ = YES;
1410        [request_ setValue:nil forHTTPHeaderField:@"Authorization"];
1411      }
1412    }
1413  }
1414
1415  // Determine if we're doing exponential backoff retries
1416  BOOL shouldDoIntervalRetry = [self isRetryEnabled]
1417    && ([self nextRetryInterval] < [self maxRetryInterval]);
1418
1419  if (shouldDoIntervalRetry) {
1420    // If an explicit max retry interval was set, we expect repeated backoffs to take
1421    // up to roughly twice that for repeated fast failures.  If the initial attempt is
1422    // already more than 3 times the max retry interval, then failures have taken a long time
1423    // (such as from network timeouts) so don't retry again to avoid the app becoming
1424    // unexpectedly unresponsive.
1425    if (maxRetryInterval_ > kUnsetMaxRetryInterval) {
1426      NSTimeInterval maxAllowedIntervalBeforeRetry = maxRetryInterval_ * 3;
1427      NSTimeInterval timeSinceInitialRequest = -[initialRequestDate_ timeIntervalSinceNow];
1428      if (timeSinceInitialRequest > maxAllowedIntervalBeforeRetry) {
1429        shouldDoIntervalRetry = NO;
1430      }
1431    }
1432  }
1433
1434  BOOL willRetry = NO;
1435  BOOL canRetry = shouldRetryForAuthRefresh || shouldDoIntervalRetry;
1436  if (canRetry) {
1437    // Check if this is a retryable error
1438    if (error == nil) {
1439      // Make an error for the status
1440      NSDictionary *userInfo = nil;
1441      if ([downloadedData_ length] > 0) {
1442        userInfo = [NSDictionary dictionaryWithObject:downloadedData_
1443                                               forKey:kGTMHTTPFetcherStatusDataKey];
1444

Large files files are truncated, but you can click here to view the full file