blob: c04e0e8bff1a7a41a1e18aca5403aed16a80aead [file] [log] [blame]
// Copyright 2016 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/renderer/dom_storage/local_storage_cached_area.h"
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "components/services/leveldb/public/cpp/util.h"
#include "content/common/dom_storage/dom_storage_map.h"
#include "content/renderer/dom_storage/local_storage_area.h"
#include "content/renderer/dom_storage/local_storage_cached_areas.h"
#include "content/renderer/dom_storage/session_web_storage_namespace_impl.h"
#include "content/renderer/render_thread_impl.h"
#include "mojo/public/cpp/bindings/strong_associated_binding.h"
#include "third_party/blink/public/mojom/dom_storage/session_storage_namespace.mojom.h"
#include "third_party/blink/public/mojom/dom_storage/storage_partition_service.mojom.h"
#include "third_party/blink/public/platform/scheduler/web_thread_scheduler.h"
#include "third_party/blink/public/platform/web_url.h"
#include "third_party/blink/public/web/web_storage_event_dispatcher.h"
namespace content {
namespace {
// Don't change or reorder any of the values in this enum, as these values
// are serialized on disk.
enum class StorageFormat : uint8_t { UTF16 = 0, Latin1 = 1 };
class GetAllCallback : public blink::mojom::StorageAreaGetAllCallback {
public:
static blink::mojom::StorageAreaGetAllCallbackAssociatedPtrInfo CreateAndBind(
base::OnceCallback<void(bool)> callback) {
blink::mojom::StorageAreaGetAllCallbackAssociatedPtrInfo ptr_info;
auto request = mojo::MakeRequest(&ptr_info);
mojo::MakeStrongAssociatedBinding(
base::WrapUnique(new GetAllCallback(std::move(callback))),
std::move(request));
return ptr_info;
}
private:
explicit GetAllCallback(base::OnceCallback<void(bool)> callback)
: m_callback(std::move(callback)) {}
void Complete(bool success) override { std::move(m_callback).Run(success); }
base::OnceCallback<void(bool)> m_callback;
};
} // namespace
// These methods are used to pack and unpack the page_url/storage_area_id into
// source strings to/from the browser.
std::string PackSource(const GURL& page_url,
const std::string& storage_area_id) {
return page_url.spec() + "\n" + storage_area_id;
}
void UnpackSource(const std::string& source,
GURL* page_url,
std::string* storage_area_id) {
std::vector<std::string> result = base::SplitString(
source, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
DCHECK_EQ(result.size(), 2u);
*page_url = GURL(result[0]);
*storage_area_id = result[1];
}
LocalStorageCachedArea::LocalStorageCachedArea(
const std::string& namespace_id,
const url::Origin& origin,
blink::mojom::SessionStorageNamespace* session_namespace,
LocalStorageCachedAreas* cached_areas,
blink::scheduler::WebThreadScheduler* main_thread_scheduler)
: namespace_id_(namespace_id),
origin_(origin),
binding_(this),
cached_areas_(cached_areas),
main_thread_scheduler_(main_thread_scheduler),
weak_factory_(this) {
DCHECK(!namespace_id_.empty());
blink::mojom::StorageAreaAssociatedPtrInfo wrapper_ptr_info;
session_namespace->OpenArea(origin_, mojo::MakeRequest(&wrapper_ptr_info));
leveldb_.Bind(std::move(wrapper_ptr_info),
main_thread_scheduler->IPCTaskRunner());
blink::mojom::StorageAreaObserverAssociatedPtrInfo ptr_info;
binding_.Bind(mojo::MakeRequest(&ptr_info),
main_thread_scheduler->IPCTaskRunner());
leveldb_->AddObserver(std::move(ptr_info));
}
LocalStorageCachedArea::LocalStorageCachedArea(
const url::Origin& origin,
blink::mojom::StoragePartitionService* storage_partition_service,
LocalStorageCachedAreas* cached_areas,
blink::scheduler::WebThreadScheduler* main_thread_scheduler)
: origin_(origin),
binding_(this),
cached_areas_(cached_areas),
main_thread_scheduler_(main_thread_scheduler),
weak_factory_(this) {
DCHECK(namespace_id_.empty());
blink::mojom::StorageAreaPtrInfo wrapper_ptr_info;
storage_partition_service->OpenLocalStorage(
origin_, mojo::MakeRequest(&wrapper_ptr_info));
leveldb_.Bind(std::move(wrapper_ptr_info),
main_thread_scheduler->IPCTaskRunner());
blink::mojom::StorageAreaObserverAssociatedPtrInfo ptr_info;
binding_.Bind(mojo::MakeRequest(&ptr_info),
main_thread_scheduler->IPCTaskRunner());
leveldb_->AddObserver(std::move(ptr_info));
}
LocalStorageCachedArea::~LocalStorageCachedArea() {}
unsigned LocalStorageCachedArea::GetLength() {
EnsureLoaded();
return map_->Length();
}
base::NullableString16 LocalStorageCachedArea::GetKey(
unsigned index,
bool* did_decrease_iterator) {
EnsureLoaded();
return map_->Key(index, did_decrease_iterator);
}
base::NullableString16 LocalStorageCachedArea::GetItem(
const base::string16& key) {
EnsureLoaded();
return map_->GetItem(key);
}
bool LocalStorageCachedArea::SetItem(const base::string16& key,
const base::string16& value,
const GURL& page_url,
const std::string& storage_area_id) {
// A quick check to reject obviously overbudget items to avoid priming the
// cache.
if ((key.length() + value.length()) * sizeof(base::char16) >
kPerStorageAreaQuota)
return false;
EnsureLoaded();
bool result = false;
base::NullableString16 old_nullable_value;
if (should_send_old_value_on_mutations_)
result = map_->SetItem(key, value, &old_nullable_value);
else
result = map_->SetItem(key, value, nullptr);
if (!result)
return false;
// Determine data formats.
bool is_session_storage = IsSessionStorage();
FormatOption key_format = is_session_storage
? FormatOption::kSessionStorageForceUTF8
: FormatOption::kLocalStorageDetectFormat;
FormatOption value_format = is_session_storage
? FormatOption::kSessionStorageForceUTF16
: FormatOption::kLocalStorageDetectFormat;
// Ignore mutations to |key| until OnSetItemComplete.
ignore_key_mutations_[key]++;
base::Optional<std::vector<uint8_t>> optional_old_value;
if (!old_nullable_value.is_null())
optional_old_value =
String16ToUint8Vector(old_nullable_value.string(), value_format);
blink::WebScopedVirtualTimePauser virtual_time_pauser =
main_thread_scheduler_->CreateWebScopedVirtualTimePauser(
"LocalStorageCachedArea");
virtual_time_pauser.PauseVirtualTime();
leveldb_->Put(String16ToUint8Vector(key, key_format),
String16ToUint8Vector(value, value_format), optional_old_value,
PackSource(page_url, storage_area_id),
base::BindOnce(&LocalStorageCachedArea::OnSetItemComplete,
weak_factory_.GetWeakPtr(), key,
std::move(virtual_time_pauser)));
if (IsSessionStorage() &&
(old_nullable_value.is_null() || old_nullable_value.string() != value)) {
blink::WebStorageArea* originating_area = areas_[storage_area_id];
DCHECK_NE(nullptr, originating_area);
SessionWebStorageNamespaceImpl session_namespace_for_event_dispatch(
namespace_id_, nullptr);
blink::WebStorageEventDispatcher::DispatchSessionStorageEvent(
blink::WebString::FromUTF16(key),
blink::WebString::FromUTF16(old_nullable_value),
blink::WebString::FromUTF16(value), origin_.GetURL(), page_url,
session_namespace_for_event_dispatch, originating_area);
}
return true;
}
void LocalStorageCachedArea::RemoveItem(const base::string16& key,
const GURL& page_url,
const std::string& storage_area_id) {
EnsureLoaded();
bool result = false;
base::string16 old_value;
if (should_send_old_value_on_mutations_)
result = map_->RemoveItem(key, &old_value);
else
result = map_->RemoveItem(key, nullptr);
if (!result)
return;
// Determine data formats.
bool is_session_storage = IsSessionStorage();
FormatOption key_format = is_session_storage
? FormatOption::kSessionStorageForceUTF8
: FormatOption::kLocalStorageDetectFormat;
FormatOption value_format = is_session_storage
? FormatOption::kSessionStorageForceUTF16
: FormatOption::kLocalStorageDetectFormat;
// Ignore mutations to |key| until OnRemoveItemComplete.
ignore_key_mutations_[key]++;
base::Optional<std::vector<uint8_t>> optional_old_value;
if (should_send_old_value_on_mutations_)
optional_old_value = String16ToUint8Vector(old_value, value_format);
blink::WebScopedVirtualTimePauser virtual_time_pauser =
main_thread_scheduler_->CreateWebScopedVirtualTimePauser(
"LocalStorageCachedArea");
virtual_time_pauser.PauseVirtualTime();
leveldb_->Delete(String16ToUint8Vector(key, key_format), optional_old_value,
PackSource(page_url, storage_area_id),
base::BindOnce(&LocalStorageCachedArea::OnRemoveItemComplete,
weak_factory_.GetWeakPtr(), key,
std::move(virtual_time_pauser)));
if (IsSessionStorage() && old_value != base::string16()) {
blink::WebStorageArea* originating_area = areas_[storage_area_id];
DCHECK_NE(nullptr, originating_area);
SessionWebStorageNamespaceImpl session_namespace_for_event_dispatch(
namespace_id_, nullptr);
blink::WebStorageEventDispatcher::DispatchSessionStorageEvent(
blink::WebString::FromUTF16(key),
blink::WebString::FromUTF16(old_value), blink::WebString(),
origin_.GetURL(), page_url, session_namespace_for_event_dispatch,
originating_area);
}
}
void LocalStorageCachedArea::Clear(const GURL& page_url,
const std::string& storage_area_id) {
bool already_empty = false;
if (IsSessionStorage()) {
EnsureLoaded();
already_empty = map_->Length() == 0u;
}
// No need to prime the cache in this case.
Reset();
map_ = new DOMStorageMap(kPerStorageAreaQuota);
ignore_all_mutations_ = true;
blink::WebScopedVirtualTimePauser virtual_time_pauser =
main_thread_scheduler_->CreateWebScopedVirtualTimePauser(
"LocalStorageCachedArea");
virtual_time_pauser.PauseVirtualTime();
leveldb_->DeleteAll(PackSource(page_url, storage_area_id),
base::BindOnce(&LocalStorageCachedArea::OnClearComplete,
weak_factory_.GetWeakPtr(),
std::move(virtual_time_pauser)));
if (IsSessionStorage() && !already_empty) {
blink::WebStorageArea* originating_area = areas_[storage_area_id];
DCHECK_NE(nullptr, originating_area);
SessionWebStorageNamespaceImpl session_namespace_for_event_dispatch(
namespace_id_, nullptr);
blink::WebStorageEventDispatcher::DispatchSessionStorageEvent(
blink::WebString(), blink::WebString(), blink::WebString(),
origin_.GetURL(), page_url, session_namespace_for_event_dispatch,
originating_area);
}
}
void LocalStorageCachedArea::AreaCreated(LocalStorageArea* area) {
areas_[area->id()] = area;
}
void LocalStorageCachedArea::AreaDestroyed(LocalStorageArea* area) {
areas_.erase(area->id());
}
// static
base::string16 LocalStorageCachedArea::Uint8VectorToString16(
const std::vector<uint8_t>& input,
FormatOption format_option) {
if (input.empty())
return base::string16();
size_t input_size = input.size();
base::string16 result;
switch (format_option) {
case FormatOption::kSessionStorageForceUTF16:
if (input_size % sizeof(base::char16) != 0) {
// TODO(mek): Better error recovery when corrupt (or otherwise invalid)
// data is detected.
LOCAL_HISTOGRAM_BOOLEAN("LocalStorageCachedArea.CorruptData", true);
LOG(ERROR) << "Corrupt data in domstorage";
return base::string16();
}
result.resize(input_size / sizeof(base::char16));
std::memcpy(&result[0], input.data(), input_size);
return result;
case FormatOption::kSessionStorageForceUTF8:
// Encoding / codepoint errors are ignored on purpose.
return base::UTF8ToUTF16(leveldb::Uint8VectorToStringPiece(input));
case FormatOption::kLocalStorageDetectFormat:
break;
}
StorageFormat format = static_cast<StorageFormat>(input[0]);
const size_t payload_size = input_size - 1;
bool corrupt = false;
switch (format) {
case StorageFormat::UTF16:
if (payload_size % sizeof(base::char16) != 0) {
corrupt = true;
break;
}
result.resize(payload_size / sizeof(base::char16));
std::memcpy(&result[0], input.data() + 1, payload_size);
break;
case StorageFormat::Latin1:
result.resize(payload_size);
std::copy(input.begin() + 1, input.end(), result.begin());
break;
default:
corrupt = true;
}
if (corrupt) {
// TODO(mek): Better error recovery when corrupt (or otherwise invalid) data
// is detected.
LOCAL_HISTOGRAM_BOOLEAN("LocalStorageCachedArea.CorruptData", true);
LOG(ERROR) << "Corrupt data in localstorage";
return base::string16();
}
return result;
}
// static
std::vector<uint8_t> LocalStorageCachedArea::String16ToUint8Vector(
const base::string16& input,
FormatOption format_option) {
switch (format_option) {
case FormatOption::kSessionStorageForceUTF16: {
std::vector<uint8_t> result;
result.reserve(input.size() * sizeof(base::char16));
const uint8_t* data = reinterpret_cast<const uint8_t*>(input.data());
result.insert(result.begin(), data,
data + input.size() * sizeof(base::char16));
return result;
}
case FormatOption::kSessionStorageForceUTF8: {
// Encoding / codepoint errors are ignored on purpose.
std::string utf8 = base::UTF16ToUTF8(base::StringPiece16(input));
return leveldb::StdStringToUint8Vector(utf8);
}
case FormatOption::kLocalStorageDetectFormat:
break;
}
bool is_8bit = true;
for (const auto& c : input) {
if (c & 0xff00) {
is_8bit = false;
break;
}
}
if (is_8bit) {
std::vector<uint8_t> result(input.size() + 1);
result[0] = static_cast<uint8_t>(StorageFormat::Latin1);
std::copy(input.begin(), input.end(), result.begin() + 1);
return result;
}
const uint8_t* data = reinterpret_cast<const uint8_t*>(input.data());
std::vector<uint8_t> result;
result.reserve(input.size() * sizeof(base::char16) + 1);
result.push_back(static_cast<uint8_t>(StorageFormat::UTF16));
result.insert(result.end(), data, data + input.size() * sizeof(base::char16));
return result;
}
void LocalStorageCachedArea::KeyAdded(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& value,
const std::string& source) {
DCHECK(!IsSessionStorage());
base::NullableString16 null_value;
KeyAddedOrChanged(key, value, null_value, source);
}
void LocalStorageCachedArea::KeyChanged(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& new_value,
const std::vector<uint8_t>& old_value,
const std::string& source) {
DCHECK(!IsSessionStorage());
base::NullableString16 old_value_str(
Uint8VectorToString16(old_value, FormatOption::kLocalStorageDetectFormat),
false);
KeyAddedOrChanged(key, new_value, old_value_str, source);
}
void LocalStorageCachedArea::KeyDeleted(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& old_value,
const std::string& source) {
DCHECK(!IsSessionStorage());
GURL page_url;
std::string storage_area_id;
UnpackSource(source, &page_url, &storage_area_id);
base::string16 key_string =
Uint8VectorToString16(key, FormatOption::kLocalStorageDetectFormat);
blink::WebStorageArea* originating_area = nullptr;
if (areas_.find(storage_area_id) != areas_.end()) {
// The source storage area is in this process.
originating_area = areas_[storage_area_id];
} else if (map_ && !ignore_all_mutations_) {
// This was from another process or the storage area is gone. If the former,
// remove it from our cache if we haven't already changed it and are waiting
// for the confirmation callback. In the latter case, we won't do anything
// because ignore_key_mutations_ won't be updated until the callback runs.
if (ignore_key_mutations_.find(key_string) == ignore_key_mutations_.end())
map_->RemoveItem(key_string, nullptr);
}
blink::WebStorageEventDispatcher::DispatchLocalStorageEvent(
blink::WebString::FromUTF16(key_string),
blink::WebString::FromUTF16(Uint8VectorToString16(
old_value, FormatOption::kLocalStorageDetectFormat)),
blink::WebString(), origin_.GetURL(), page_url, originating_area);
}
void LocalStorageCachedArea::AllDeleted(const std::string& source) {
GURL page_url;
std::string storage_area_id;
UnpackSource(source, &page_url, &storage_area_id);
blink::WebStorageArea* originating_area = nullptr;
if (areas_.find(storage_area_id) != areas_.end()) {
// The source storage area is in this process.
originating_area = areas_[storage_area_id];
} else if (map_ && !ignore_all_mutations_) {
scoped_refptr<DOMStorageMap> old = map_;
map_ = new DOMStorageMap(kPerStorageAreaQuota);
// We have to retain local additions which happened after this clear
// operation from another process.
auto iter = ignore_key_mutations_.begin();
while (iter != ignore_key_mutations_.end()) {
base::NullableString16 value = old->GetItem(iter->first);
if (!value.is_null())
map_->SetItem(iter->first, value.string(), nullptr);
++iter;
}
}
if (IsSessionStorage()) {
SessionWebStorageNamespaceImpl session_namespace_for_event_dispatch(
namespace_id_, nullptr);
blink::WebStorageEventDispatcher::DispatchSessionStorageEvent(
blink::WebString(), blink::WebString(), blink::WebString(),
origin_.GetURL(), page_url, session_namespace_for_event_dispatch,
originating_area);
} else {
blink::WebStorageEventDispatcher::DispatchLocalStorageEvent(
blink::WebString(), blink::WebString(), blink::WebString(),
origin_.GetURL(), page_url, originating_area);
}
}
void LocalStorageCachedArea::ShouldSendOldValueOnMutations(bool value) {
DCHECK(!IsSessionStorage());
should_send_old_value_on_mutations_ = value;
}
void LocalStorageCachedArea::KeyAddedOrChanged(
const std::vector<uint8_t>& key,
const std::vector<uint8_t>& new_value,
const base::NullableString16& old_value,
const std::string& source) {
DCHECK(!IsSessionStorage());
GURL page_url;
std::string storage_area_id;
UnpackSource(source, &page_url, &storage_area_id);
base::string16 key_string =
Uint8VectorToString16(key, FormatOption::kLocalStorageDetectFormat);
base::string16 new_value_string =
Uint8VectorToString16(new_value, FormatOption::kLocalStorageDetectFormat);
blink::WebStorageArea* originating_area = nullptr;
if (areas_.find(storage_area_id) != areas_.end()) {
// The source storage area is in this process.
originating_area = areas_[storage_area_id];
} else if (map_ && !ignore_all_mutations_) {
// This was from another process or the storage area is gone. If the former,
// apply it to our cache if we haven't already changed it and are waiting
// for the confirmation callback. In the latter case, we won't do anything
// because ignore_key_mutations_ won't be updated until the callback runs.
if (ignore_key_mutations_.find(key_string) == ignore_key_mutations_.end()) {
// We turn off quota checking here to accomodate the over budget allowance
// that's provided in the browser process.
map_->set_quota(std::numeric_limits<int32_t>::max());
map_->SetItem(key_string, new_value_string, nullptr);
map_->set_quota(kPerStorageAreaQuota);
}
}
blink::WebStorageEventDispatcher::DispatchLocalStorageEvent(
blink::WebString::FromUTF16(key_string),
blink::WebString::FromUTF16(old_value),
blink::WebString::FromUTF16(new_value_string), origin_.GetURL(), page_url,
originating_area);
}
void LocalStorageCachedArea::EnsureLoaded() {
if (map_)
return;
base::TimeTicks before = base::TimeTicks::Now();
ignore_all_mutations_ = true;
bool success = false;
std::vector<blink::mojom::KeyValuePtr> data;
leveldb_->GetAll(GetAllCallback::CreateAndBind(
base::BindOnce(&LocalStorageCachedArea::OnGetAllComplete,
weak_factory_.GetWeakPtr())),
&success, &data);
DOMStorageValuesMap values;
bool is_session_storage = IsSessionStorage();
FormatOption key_format = is_session_storage
? FormatOption::kSessionStorageForceUTF8
: FormatOption::kLocalStorageDetectFormat;
FormatOption value_format = is_session_storage
? FormatOption::kSessionStorageForceUTF16
: FormatOption::kLocalStorageDetectFormat;
for (size_t i = 0; i < data.size(); ++i) {
values[Uint8VectorToString16(data[i]->key, key_format)] =
base::NullableString16(
Uint8VectorToString16(data[i]->value, value_format), false);
}
map_ = new DOMStorageMap(kPerStorageAreaQuota);
map_->SwapValues(&values);
base::TimeDelta time_to_prime = base::TimeTicks::Now() - before;
UMA_HISTOGRAM_TIMES("LocalStorage.MojoTimeToPrime", time_to_prime);
size_t local_storage_size_kb = map_->storage_used() / 1024;
// Track localStorage size, from 0-6MB. Note that the maximum size should be
// 5MB, but we add some slop since we want to make sure the max size is always
// above what we see in practice, since histograms can't change.
UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.MojoSizeInKB",
local_storage_size_kb,
1, 6 * 1024, 50);
if (local_storage_size_kb < 100) {
UMA_HISTOGRAM_TIMES("LocalStorage.MojoTimeToPrimeForUnder100KB",
time_to_prime);
} else if (local_storage_size_kb < 1000) {
UMA_HISTOGRAM_TIMES("LocalStorage.MojoTimeToPrimeFor100KBTo1MB",
time_to_prime);
} else {
UMA_HISTOGRAM_TIMES("LocalStorage.MojoTimeToPrimeFor1MBTo5MB",
time_to_prime);
}
}
void LocalStorageCachedArea::OnSetItemComplete(
const base::string16& key,
blink::WebScopedVirtualTimePauser,
bool success) {
if (!success) {
Reset();
return;
}
auto found = ignore_key_mutations_.find(key);
DCHECK(found != ignore_key_mutations_.end());
if (--found->second == 0)
ignore_key_mutations_.erase(found);
}
void LocalStorageCachedArea::OnRemoveItemComplete(
const base::string16& key,
blink::WebScopedVirtualTimePauser,
bool success) {
DCHECK(success);
auto found = ignore_key_mutations_.find(key);
DCHECK(found != ignore_key_mutations_.end());
if (--found->second == 0)
ignore_key_mutations_.erase(found);
}
void LocalStorageCachedArea::OnClearComplete(blink::WebScopedVirtualTimePauser,
bool success) {
DCHECK(success);
DCHECK(ignore_all_mutations_);
ignore_all_mutations_ = false;
}
void LocalStorageCachedArea::OnGetAllComplete(bool success) {
// Since the GetAll method is synchronous, we need this asynchronously
// delivered notification to avoid applying changes to the returned array
// that we already have.
DCHECK(success);
DCHECK(ignore_all_mutations_);
ignore_all_mutations_ = false;
}
void LocalStorageCachedArea::Reset() {
map_ = nullptr;
ignore_key_mutations_.clear();
ignore_all_mutations_ = false;
weak_factory_.InvalidateWeakPtrs();
}
} // namespace content