blob: 2b945dea40fc23536b5d7a92c92dc133fcad4fa0 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/dom_storage/dom_storage_context_impl.h"
#include <inttypes.h>
#include <stddef.h>
#include <stdlib.h>
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/guid.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/sys_info.h"
#include "base/time/time.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/process_memory_dump.h"
#include "content/browser/dom_storage/dom_storage_area.h"
#include "content/browser/dom_storage/dom_storage_database.h"
#include "content/browser/dom_storage/dom_storage_namespace.h"
#include "content/browser/dom_storage/dom_storage_task_runner.h"
#include "content/browser/dom_storage/session_storage_database.h"
#include "content/common/dom_storage/dom_storage_types.h"
#include "content/public/browser/dom_storage_context.h"
#include "content/public/browser/local_storage_usage_info.h"
#include "content/public/browser/session_storage_usage_info.h"
#include "storage/browser/quota/special_storage_policy.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
// Limits on the cache size and number of areas in memory, over which the areas
// are purged.
#if defined(OS_ANDROID)
const unsigned kMaxStorageAreaCount = 20;
const size_t kMaxCacheSize = 2 * 1024 * 1024;
#else
const unsigned kMaxStorageAreaCount = 100;
const size_t kMaxCacheSize = 20 * 1024 * 1024;
#endif
const int kSessionStoraceScavengingSeconds = 60;
// Offset the session storage namespace ids generated by different contexts
// to help identify when an id from one is mistakenly used in another.
int g_session_id_offset_sequence = 1;
const int kSessionIdOffsetAmount = 10000;
// Aggregates statistics from all the namespaces.
DOMStorageNamespace::UsageStatistics GetTotalNamespaceStatistics(
const DOMStorageContextImpl::StorageNamespaceMap& namespace_map) {
DOMStorageNamespace::UsageStatistics total_stats = {0};
for (const auto& it : namespace_map) {
DOMStorageNamespace::UsageStatistics stats =
it.second->GetUsageStatistics();
total_stats.total_cache_size += stats.total_cache_size;
total_stats.total_area_count += stats.total_area_count;
total_stats.inactive_area_count += stats.inactive_area_count;
}
return total_stats;
}
} // namespace
DOMStorageContextImpl::DOMStorageContextImpl(
const base::FilePath& localstorage_directory,
const base::FilePath& sessionstorage_directory,
storage::SpecialStoragePolicy* special_storage_policy,
scoped_refptr<DOMStorageTaskRunner> task_runner)
: localstorage_directory_(localstorage_directory),
sessionstorage_directory_(sessionstorage_directory),
task_runner_(std::move(task_runner)),
session_id_offset_(abs((g_session_id_offset_sequence++ % 10)) *
kSessionIdOffsetAmount),
session_id_sequence_(session_id_offset_),
is_shutdown_(false),
force_keep_session_state_(false),
special_storage_policy_(special_storage_policy),
scavenging_started_(false),
is_low_end_device_(base::SysInfo::IsLowEndDevice()) {
// Tests may run without task runners.
if (task_runner_) {
// Registering dump provider is safe even outside the task runner.
base::trace_event::MemoryDumpManager::GetInstance()
->RegisterDumpProviderWithSequencedTaskRunner(
this, "DOMStorage", task_runner_->GetSequencedTaskRunner(
DOMStorageTaskRunner::PRIMARY_SEQUENCE),
base::trace_event::MemoryDumpProvider::Options());
}
}
DOMStorageContextImpl::~DOMStorageContextImpl() {
DCHECK(is_shutdown_);
if (session_storage_database_.get()) {
// SessionStorageDatabase shouldn't be deleted right away: deleting it will
// potentially involve waiting in leveldb::DBImpl::~DBImpl, and waiting
// shouldn't happen on this thread.
SessionStorageDatabase* to_release = session_storage_database_.get();
to_release->AddRef();
session_storage_database_ = NULL;
task_runner_->PostShutdownBlockingTask(
FROM_HERE,
DOMStorageTaskRunner::COMMIT_SEQUENCE,
base::Bind(&SessionStorageDatabase::Release,
base::Unretained(to_release)));
}
}
DOMStorageNamespace* DOMStorageContextImpl::GetStorageNamespace(
int64_t namespace_id) {
if (is_shutdown_)
return NULL;
StorageNamespaceMap::iterator found = namespaces_.find(namespace_id);
if (found == namespaces_.end()) {
if (namespace_id == kLocalStorageNamespaceId) {
if (!localstorage_directory_.empty()) {
if (!base::CreateDirectory(localstorage_directory_)) {
LOG(ERROR) << "Failed to create 'Local Storage' directory,"
" falling back to in-memory only.";
localstorage_directory_ = base::FilePath();
}
}
DOMStorageNamespace* local =
new DOMStorageNamespace(localstorage_directory_, task_runner_.get());
namespaces_[kLocalStorageNamespaceId] = local;
return local;
}
return NULL;
}
return found->second.get();
}
void DOMStorageContextImpl::GetLocalStorageUsage(
std::vector<LocalStorageUsageInfo>* infos,
bool include_file_info) {
if (localstorage_directory_.empty()) {
DOMStorageNamespace* local = GetStorageNamespace(kLocalStorageNamespaceId);
std::vector<GURL> origins;
local->GetOriginsWithAreas(&origins);
for (const GURL& origin : origins) {
LocalStorageUsageInfo info;
info.origin = origin;
infos->push_back(info);
}
return;
}
base::FileEnumerator enumerator(localstorage_directory_, false,
base::FileEnumerator::FILES);
for (base::FilePath path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
if (path.MatchesExtension(DOMStorageArea::kDatabaseFileExtension)) {
LocalStorageUsageInfo info;
info.origin = DOMStorageArea::OriginFromDatabaseFileName(path);
if (include_file_info) {
base::FileEnumerator::FileInfo find_info = enumerator.GetInfo();
info.data_size = find_info.GetSize();
info.last_modified = find_info.GetLastModifiedTime();
}
infos->push_back(info);
}
}
}
void DOMStorageContextImpl::GetSessionStorageUsage(
std::vector<SessionStorageUsageInfo>* infos) {
if (!session_storage_database_.get()) {
for (const auto& entry : namespaces_) {
std::vector<GURL> origins;
entry.second->GetOriginsWithAreas(&origins);
for (const GURL& origin : origins) {
SessionStorageUsageInfo info;
info.persistent_namespace_id = entry.second->persistent_namespace_id();
info.origin = origin;
infos->push_back(info);
}
}
return;
}
std::map<std::string, std::vector<GURL> > namespaces_and_origins;
session_storage_database_->ReadNamespacesAndOrigins(
&namespaces_and_origins);
for (std::map<std::string, std::vector<GURL> >::const_iterator it =
namespaces_and_origins.begin();
it != namespaces_and_origins.end(); ++it) {
for (std::vector<GURL>::const_iterator origin_it = it->second.begin();
origin_it != it->second.end(); ++origin_it) {
SessionStorageUsageInfo info;
info.persistent_namespace_id = it->first;
info.origin = *origin_it;
infos->push_back(info);
}
}
}
void DOMStorageContextImpl::DeleteLocalStorageForPhysicalOrigin(
const GURL& origin_url) {
DCHECK(!is_shutdown_);
url::Origin origin(origin_url);
DOMStorageNamespace* local = GetStorageNamespace(kLocalStorageNamespaceId);
std::vector<GURL> origins;
local->GetOriginsWithAreas(&origins);
// Suborigin at the physical origin of |origin_url| should have their storage
// deleted as well.
// https://w3c.github.io/webappsec-suborigins/
for (const auto& origin_candidate_url : origins) {
url::Origin origin_candidate(origin_candidate_url);
// |origin| is guaranteed to be deleted below, so don't delete it until
// then. That is, only suborigins at the same physical origin as |origin|
// should be deleted at this point.
if (!origin_candidate.IsSameOriginWith(origin) &&
origin_candidate.IsSamePhysicalOriginWith(origin)) {
DeleteLocalStorage(origin_candidate_url);
}
}
// It is important to always explicitly delete |origin|. If it does not appear
// it |origins| above, there still may be a directory open in the namespace in
// preparation for this storage, and this call will make sure that the
// directory is deleted.
DeleteLocalStorage(origin_url);
}
void DOMStorageContextImpl::DeleteLocalStorage(const GURL& origin_url) {
DOMStorageNamespace* local = GetStorageNamespace(kLocalStorageNamespaceId);
local->DeleteLocalStorageOrigin(origin_url);
// Synthesize a 'cleared' event if the area is open so CachedAreas in
// renderers get emptied out too.
DOMStorageArea* area = local->GetOpenStorageArea(origin_url);
if (area)
NotifyAreaCleared(area, origin_url);
}
void DOMStorageContextImpl::DeleteSessionStorage(
const SessionStorageUsageInfo& usage_info) {
DCHECK(!is_shutdown_);
DOMStorageNamespace* dom_storage_namespace = NULL;
std::map<std::string, int64_t>::const_iterator it =
persistent_namespace_id_to_namespace_id_.find(
usage_info.persistent_namespace_id);
if (it != persistent_namespace_id_to_namespace_id_.end()) {
dom_storage_namespace = GetStorageNamespace(it->second);
} else {
int64_t namespace_id = AllocateSessionId();
CreateSessionNamespace(namespace_id, usage_info.persistent_namespace_id);
dom_storage_namespace = GetStorageNamespace(namespace_id);
}
dom_storage_namespace->DeleteSessionStorageOrigin(usage_info.origin);
// Synthesize a 'cleared' event if the area is open so CachedAreas in
// renderers get emptied out too.
DOMStorageArea* area =
dom_storage_namespace->GetOpenStorageArea(usage_info.origin);
if (area)
NotifyAreaCleared(area, usage_info.origin);
}
void DOMStorageContextImpl::Flush() {
for (auto& entry : namespaces_)
entry.second->Flush();
}
void DOMStorageContextImpl::Shutdown() {
if (task_runner_)
task_runner_->AssertIsRunningOnPrimarySequence();
is_shutdown_ = true;
StorageNamespaceMap::const_iterator it = namespaces_.begin();
for (; it != namespaces_.end(); ++it)
it->second->Shutdown();
base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider(
this);
if (localstorage_directory_.empty() && !session_storage_database_.get())
return;
// Respect the content policy settings about what to
// keep and what to discard.
if (force_keep_session_state_)
return; // Keep everything.
bool has_session_only_origins =
special_storage_policy_.get() &&
special_storage_policy_->HasSessionOnlyOrigins();
if (has_session_only_origins) {
// We may have to delete something. We continue on the
// commit sequence after area shutdown tasks have cycled
// thru that sequence (and closed their database files).
bool success = task_runner_->PostShutdownBlockingTask(
FROM_HERE,
DOMStorageTaskRunner::COMMIT_SEQUENCE,
base::Bind(&DOMStorageContextImpl::ClearSessionOnlyOrigins, this));
DCHECK(success);
}
}
void DOMStorageContextImpl::AddEventObserver(EventObserver* observer) {
event_observers_.AddObserver(observer);
}
void DOMStorageContextImpl::RemoveEventObserver(EventObserver* observer) {
event_observers_.RemoveObserver(observer);
}
void DOMStorageContextImpl::NotifyItemSet(
const DOMStorageArea* area,
const base::string16& key,
const base::string16& new_value,
const base::NullableString16& old_value,
const GURL& page_url) {
for (auto& observer : event_observers_)
observer.OnDOMStorageItemSet(area, key, new_value, old_value, page_url);
}
void DOMStorageContextImpl::NotifyItemRemoved(
const DOMStorageArea* area,
const base::string16& key,
const base::string16& old_value,
const GURL& page_url) {
for (auto& observer : event_observers_)
observer.OnDOMStorageItemRemoved(area, key, old_value, page_url);
}
void DOMStorageContextImpl::NotifyAreaCleared(
const DOMStorageArea* area,
const GURL& page_url) {
for (auto& observer : event_observers_)
observer.OnDOMStorageAreaCleared(area, page_url);
}
int64_t DOMStorageContextImpl::AllocateSessionId() {
return base::subtle::Barrier_AtomicIncrement(&session_id_sequence_, 1);
}
std::string DOMStorageContextImpl::AllocatePersistentSessionId() {
std::string guid = base::GenerateGUID();
std::replace(guid.begin(), guid.end(), '-', '_');
return guid;
}
// Used to diagnose unknown namespace_ids given to the ipc message filter.
base::Optional<bad_message::BadMessageReason>
DOMStorageContextImpl::DiagnoseSessionNamespaceId(int64_t namespace_id) {
if (namespace_id < session_id_offset_ ||
namespace_id - session_id_offset_ > kSessionIdOffsetAmount) {
return bad_message::DSH_WRONG_STORAGE_PARTITION;
}
if (base::ContainsValue(recently_deleted_session_ids_, namespace_id))
return bad_message::DSH_DELETED_SESSION_ID;
int last_allocated_session_id =
base::subtle::Barrier_AtomicIncrement(&session_id_sequence_, 0);
if (namespace_id <= last_allocated_session_id)
return bad_message::DSH_NOT_CREATED_SESSION_ID;
return bad_message::DSH_NOT_ALLOCATED_SESSION_ID;
}
void DOMStorageContextImpl::CreateSessionNamespace(
int64_t namespace_id,
const std::string& persistent_namespace_id) {
if (is_shutdown_)
return;
DCHECK(namespace_id != kLocalStorageNamespaceId);
DCHECK(namespaces_.find(namespace_id) == namespaces_.end());
namespaces_[namespace_id] = new DOMStorageNamespace(
namespace_id, persistent_namespace_id, session_storage_database_.get(),
task_runner_.get());
persistent_namespace_id_to_namespace_id_[persistent_namespace_id] =
namespace_id;
}
void DOMStorageContextImpl::DeleteSessionNamespace(int64_t namespace_id,
bool should_persist_data) {
DCHECK_NE(kLocalStorageNamespaceId, namespace_id);
StorageNamespaceMap::const_iterator it = namespaces_.find(namespace_id);
if (it == namespaces_.end())
return;
std::string persistent_namespace_id = it->second->persistent_namespace_id();
if (session_storage_database_.get()) {
if (!should_persist_data) {
task_runner_->PostShutdownBlockingTask(
FROM_HERE,
DOMStorageTaskRunner::COMMIT_SEQUENCE,
base::Bind(
base::IgnoreResult(&SessionStorageDatabase::DeleteNamespace),
session_storage_database_,
persistent_namespace_id));
} else {
// Ensure that the data gets committed before we shut down.
it->second->Shutdown();
if (!scavenging_started_) {
// Protect the persistent namespace ID from scavenging.
protected_persistent_session_ids_.insert(persistent_namespace_id);
}
}
}
persistent_namespace_id_to_namespace_id_.erase(persistent_namespace_id);
namespaces_.erase(namespace_id);
recently_deleted_session_ids_.push_back(namespace_id);
if (recently_deleted_session_ids_.size() > 10)
recently_deleted_session_ids_.pop_front();
}
void DOMStorageContextImpl::CloneSessionNamespace(
int64_t existing_id,
int64_t new_id,
const std::string& new_persistent_id) {
if (is_shutdown_)
return;
DCHECK_NE(kLocalStorageNamespaceId, existing_id);
DCHECK_NE(kLocalStorageNamespaceId, new_id);
StorageNamespaceMap::iterator found = namespaces_.find(existing_id);
if (found != namespaces_.end()) {
namespaces_[new_id] = found->second->Clone(new_id, new_persistent_id);
return;
}
CreateSessionNamespace(new_id, new_persistent_id);
}
void DOMStorageContextImpl::ClearSessionOnlyOrigins() {
if (!localstorage_directory_.empty()) {
std::vector<LocalStorageUsageInfo> infos;
const bool kDontIncludeFileInfo = false;
GetLocalStorageUsage(&infos, kDontIncludeFileInfo);
for (size_t i = 0; i < infos.size(); ++i) {
const GURL& origin = infos[i].origin;
if (special_storage_policy_->IsStorageProtected(origin))
continue;
if (!special_storage_policy_->IsStorageSessionOnly(origin))
continue;
base::FilePath database_file_path = localstorage_directory_.Append(
DOMStorageArea::DatabaseFileNameFromOrigin(origin));
sql::Connection::Delete(database_file_path);
}
}
if (session_storage_database_.get()) {
std::vector<SessionStorageUsageInfo> infos;
GetSessionStorageUsage(&infos);
for (size_t i = 0; i < infos.size(); ++i) {
const GURL& origin = infos[i].origin;
if (special_storage_policy_->IsStorageProtected(origin))
continue;
if (!special_storage_policy_->IsStorageSessionOnly(origin))
continue;
session_storage_database_->DeleteArea(infos[i].persistent_namespace_id,
origin);
}
}
}
void DOMStorageContextImpl::SetSaveSessionStorageOnDisk() {
DCHECK(namespaces_.empty());
if (!sessionstorage_directory_.empty()) {
session_storage_database_ = new SessionStorageDatabase(
sessionstorage_directory_);
}
}
void DOMStorageContextImpl::StartScavengingUnusedSessionStorage() {
if (session_storage_database_.get()) {
task_runner_->PostDelayedTask(
FROM_HERE, base::Bind(&DOMStorageContextImpl::FindUnusedNamespaces,
this),
base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds));
}
}
void DOMStorageContextImpl::PurgeMemory(PurgeOption purge_option) {
if (is_shutdown_)
return;
DOMStorageNamespace::UsageStatistics initial_stats =
GetTotalNamespaceStatistics(namespaces_);
if (!initial_stats.total_area_count)
return;
// Track the total localStorage cache size.
UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageCacheSizeInKB",
initial_stats.total_cache_size / 1024, 1, 100000,
50);
const char* purge_reason = nullptr;
if (purge_option == PURGE_IF_NEEDED) {
// Purging is done based on the cache sizes without including the database
// size since it can be expensive trying to estimate the sqlite usage for
// all databases. For low end devices purge all inactive areas.
if (initial_stats.total_cache_size > kMaxCacheSize)
purge_reason = "SizeLimitExceeded";
else if (initial_stats.total_area_count > kMaxStorageAreaCount)
purge_reason = "AreaCountLimitExceeded";
else if (is_low_end_device_)
purge_reason = "InactiveOnLowEndDevice";
if (!purge_reason)
return;
purge_option = PURGE_UNOPENED;
} else {
if (purge_option == PURGE_AGGRESSIVE)
purge_reason = "AggressivePurgeTriggered";
else
purge_reason = "ModeratePurgeTriggered";
}
// Return if no areas can be purged with the given option.
bool aggressively = purge_option == PURGE_AGGRESSIVE;
if (!aggressively && !initial_stats.inactive_area_count) {
return;
}
for (const auto& it : namespaces_)
it.second->PurgeMemory(aggressively);
// Track the size of cache purged.
size_t purged_size_kib =
(initial_stats.total_cache_size -
GetTotalNamespaceStatistics(namespaces_).total_cache_size) /
1024;
std::string full_histogram_name =
std::string("LocalStorage.BrowserLocalStorageCachePurgedInKB.") +
purge_reason;
base::HistogramBase* histogram = base::Histogram::FactoryGet(
full_histogram_name, 1, 100000, 50,
base::HistogramBase::kUmaTargetedHistogramFlag);
if (histogram)
histogram->Add(purged_size_kib);
UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageCachePurgedInKB",
purged_size_kib, 1, 100000, 50);
}
bool DOMStorageContextImpl::OnMemoryDump(
const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) {
if (session_storage_database_)
session_storage_database_->OnMemoryDump(pmd);
if (args.level_of_detail ==
base::trace_event::MemoryDumpLevelOfDetail::BACKGROUND) {
DOMStorageNamespace::UsageStatistics total_stats =
GetTotalNamespaceStatistics(namespaces_);
auto* mad = pmd->CreateAllocatorDump(
base::StringPrintf("dom_storage/0x%" PRIXPTR "/cache_size",
reinterpret_cast<uintptr_t>(this)));
mad->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
base::trace_event::MemoryAllocatorDump::kUnitsBytes,
total_stats.total_cache_size);
mad->AddScalar("inactive_areas",
base::trace_event::MemoryAllocatorDump::kUnitsObjects,
total_stats.inactive_area_count);
mad->AddScalar("total_areas",
base::trace_event::MemoryAllocatorDump::kUnitsObjects,
total_stats.total_area_count);
return true;
}
for (const auto& it : namespaces_) {
it.second->OnMemoryDump(pmd);
}
return true;
}
void DOMStorageContextImpl::FindUnusedNamespaces() {
DCHECK(session_storage_database_.get());
if (scavenging_started_)
return;
scavenging_started_ = true;
std::set<std::string> namespace_ids_in_use;
for (StorageNamespaceMap::const_iterator it = namespaces_.begin();
it != namespaces_.end(); ++it)
namespace_ids_in_use.insert(it->second->persistent_namespace_id());
std::set<std::string> protected_persistent_session_ids;
protected_persistent_session_ids.swap(protected_persistent_session_ids_);
task_runner_->PostShutdownBlockingTask(
FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE,
base::Bind(
&DOMStorageContextImpl::FindUnusedNamespacesInCommitSequence,
this, namespace_ids_in_use, protected_persistent_session_ids));
}
void DOMStorageContextImpl::FindUnusedNamespacesInCommitSequence(
const std::set<std::string>& namespace_ids_in_use,
const std::set<std::string>& protected_persistent_session_ids) {
DCHECK(session_storage_database_.get());
// Delete all namespaces which don't have an associated DOMStorageNamespace
// alive.
std::map<std::string, std::vector<GURL> > namespaces_and_origins;
session_storage_database_->ReadNamespacesAndOrigins(&namespaces_and_origins);
for (std::map<std::string, std::vector<GURL> >::const_iterator it =
namespaces_and_origins.begin();
it != namespaces_and_origins.end(); ++it) {
if (namespace_ids_in_use.find(it->first) == namespace_ids_in_use.end() &&
protected_persistent_session_ids.find(it->first) ==
protected_persistent_session_ids.end()) {
deletable_persistent_namespace_ids_.push_back(it->first);
}
}
if (!deletable_persistent_namespace_ids_.empty()) {
task_runner_->PostDelayedTask(
FROM_HERE, base::Bind(
&DOMStorageContextImpl::DeleteNextUnusedNamespace,
this),
base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds));
}
}
void DOMStorageContextImpl::DeleteNextUnusedNamespace() {
if (is_shutdown_)
return;
task_runner_->PostShutdownBlockingTask(
FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE,
base::Bind(
&DOMStorageContextImpl::DeleteNextUnusedNamespaceInCommitSequence,
this));
}
void DOMStorageContextImpl::DeleteNextUnusedNamespaceInCommitSequence() {
if (deletable_persistent_namespace_ids_.empty())
return;
const std::string& persistent_id = deletable_persistent_namespace_ids_.back();
session_storage_database_->DeleteNamespace(persistent_id);
deletable_persistent_namespace_ids_.pop_back();
if (!deletable_persistent_namespace_ids_.empty()) {
task_runner_->PostDelayedTask(
FROM_HERE, base::Bind(
&DOMStorageContextImpl::DeleteNextUnusedNamespace,
this),
base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds));
}
}
} // namespace content