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