PageRenderTime 61ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

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