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

http://macfuse.googlecode.com/ · Objective C · 612 lines · 394 code · 121 blank · 97 comment · 69 complexity · 596ba1fc2a20d7227a3c75ef8c13196c MD5 · raw file

  1. /* Copyright (c) 2010 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. // GTMHTTPFetchHistory.m
  17. //
  18. #import "GTMHTTPFetchHistory.h"
  19. const NSTimeInterval kCachedURLReservationInterval = 60.0; // 1 minute
  20. static NSString* const kGTMIfNoneMatchHeader = @"If-None-Match";
  21. static NSString* const kGTMETagHeader = @"Etag";
  22. #if GTM_IPHONE
  23. // iPhone: up to 1MB memory
  24. const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity = 1 * 1024 * 1024;
  25. #else
  26. // Mac OS X: up to 15MB memory
  27. const NSUInteger kGTMDefaultETaggedDataCacheMemoryCapacity = 15 * 1024 * 1024;
  28. #endif
  29. @implementation GTMCookieStorage
  30. - (id)init {
  31. self = [super init];
  32. if (self != nil) {
  33. cookies_ = [[NSMutableArray alloc] init];
  34. }
  35. return self;
  36. }
  37. - (void)dealloc {
  38. [cookies_ release];
  39. [super dealloc];
  40. }
  41. // Add all cookies in the new cookie array to the storage,
  42. // replacing stored cookies as appropriate.
  43. //
  44. // Side effect: removes expired cookies from the storage array.
  45. - (void)setCookies:(NSArray *)newCookies {
  46. @synchronized(cookies_) {
  47. [self removeExpiredCookies];
  48. for (NSHTTPCookie *newCookie in newCookies) {
  49. if ([[newCookie name] length] > 0
  50. && [[newCookie domain] length] > 0
  51. && [[newCookie path] length] > 0) {
  52. // remove the cookie if it's currently in the array
  53. NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
  54. if (oldCookie) {
  55. [cookies_ removeObjectIdenticalTo:oldCookie];
  56. }
  57. // make sure the cookie hasn't already expired
  58. NSDate *expiresDate = [newCookie expiresDate];
  59. if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
  60. [cookies_ addObject:newCookie];
  61. }
  62. } else {
  63. NSAssert1(NO, @"Cookie incomplete: %@", newCookie);
  64. }
  65. }
  66. }
  67. }
  68. - (void)deleteCookie:(NSHTTPCookie *)cookie {
  69. @synchronized(cookies_) {
  70. NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
  71. if (foundCookie) {
  72. [cookies_ removeObjectIdenticalTo:foundCookie];
  73. }
  74. }
  75. }
  76. // Retrieve all cookies appropriate for the given URL, considering
  77. // domain, path, cookie name, expiration, security setting.
  78. // Side effect: removed expired cookies from the storage array.
  79. - (NSArray *)cookiesForURL:(NSURL *)theURL {
  80. NSMutableArray *foundCookies = nil;
  81. @synchronized(cookies_) {
  82. [self removeExpiredCookies];
  83. // We'll prepend "." to the desired domain, since we want the
  84. // actual domain "nytimes.com" to still match the cookie domain
  85. // ".nytimes.com" when we check it below with hasSuffix.
  86. NSString *host = [[theURL host] lowercaseString];
  87. NSString *path = [theURL path];
  88. NSString *scheme = [theURL scheme];
  89. NSString *domain = nil;
  90. BOOL isLocalhostRetrieval = NO;
  91. if ([host isEqual:@"localhost"]) {
  92. isLocalhostRetrieval = YES;
  93. } else {
  94. if (host) {
  95. domain = [@"." stringByAppendingString:host];
  96. }
  97. }
  98. NSUInteger numberOfCookies = [cookies_ count];
  99. for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
  100. NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
  101. NSString *cookieDomain = [[storedCookie domain] lowercaseString];
  102. NSString *cookiePath = [storedCookie path];
  103. BOOL cookieIsSecure = [storedCookie isSecure];
  104. BOOL isDomainOK;
  105. if (isLocalhostRetrieval) {
  106. // prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
  107. // is "localhost.local"
  108. isDomainOK = [cookieDomain isEqual:@"localhost"]
  109. || [cookieDomain isEqual:@"localhost.local"];
  110. } else {
  111. isDomainOK = [domain hasSuffix:cookieDomain];
  112. }
  113. BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
  114. BOOL isSecureOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
  115. if (isDomainOK && isPathOK && isSecureOK) {
  116. if (foundCookies == nil) {
  117. foundCookies = [NSMutableArray arrayWithCapacity:1];
  118. }
  119. [foundCookies addObject:storedCookie];
  120. }
  121. }
  122. }
  123. return foundCookies;
  124. }
  125. // Return a cookie from the array with the same name, domain, and path as the
  126. // given cookie, or else return nil if none found.
  127. //
  128. // Both the cookie being tested and all cookies in the storage array should
  129. // be valid (non-nil name, domains, paths).
  130. //
  131. // Note: this should only be called from inside a @synchronized(cookies_) block
  132. - (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
  133. NSUInteger numberOfCookies = [cookies_ count];
  134. NSString *name = [cookie name];
  135. NSString *domain = [cookie domain];
  136. NSString *path = [cookie path];
  137. NSAssert3(name && domain && path, @"Invalid cookie (name:%@ domain:%@ path:%@)",
  138. name, domain, path);
  139. for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
  140. NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:idx];
  141. if ([[storedCookie name] isEqual:name]
  142. && [[storedCookie domain] isEqual:domain]
  143. && [[storedCookie path] isEqual:path]) {
  144. return storedCookie;
  145. }
  146. }
  147. return nil;
  148. }
  149. // Internal routine to remove any expired cookies from the array, excluding
  150. // cookies with nil expirations.
  151. //
  152. // Note: this should only be called from inside a @synchronized(cookies_) block
  153. - (void)removeExpiredCookies {
  154. // count backwards since we're deleting items from the array
  155. for (NSInteger idx = (NSInteger)[cookies_ count] - 1; idx >= 0; idx--) {
  156. NSHTTPCookie *storedCookie = [cookies_ objectAtIndex:(NSUInteger)idx];
  157. NSDate *expiresDate = [storedCookie expiresDate];
  158. if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
  159. [cookies_ removeObjectAtIndex:(NSUInteger)idx];
  160. }
  161. }
  162. }
  163. - (void)removeAllCookies {
  164. @synchronized(cookies_) {
  165. [cookies_ removeAllObjects];
  166. }
  167. }
  168. @end
  169. //
  170. // GTMCachedURLResponse
  171. //
  172. @implementation GTMCachedURLResponse
  173. @synthesize response = response_;
  174. @synthesize data = data_;
  175. @synthesize reservationDate = reservationDate_;
  176. @synthesize useDate = useDate_;
  177. - (id)initWithResponse:(NSURLResponse *)response data:(NSData *)data {
  178. self = [super init];
  179. if (self != nil) {
  180. response_ = [response retain];
  181. data_ = [data retain];
  182. useDate_ = [[NSDate alloc] init];
  183. }
  184. return self;
  185. }
  186. - (void)dealloc {
  187. [response_ release];
  188. [data_ release];
  189. [useDate_ release];
  190. [reservationDate_ release];
  191. [super dealloc];
  192. }
  193. - (NSString *)description {
  194. NSString *reservationStr = reservationDate_ ?
  195. [NSString stringWithFormat:@" resDate:%@", reservationDate_] : @"";
  196. return [NSString stringWithFormat:@"%@ %p: {bytes:%@ useDate:%@%@}",
  197. [self class], self,
  198. data_ ? [NSNumber numberWithInt:(int)[data_ length]] : nil,
  199. useDate_,
  200. reservationStr];
  201. }
  202. - (NSComparisonResult)compareUseDate:(GTMCachedURLResponse *)other {
  203. return [useDate_ compare:[other useDate]];
  204. }
  205. @end
  206. //
  207. // GTMURLCache
  208. //
  209. @implementation GTMURLCache
  210. @dynamic memoryCapacity;
  211. - (id)init {
  212. return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity];
  213. }
  214. - (id)initWithMemoryCapacity:(NSUInteger)totalBytes {
  215. self = [super init];
  216. if (self != nil) {
  217. memoryCapacity_ = totalBytes;
  218. responses_ = [[NSMutableDictionary alloc] initWithCapacity:5];
  219. reservationInterval_ = kCachedURLReservationInterval;
  220. }
  221. return self;
  222. }
  223. - (void)dealloc {
  224. [responses_ release];
  225. [super dealloc];
  226. }
  227. - (NSString *)description {
  228. return [NSString stringWithFormat:@"%@ %p: {responses:%@}",
  229. [self class], self, [responses_ allValues]];
  230. }
  231. // Setters/getters
  232. - (void)pruneCacheResponses {
  233. // Internal routine to remove the least-recently-used responses when the
  234. // cache has grown too large
  235. if (memoryCapacity_ >= totalDataSize_) return;
  236. // Sort keys by date
  237. SEL sel = @selector(compareUseDate:);
  238. NSArray *sortedKeys = [responses_ keysSortedByValueUsingSelector:sel];
  239. // The least-recently-used keys are at the beginning of the sorted array;
  240. // remove those (except ones still reserved) until the total data size is
  241. // reduced sufficiently
  242. for (NSURL *key in sortedKeys) {
  243. GTMCachedURLResponse *response = [responses_ objectForKey:key];
  244. NSDate *resDate = [response reservationDate];
  245. BOOL isResponseReserved = (resDate != nil)
  246. && ([resDate timeIntervalSinceNow] > -reservationInterval_);
  247. if (!isResponseReserved) {
  248. // We can remove this response from the cache
  249. NSUInteger storedSize = [[response data] length];
  250. totalDataSize_ -= storedSize;
  251. [responses_ removeObjectForKey:key];
  252. }
  253. // If we've removed enough response data, then we're done
  254. if (memoryCapacity_ >= totalDataSize_) break;
  255. }
  256. }
  257. - (void)storeCachedResponse:(GTMCachedURLResponse *)cachedResponse
  258. forRequest:(NSURLRequest *)request {
  259. @synchronized(self) {
  260. // Remove any previous entry for this request
  261. [self removeCachedResponseForRequest:request];
  262. // cache this one only if it's not bigger than our cache
  263. NSUInteger storedSize = [[cachedResponse data] length];
  264. if (storedSize < memoryCapacity_) {
  265. NSURL *key = [request URL];
  266. [responses_ setObject:cachedResponse forKey:key];
  267. totalDataSize_ += storedSize;
  268. [self pruneCacheResponses];
  269. }
  270. }
  271. }
  272. - (GTMCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
  273. GTMCachedURLResponse *response;
  274. @synchronized(self) {
  275. NSURL *key = [request URL];
  276. response = [[[responses_ objectForKey:key] retain] autorelease];
  277. // Touch the date to indicate this was recently retrieved
  278. [response setUseDate:[NSDate date]];
  279. }
  280. return response;
  281. }
  282. - (void)removeCachedResponseForRequest:(NSURLRequest *)request {
  283. @synchronized(self) {
  284. NSURL *key = [request URL];
  285. totalDataSize_ -= [[[responses_ objectForKey:key] data] length];
  286. [responses_ removeObjectForKey:key];
  287. }
  288. }
  289. - (void)removeAllCachedResponses {
  290. @synchronized(self) {
  291. [responses_ removeAllObjects];
  292. totalDataSize_ = 0;
  293. }
  294. }
  295. - (NSUInteger)memoryCapacity {
  296. return memoryCapacity_;
  297. }
  298. - (void)setMemoryCapacity:(NSUInteger)totalBytes {
  299. @synchronized(self) {
  300. BOOL didShrink = (totalBytes < memoryCapacity_);
  301. memoryCapacity_ = totalBytes;
  302. if (didShrink) {
  303. [self pruneCacheResponses];
  304. }
  305. }
  306. }
  307. // Methods for unit testing.
  308. - (void)setReservationInterval:(NSTimeInterval)secs {
  309. reservationInterval_ = secs;
  310. }
  311. - (NSDictionary *)responses {
  312. return responses_;
  313. }
  314. - (NSUInteger)totalDataSize {
  315. return totalDataSize_;
  316. }
  317. @end
  318. //
  319. // GTMHTTPFetchHistory
  320. //
  321. @interface GTMHTTPFetchHistory ()
  322. - (NSString *)cachedETagForRequest:(NSURLRequest *)request;
  323. - (void)removeCachedDataForRequest:(NSURLRequest *)request;
  324. @end
  325. @implementation GTMHTTPFetchHistory
  326. @synthesize cookieStorage = cookieStorage_;
  327. @dynamic shouldRememberETags;
  328. @dynamic shouldCacheETaggedData;
  329. @dynamic memoryCapacity;
  330. - (id)init {
  331. return [self initWithMemoryCapacity:kGTMDefaultETaggedDataCacheMemoryCapacity
  332. shouldCacheETaggedData:NO];
  333. }
  334. - (id)initWithMemoryCapacity:(NSUInteger)totalBytes
  335. shouldCacheETaggedData:(BOOL)shouldCacheETaggedData {
  336. self = [super init];
  337. if (self != nil) {
  338. etaggedDataCache_ = [[GTMURLCache alloc] initWithMemoryCapacity:totalBytes];
  339. shouldRememberETags_ = shouldCacheETaggedData;
  340. shouldCacheETaggedData_ = shouldCacheETaggedData;
  341. cookieStorage_ = [[GTMCookieStorage alloc] init];
  342. }
  343. return self;
  344. }
  345. - (void)dealloc {
  346. [etaggedDataCache_ release];
  347. [cookieStorage_ release];
  348. [super dealloc];
  349. }
  350. - (void)updateRequest:(NSMutableURLRequest *)request isHTTPGet:(BOOL)isHTTPGet {
  351. @synchronized(self) {
  352. if ([self shouldRememberETags]) {
  353. // If this URL is in the history, and no ETag has been set, then
  354. // set the ETag header field
  355. // If we have a history, we're tracking across fetches, so we don't
  356. // want to pull results from any other cache
  357. [request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
  358. if (isHTTPGet) {
  359. // We'll only add an ETag if there's no ETag specified in the user's
  360. // request
  361. NSString *specifiedETag = [request valueForHTTPHeaderField:kGTMIfNoneMatchHeader];
  362. if (specifiedETag == nil) {
  363. // No ETag: extract the previous ETag for this request from the
  364. // fetch history, and add it to the request
  365. NSString *cachedETag = [self cachedETagForRequest:request];
  366. if (cachedETag != nil) {
  367. [request addValue:cachedETag forHTTPHeaderField:kGTMIfNoneMatchHeader];
  368. }
  369. } else {
  370. // Has an ETag: remove any stored response in the fetch history
  371. // for this request, as the If-None-Match header could lead to
  372. // a 304 Not Modified, and we want that error delivered to the
  373. // user since they explicitly specified the ETag
  374. [self removeCachedDataForRequest:request];
  375. }
  376. }
  377. }
  378. }
  379. }
  380. - (void)updateFetchHistoryWithRequest:(NSURLRequest *)request
  381. response:(NSURLResponse *)response
  382. downloadedData:(NSData *)downloadedData {
  383. @synchronized(self) {
  384. if (![self shouldRememberETags]) return;
  385. if (![response respondsToSelector:@selector(allHeaderFields)]) return;
  386. NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
  387. if (statusCode != kGTMHTTPFetcherStatusNotModified) {
  388. // Save this ETag string for successful results (<300)
  389. // If there's no last modified string, clear the dictionary
  390. // entry for this URL. Also cache or delete the data, if appropriate
  391. // (when etaggedDataCache is non-nil.)
  392. NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
  393. NSString* etag = [headers objectForKey:kGTMETagHeader];
  394. if (etag != nil && statusCode < 300) {
  395. // we want to cache responses for the headers, even if the client
  396. // doesn't want the response body data caches
  397. NSData *dataToStore = shouldCacheETaggedData_ ? downloadedData : nil;
  398. GTMCachedURLResponse *cachedResponse;
  399. cachedResponse = [[[GTMCachedURLResponse alloc] initWithResponse:response
  400. data:dataToStore] autorelease];
  401. [etaggedDataCache_ storeCachedResponse:cachedResponse
  402. forRequest:request];
  403. } else {
  404. [etaggedDataCache_ removeCachedResponseForRequest:request];
  405. }
  406. }
  407. }
  408. }
  409. - (NSString *)cachedETagForRequest:(NSURLRequest *)request {
  410. // Internal routine.
  411. GTMCachedURLResponse *cachedResponse;
  412. cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
  413. NSURLResponse *response = [cachedResponse response];
  414. NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
  415. NSString *cachedETag = [headers objectForKey:kGTMETagHeader];
  416. if (cachedETag) {
  417. // Since the request having an ETag implies this request is about
  418. // to be fetched again, reserve the cached response to ensure that
  419. // that it will be around at least until the fetch completes.
  420. //
  421. // When the fetch completes, either the cached response will be replaced
  422. // with a new response, or the cachedDataForRequest: method below will
  423. // clear the reservation.
  424. [cachedResponse setReservationDate:[NSDate date]];
  425. }
  426. return cachedETag;
  427. }
  428. - (NSData *)cachedDataForRequest:(NSURLRequest *)request {
  429. @synchronized(self) {
  430. GTMCachedURLResponse *cachedResponse;
  431. cachedResponse = [etaggedDataCache_ cachedResponseForRequest:request];
  432. NSData *cachedData = [cachedResponse data];
  433. // Since the data for this cached request is being obtained from the cache,
  434. // we can clear the reservation as the fetch has completed.
  435. [cachedResponse setReservationDate:nil];
  436. return cachedData;
  437. }
  438. }
  439. - (void)removeCachedDataForRequest:(NSURLRequest *)request {
  440. @synchronized(self) {
  441. [etaggedDataCache_ removeCachedResponseForRequest:request];
  442. }
  443. }
  444. - (void)clearETaggedDataCache {
  445. @synchronized(self) {
  446. [etaggedDataCache_ removeAllCachedResponses];
  447. }
  448. }
  449. - (void)clearHistory {
  450. @synchronized(self) {
  451. [self clearETaggedDataCache];
  452. [cookieStorage_ removeAllCookies];
  453. }
  454. }
  455. - (void)removeAllCookies {
  456. @synchronized(self) {
  457. [cookieStorage_ removeAllCookies];
  458. }
  459. }
  460. - (BOOL)shouldRememberETags {
  461. return shouldRememberETags_;
  462. }
  463. - (void)setShouldRememberETags:(BOOL)flag {
  464. BOOL wasRemembering = shouldRememberETags_;
  465. shouldRememberETags_ = flag;
  466. if (wasRemembering && !flag) {
  467. // Free up the cache memory
  468. [self clearETaggedDataCache];
  469. }
  470. }
  471. - (BOOL)shouldCacheETaggedData {
  472. return shouldCacheETaggedData_;
  473. }
  474. - (void)setShouldCacheETaggedData:(BOOL)flag {
  475. BOOL wasCaching = shouldCacheETaggedData_;
  476. shouldCacheETaggedData_ = flag;
  477. if (flag) {
  478. self.shouldRememberETags = YES;
  479. }
  480. if (wasCaching && !flag) {
  481. // users expect turning off caching to free up the cache memory
  482. [self clearETaggedDataCache];
  483. }
  484. }
  485. - (NSUInteger)memoryCapacity {
  486. return [etaggedDataCache_ memoryCapacity];
  487. }
  488. - (void)setMemoryCapacity:(NSUInteger)totalBytes {
  489. [etaggedDataCache_ setMemoryCapacity:totalBytes];
  490. }
  491. @end