1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "content/browser/appcache/mock_appcache_storage.h" 6 7 #include "base/bind.h" 8 #include "base/logging.h" 9 #include "base/memory/ref_counted.h" 10 #include "base/message_loop/message_loop.h" 11 #include "base/stl_util.h" 12 #include "content/browser/appcache/appcache.h" 13 #include "content/browser/appcache/appcache_entry.h" 14 #include "content/browser/appcache/appcache_group.h" 15 #include "content/browser/appcache/appcache_response.h" 16 #include "content/browser/appcache/appcache_service_impl.h" 17 18 // This is a quick and easy 'mock' implementation of the storage interface 19 // that doesn't put anything to disk. 20 // 21 // We simply add an extra reference to objects when they're put in storage, 22 // and remove the extra reference when they are removed from storage. 23 // Responses are never really removed from the in-memory disk cache. 24 // Delegate callbacks are made asyncly to appropiately mimic what will 25 // happen with a real disk-backed storage impl that involves IO on a 26 // background thread. 27 28 namespace content { 29 30 MockAppCacheStorage::MockAppCacheStorage(AppCacheServiceImpl* service) 31 : AppCacheStorage(service), 32 simulate_make_group_obsolete_failure_(false), 33 simulate_store_group_and_newest_cache_failure_(false), 34 simulate_find_main_resource_(false), 35 simulate_find_sub_resource_(false), 36 simulated_found_cache_id_(kAppCacheNoCacheId), 37 simulated_found_group_id_(0), 38 simulated_found_network_namespace_(false), 39 weak_factory_(this) { 40 last_cache_id_ = 0; 41 last_group_id_ = 0; 42 last_response_id_ = 0; 43 } 44 45 MockAppCacheStorage::~MockAppCacheStorage() { 46 } 47 48 void MockAppCacheStorage::GetAllInfo(Delegate* delegate) { 49 ScheduleTask( 50 base::Bind(&MockAppCacheStorage::ProcessGetAllInfo, 51 weak_factory_.GetWeakPtr(), 52 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 53 } 54 55 void MockAppCacheStorage::LoadCache(int64 id, Delegate* delegate) { 56 DCHECK(delegate); 57 AppCache* cache = working_set_.GetCache(id); 58 if (ShouldCacheLoadAppearAsync(cache)) { 59 ScheduleTask( 60 base::Bind(&MockAppCacheStorage::ProcessLoadCache, 61 weak_factory_.GetWeakPtr(), id, 62 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 63 return; 64 } 65 ProcessLoadCache(id, GetOrCreateDelegateReference(delegate)); 66 } 67 68 void MockAppCacheStorage::LoadOrCreateGroup( 69 const GURL& manifest_url, Delegate* delegate) { 70 DCHECK(delegate); 71 AppCacheGroup* group = working_set_.GetGroup(manifest_url); 72 if (ShouldGroupLoadAppearAsync(group)) { 73 ScheduleTask( 74 base::Bind(&MockAppCacheStorage::ProcessLoadOrCreateGroup, 75 weak_factory_.GetWeakPtr(), manifest_url, 76 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 77 return; 78 } 79 ProcessLoadOrCreateGroup( 80 manifest_url, GetOrCreateDelegateReference(delegate)); 81 } 82 83 void MockAppCacheStorage::StoreGroupAndNewestCache( 84 AppCacheGroup* group, AppCache* newest_cache, Delegate* delegate) { 85 DCHECK(group && delegate && newest_cache); 86 87 // Always make this operation look async. 88 ScheduleTask( 89 base::Bind(&MockAppCacheStorage::ProcessStoreGroupAndNewestCache, 90 weak_factory_.GetWeakPtr(), make_scoped_refptr(group), 91 make_scoped_refptr(newest_cache), 92 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 93 } 94 95 void MockAppCacheStorage::FindResponseForMainRequest( 96 const GURL& url, const GURL& preferred_manifest_url, Delegate* delegate) { 97 DCHECK(delegate); 98 99 // Note: MockAppCacheStorage does not respect the preferred_manifest_url. 100 101 // Always make this operation look async. 102 ScheduleTask( 103 base::Bind(&MockAppCacheStorage::ProcessFindResponseForMainRequest, 104 weak_factory_.GetWeakPtr(), url, 105 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 106 } 107 108 void MockAppCacheStorage::FindResponseForSubRequest( 109 AppCache* cache, const GURL& url, 110 AppCacheEntry* found_entry, AppCacheEntry* found_fallback_entry, 111 bool* found_network_namespace) { 112 DCHECK(cache && cache->is_complete()); 113 114 // This layer of indirection is here to facilitate testing. 115 if (simulate_find_sub_resource_) { 116 *found_entry = simulated_found_entry_; 117 *found_fallback_entry = simulated_found_fallback_entry_; 118 *found_network_namespace = simulated_found_network_namespace_; 119 simulate_find_sub_resource_ = false; 120 return; 121 } 122 123 GURL fallback_namespace_not_used; 124 GURL intercept_namespace_not_used; 125 cache->FindResponseForRequest( 126 url, found_entry, &intercept_namespace_not_used, 127 found_fallback_entry, &fallback_namespace_not_used, 128 found_network_namespace); 129 } 130 131 void MockAppCacheStorage::MarkEntryAsForeign( 132 const GURL& entry_url, int64 cache_id) { 133 AppCache* cache = working_set_.GetCache(cache_id); 134 if (cache) { 135 AppCacheEntry* entry = cache->GetEntry(entry_url); 136 DCHECK(entry); 137 if (entry) 138 entry->add_types(AppCacheEntry::FOREIGN); 139 } 140 } 141 142 void MockAppCacheStorage::MakeGroupObsolete(AppCacheGroup* group, 143 Delegate* delegate, 144 int response_code) { 145 DCHECK(group && delegate); 146 147 // Always make this method look async. 148 ScheduleTask( 149 base::Bind(&MockAppCacheStorage::ProcessMakeGroupObsolete, 150 weak_factory_.GetWeakPtr(), 151 make_scoped_refptr(group), 152 make_scoped_refptr(GetOrCreateDelegateReference(delegate)), 153 response_code)); 154 } 155 156 AppCacheResponseReader* MockAppCacheStorage::CreateResponseReader( 157 const GURL& manifest_url, int64 group_id, int64 response_id) { 158 if (simulated_reader_) 159 return simulated_reader_.release(); 160 return new AppCacheResponseReader(response_id, group_id, disk_cache()); 161 } 162 163 AppCacheResponseWriter* MockAppCacheStorage::CreateResponseWriter( 164 const GURL& manifest_url, int64 group_id) { 165 return new AppCacheResponseWriter(NewResponseId(), group_id, disk_cache()); 166 } 167 168 void MockAppCacheStorage::DoomResponses( 169 const GURL& manifest_url, const std::vector<int64>& response_ids) { 170 DeleteResponses(manifest_url, response_ids); 171 } 172 173 void MockAppCacheStorage::DeleteResponses( 174 const GURL& manifest_url, const std::vector<int64>& response_ids) { 175 // We don't bother with actually removing responses from the disk-cache, 176 // just keep track of which ids have been doomed or deleted 177 std::vector<int64>::const_iterator it = response_ids.begin(); 178 while (it != response_ids.end()) { 179 doomed_response_ids_.insert(*it); 180 ++it; 181 } 182 } 183 184 void MockAppCacheStorage::ProcessGetAllInfo( 185 scoped_refptr<DelegateReference> delegate_ref) { 186 if (delegate_ref->delegate) 187 delegate_ref->delegate->OnAllInfo(simulated_appcache_info_.get()); 188 } 189 190 void MockAppCacheStorage::ProcessLoadCache( 191 int64 id, scoped_refptr<DelegateReference> delegate_ref) { 192 AppCache* cache = working_set_.GetCache(id); 193 if (delegate_ref->delegate) 194 delegate_ref->delegate->OnCacheLoaded(cache, id); 195 } 196 197 void MockAppCacheStorage::ProcessLoadOrCreateGroup( 198 const GURL& manifest_url, scoped_refptr<DelegateReference> delegate_ref) { 199 scoped_refptr<AppCacheGroup> group(working_set_.GetGroup(manifest_url)); 200 201 // Newly created groups are not put in the stored_groups collection 202 // until StoreGroupAndNewestCache is called. 203 if (!group.get()) 204 group = new AppCacheGroup(service_->storage(), manifest_url, NewGroupId()); 205 206 if (delegate_ref->delegate) 207 delegate_ref->delegate->OnGroupLoaded(group.get(), manifest_url); 208 } 209 210 void MockAppCacheStorage::ProcessStoreGroupAndNewestCache( 211 scoped_refptr<AppCacheGroup> group, 212 scoped_refptr<AppCache> newest_cache, 213 scoped_refptr<DelegateReference> delegate_ref) { 214 Delegate* delegate = delegate_ref->delegate; 215 if (simulate_store_group_and_newest_cache_failure_) { 216 if (delegate) 217 delegate->OnGroupAndNewestCacheStored( 218 group.get(), newest_cache.get(), false, false); 219 return; 220 } 221 222 AddStoredGroup(group.get()); 223 if (newest_cache.get() != group->newest_complete_cache()) { 224 newest_cache->set_complete(true); 225 group->AddCache(newest_cache.get()); 226 AddStoredCache(newest_cache.get()); 227 228 // Copy the collection prior to removal, on final release 229 // of a cache the group's collection will change. 230 AppCacheGroup::Caches copy = group->old_caches(); 231 RemoveStoredCaches(copy); 232 } 233 234 if (delegate) 235 delegate->OnGroupAndNewestCacheStored( 236 group.get(), newest_cache.get(), true, false); 237 } 238 239 namespace { 240 241 struct FoundCandidate { 242 GURL namespace_entry_url; 243 AppCacheEntry entry; 244 int64 cache_id; 245 int64 group_id; 246 GURL manifest_url; 247 bool is_cache_in_use; 248 249 FoundCandidate() 250 : cache_id(kAppCacheNoCacheId), group_id(0), is_cache_in_use(false) {} 251 }; 252 253 void MaybeTakeNewNamespaceEntry( 254 AppCacheNamespaceType namespace_type, 255 const AppCacheEntry &entry, 256 const GURL& namespace_url, 257 bool cache_is_in_use, 258 FoundCandidate* best_candidate, 259 GURL* best_candidate_namespace, 260 AppCache* cache, 261 AppCacheGroup* group) { 262 DCHECK(entry.has_response_id()); 263 264 bool take_new_entry = true; 265 266 // Does the new candidate entry trump our current best candidate? 267 if (best_candidate->entry.has_response_id()) { 268 // Longer namespace prefix matches win. 269 size_t candidate_length = 270 namespace_url.spec().length(); 271 size_t best_length = 272 best_candidate_namespace->spec().length(); 273 274 if (candidate_length > best_length) { 275 take_new_entry = true; 276 } else if (candidate_length == best_length && 277 cache_is_in_use && !best_candidate->is_cache_in_use) { 278 take_new_entry = true; 279 } else { 280 take_new_entry = false; 281 } 282 } 283 284 if (take_new_entry) { 285 if (namespace_type == APPCACHE_FALLBACK_NAMESPACE) { 286 best_candidate->namespace_entry_url = 287 cache->GetFallbackEntryUrl(namespace_url); 288 } else { 289 best_candidate->namespace_entry_url = 290 cache->GetInterceptEntryUrl(namespace_url); 291 } 292 best_candidate->entry = entry; 293 best_candidate->cache_id = cache->cache_id(); 294 best_candidate->group_id = group->group_id(); 295 best_candidate->manifest_url = group->manifest_url(); 296 best_candidate->is_cache_in_use = cache_is_in_use; 297 *best_candidate_namespace = namespace_url; 298 } 299 } 300 } // namespace 301 302 void MockAppCacheStorage::ProcessFindResponseForMainRequest( 303 const GURL& url, scoped_refptr<DelegateReference> delegate_ref) { 304 if (simulate_find_main_resource_) { 305 simulate_find_main_resource_ = false; 306 if (delegate_ref->delegate) { 307 delegate_ref->delegate->OnMainResponseFound( 308 url, simulated_found_entry_, 309 simulated_found_fallback_url_, simulated_found_fallback_entry_, 310 simulated_found_cache_id_, simulated_found_group_id_, 311 simulated_found_manifest_url_); 312 } 313 return; 314 } 315 316 // This call has no persistent side effects, if the delegate has gone 317 // away, we can just bail out early. 318 if (!delegate_ref->delegate) 319 return; 320 321 // TODO(michaeln): The heuristics around choosing amoungst 322 // multiple candidates is under specified, and just plain 323 // not fully understood. Refine these over time. In particular, 324 // * prefer candidates from newer caches 325 // * take into account the cache associated with the document 326 // that initiated the navigation 327 // * take into account the cache associated with the document 328 // currently residing in the frame being navigated 329 FoundCandidate found_candidate; 330 GURL found_intercept_candidate_namespace; 331 FoundCandidate found_fallback_candidate; 332 GURL found_fallback_candidate_namespace; 333 334 for (StoredGroupMap::const_iterator it = stored_groups_.begin(); 335 it != stored_groups_.end(); ++it) { 336 AppCacheGroup* group = it->second.get(); 337 AppCache* cache = group->newest_complete_cache(); 338 if (group->is_obsolete() || !cache || 339 (url.GetOrigin() != group->manifest_url().GetOrigin())) { 340 continue; 341 } 342 343 AppCacheEntry found_entry; 344 AppCacheEntry found_fallback_entry; 345 GURL found_intercept_namespace; 346 GURL found_fallback_namespace; 347 bool ignore_found_network_namespace = false; 348 bool found = cache->FindResponseForRequest( 349 url, &found_entry, &found_intercept_namespace, 350 &found_fallback_entry, &found_fallback_namespace, 351 &ignore_found_network_namespace); 352 353 // 6.11.1 Navigating across documents, Step 10. 354 // Network namespacing doesn't apply to main resource loads, 355 // and foreign entries are excluded. 356 if (!found || ignore_found_network_namespace || 357 (found_entry.has_response_id() && found_entry.IsForeign()) || 358 (found_fallback_entry.has_response_id() && 359 found_fallback_entry.IsForeign())) { 360 continue; 361 } 362 363 // We have a bias for hits from caches that are in use. 364 bool is_in_use = IsCacheStored(cache) && !cache->HasOneRef(); 365 366 if (found_entry.has_response_id() && 367 found_intercept_namespace.is_empty()) { 368 found_candidate.namespace_entry_url = GURL(); 369 found_candidate.entry = found_entry; 370 found_candidate.cache_id = cache->cache_id(); 371 found_candidate.group_id = group->group_id(); 372 found_candidate.manifest_url = group->manifest_url(); 373 found_candidate.is_cache_in_use = is_in_use; 374 if (is_in_use) 375 break; // We break out of the loop with this direct hit. 376 } else if (found_entry.has_response_id() && 377 !found_intercept_namespace.is_empty()) { 378 MaybeTakeNewNamespaceEntry( 379 APPCACHE_INTERCEPT_NAMESPACE, 380 found_entry, found_intercept_namespace, is_in_use, 381 &found_candidate, &found_intercept_candidate_namespace, 382 cache, group); 383 } else { 384 DCHECK(found_fallback_entry.has_response_id()); 385 MaybeTakeNewNamespaceEntry( 386 APPCACHE_FALLBACK_NAMESPACE, 387 found_fallback_entry, found_fallback_namespace, is_in_use, 388 &found_fallback_candidate, &found_fallback_candidate_namespace, 389 cache, group); 390 } 391 } 392 393 // Found a direct hit or an intercept namespace hit. 394 if (found_candidate.entry.has_response_id()) { 395 delegate_ref->delegate->OnMainResponseFound( 396 url, found_candidate.entry, found_candidate.namespace_entry_url, 397 AppCacheEntry(), found_candidate.cache_id, found_candidate.group_id, 398 found_candidate.manifest_url); 399 return; 400 } 401 402 // Found a fallback namespace. 403 if (found_fallback_candidate.entry.has_response_id()) { 404 delegate_ref->delegate->OnMainResponseFound( 405 url, AppCacheEntry(), 406 found_fallback_candidate.namespace_entry_url, 407 found_fallback_candidate.entry, 408 found_fallback_candidate.cache_id, 409 found_fallback_candidate.group_id, 410 found_fallback_candidate.manifest_url); 411 return; 412 } 413 414 // Didn't find anything. 415 delegate_ref->delegate->OnMainResponseFound( 416 url, AppCacheEntry(), GURL(), AppCacheEntry(), kAppCacheNoCacheId, 0, 417 GURL()); 418 } 419 420 void MockAppCacheStorage::ProcessMakeGroupObsolete( 421 scoped_refptr<AppCacheGroup> group, 422 scoped_refptr<DelegateReference> delegate_ref, 423 int response_code) { 424 if (simulate_make_group_obsolete_failure_) { 425 if (delegate_ref->delegate) 426 delegate_ref->delegate->OnGroupMadeObsolete( 427 group.get(), false, response_code); 428 return; 429 } 430 431 RemoveStoredGroup(group.get()); 432 if (group->newest_complete_cache()) 433 RemoveStoredCache(group->newest_complete_cache()); 434 435 // Copy the collection prior to removal, on final release 436 // of a cache the group's collection will change. 437 AppCacheGroup::Caches copy = group->old_caches(); 438 RemoveStoredCaches(copy); 439 440 group->set_obsolete(true); 441 442 // Also remove from the working set, caches for an 'obsolete' group 443 // may linger in use, but the group itself cannot be looked up by 444 // 'manifest_url' in the working set any longer. 445 working_set()->RemoveGroup(group.get()); 446 447 if (delegate_ref->delegate) 448 delegate_ref->delegate->OnGroupMadeObsolete( 449 group.get(), true, response_code); 450 } 451 452 void MockAppCacheStorage::ScheduleTask(const base::Closure& task) { 453 pending_tasks_.push_back(task); 454 base::MessageLoop::current()->PostTask( 455 FROM_HERE, 456 base::Bind(&MockAppCacheStorage::RunOnePendingTask, 457 weak_factory_.GetWeakPtr())); 458 } 459 460 void MockAppCacheStorage::RunOnePendingTask() { 461 DCHECK(!pending_tasks_.empty()); 462 base::Closure task = pending_tasks_.front(); 463 pending_tasks_.pop_front(); 464 task.Run(); 465 } 466 467 void MockAppCacheStorage::AddStoredCache(AppCache* cache) { 468 int64 cache_id = cache->cache_id(); 469 if (stored_caches_.find(cache_id) == stored_caches_.end()) { 470 stored_caches_.insert( 471 StoredCacheMap::value_type(cache_id, make_scoped_refptr(cache))); 472 } 473 } 474 475 void MockAppCacheStorage::RemoveStoredCache(AppCache* cache) { 476 // Do not remove from the working set, active caches are still usable 477 // and may be looked up by id until they fall out of use. 478 stored_caches_.erase(cache->cache_id()); 479 } 480 481 void MockAppCacheStorage::RemoveStoredCaches( 482 const AppCacheGroup::Caches& caches) { 483 AppCacheGroup::Caches::const_iterator it = caches.begin(); 484 while (it != caches.end()) { 485 RemoveStoredCache(*it); 486 ++it; 487 } 488 } 489 490 void MockAppCacheStorage::AddStoredGroup(AppCacheGroup* group) { 491 const GURL& url = group->manifest_url(); 492 if (stored_groups_.find(url) == stored_groups_.end()) { 493 stored_groups_.insert( 494 StoredGroupMap::value_type(url, make_scoped_refptr(group))); 495 } 496 } 497 498 void MockAppCacheStorage::RemoveStoredGroup(AppCacheGroup* group) { 499 stored_groups_.erase(group->manifest_url()); 500 } 501 502 bool MockAppCacheStorage::ShouldGroupLoadAppearAsync( 503 const AppCacheGroup* group) { 504 // We'll have to query the database to see if a group for the 505 // manifest_url exists on disk. So return true for async. 506 if (!group) 507 return true; 508 509 // Groups without a newest cache can't have been put to disk yet, so 510 // we can synchronously return a reference we have in the working set. 511 if (!group->newest_complete_cache()) 512 return false; 513 514 // The LoadGroup interface implies also loading the newest cache, so 515 // if loading the newest cache should appear async, so too must the 516 // loading of this group. 517 if (!ShouldCacheLoadAppearAsync(group->newest_complete_cache())) 518 return false; 519 520 521 // If any of the old caches are "in use", then the group must also 522 // be memory resident and not require async loading. 523 const AppCacheGroup::Caches& old_caches = group->old_caches(); 524 AppCacheGroup::Caches::const_iterator it = old_caches.begin(); 525 while (it != old_caches.end()) { 526 // "in use" caches don't require async loading 527 if (!ShouldCacheLoadAppearAsync(*it)) 528 return false; 529 ++it; 530 } 531 532 return true; 533 } 534 535 bool MockAppCacheStorage::ShouldCacheLoadAppearAsync(const AppCache* cache) { 536 if (!cache) 537 return true; 538 539 // If the 'stored' ref is the only ref, real storage will have to load from 540 // the database. 541 return IsCacheStored(cache) && cache->HasOneRef(); 542 } 543 544 } // namespace content 545