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