Firefox keeps the Storage panel current by sending store update packets when localStorage or sessionStorage changes. Forward successful Web Storage mutations to the storage actors and emit the matching update or clear packet. Use listener IDs for storage updates so the local and session storage actors can subscribe independently.
287 lines
8.9 KiB
C++
287 lines
8.9 KiB
C++
/*
|
|
* Copyright (c) 2026, Ladybird contributors
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/JsonArray.h>
|
|
#include <AK/JsonObject.h>
|
|
#include <AK/QuickSort.h>
|
|
#include <LibDevTools/Actors/StorageActor.h>
|
|
#include <LibDevTools/Actors/TabActor.h>
|
|
#include <LibDevTools/DevToolsDelegate.h>
|
|
#include <LibDevTools/DevToolsServer.h>
|
|
#include <LibDevTools/StorageHelpers.h>
|
|
|
|
namespace DevTools {
|
|
|
|
static constexpr auto max_store_object_count = 50uz;
|
|
|
|
static JsonObject storage_field(StringView name)
|
|
{
|
|
JsonObject field;
|
|
field.set("name"sv, name);
|
|
field.set("editable"sv, true);
|
|
return field;
|
|
}
|
|
|
|
static JsonObject serialize_storage_item(DevToolsDelegate::StorageItem const& item)
|
|
{
|
|
JsonObject object;
|
|
object.set("name"sv, item.name);
|
|
object.set("value"sv, item.value);
|
|
return object;
|
|
}
|
|
|
|
NonnullRefPtr<StorageActor> StorageActor::create(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, Web::StorageAPI::StorageEndpointType storage_endpoint)
|
|
{
|
|
return adopt_ref(*new StorageActor(devtools, move(name), move(tab), storage_endpoint));
|
|
}
|
|
|
|
StorageActor::StorageActor(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, Web::StorageAPI::StorageEndpointType storage_endpoint)
|
|
: Actor(devtools, move(name))
|
|
, m_tab(move(tab))
|
|
, m_storage_endpoint(storage_endpoint)
|
|
{
|
|
if (auto tab = m_tab.strong_ref()) {
|
|
m_storage_change_listener_id = devtools.delegate().add_storage_change_listener(
|
|
tab->description(),
|
|
weak_callback(*this, [](auto& self, DevToolsDelegate::StorageChange change) {
|
|
self.on_storage_changed(move(change));
|
|
}));
|
|
}
|
|
}
|
|
|
|
StorageActor::~StorageActor()
|
|
{
|
|
if (m_storage_change_listener_id == 0)
|
|
return;
|
|
|
|
if (auto tab = m_tab.strong_ref())
|
|
devtools().delegate().remove_storage_change_listener(tab->description(), m_storage_change_listener_id);
|
|
}
|
|
|
|
StringView StorageActor::resource_type() const
|
|
{
|
|
if (m_storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage)
|
|
return "local-storage"sv;
|
|
VERIFY(m_storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
|
|
return "session-storage"sv;
|
|
}
|
|
|
|
StringView StorageActor::resource_key() const
|
|
{
|
|
if (m_storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage)
|
|
return "localStorage"sv;
|
|
VERIFY(m_storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage);
|
|
return "sessionStorage"sv;
|
|
}
|
|
|
|
Optional<String> StorageActor::host() const
|
|
{
|
|
auto tab = m_tab.strong_ref();
|
|
if (!tab)
|
|
return {};
|
|
return storage_host_for_url(tab->description().url);
|
|
}
|
|
|
|
JsonObject StorageActor::serialize_storage() const
|
|
{
|
|
JsonObject hosts;
|
|
if (auto storage_host = host(); storage_host.has_value())
|
|
hosts.set(*storage_host, JsonArray {});
|
|
|
|
JsonObject traits;
|
|
traits.set("supportsAddItem"sv, true);
|
|
traits.set("supportsRemoveAll"sv, true);
|
|
traits.set("supportsRemoveAllSessionCookies"sv, false);
|
|
traits.set("supportsRemoveItem"sv, true);
|
|
|
|
JsonObject storage;
|
|
storage.set("actor"sv, name());
|
|
if (auto tab = m_tab.strong_ref()) {
|
|
storage.set("browsingContextID"sv, tab->description().id);
|
|
storage.set("innerWindowId"sv, tab->inner_window_id());
|
|
storage.set("resourceId"sv, MUST(String::formatted("{}-{}", resource_key(), tab->inner_window_id())));
|
|
}
|
|
storage.set("hosts"sv, move(hosts));
|
|
storage.set("resourceKey"sv, resource_key());
|
|
storage.set("traits"sv, move(traits));
|
|
return storage;
|
|
}
|
|
|
|
void StorageActor::handle_message(Message const& message)
|
|
{
|
|
if (message.type == "getFields"sv) {
|
|
get_fields(message);
|
|
return;
|
|
}
|
|
|
|
if (message.type == "getStoreObjects"sv) {
|
|
get_store_objects(message);
|
|
return;
|
|
}
|
|
|
|
send_unrecognized_packet_type_error(message);
|
|
}
|
|
|
|
void StorageActor::get_fields(Message const& message)
|
|
{
|
|
JsonArray fields;
|
|
fields.must_append(storage_field("name"sv));
|
|
fields.must_append(storage_field("value"sv));
|
|
|
|
JsonObject response;
|
|
response.set("value"sv, move(fields));
|
|
send_response(message, move(response));
|
|
}
|
|
|
|
void StorageActor::get_store_objects(Message const& message)
|
|
{
|
|
auto requested_host_ref = get_required_parameter<String>(message, "host"sv);
|
|
if (!requested_host_ref.has_value())
|
|
return;
|
|
String requested_host = *requested_host_ref;
|
|
|
|
auto names_value = message.data.get("names"sv);
|
|
Optional<JsonArray> requested_names;
|
|
if (names_value.has_value() && names_value->is_array())
|
|
requested_names = names_value->as_array();
|
|
|
|
JsonObject options;
|
|
auto options_value = message.data.get("options"sv);
|
|
if (options_value.has_value() && options_value->is_object())
|
|
options = options_value->as_object();
|
|
|
|
auto tab = m_tab.strong_ref();
|
|
if (!tab) {
|
|
send_store_objects(message, requested_host, move(requested_names), move(options), Vector<DevToolsDelegate::StorageItem> {});
|
|
return;
|
|
}
|
|
|
|
devtools().delegate().inspect_storage(
|
|
tab->description(),
|
|
m_storage_endpoint,
|
|
weak_callback(*this, [message, requested_host, requested_names, options](auto& self, ErrorOr<Vector<DevToolsDelegate::StorageItem>> storage_items) {
|
|
self.send_store_objects(message, requested_host, requested_names, options, move(storage_items));
|
|
}));
|
|
}
|
|
|
|
void StorageActor::send_store_objects(Message const& message, Optional<String> requested_host, Optional<JsonArray> requested_names, JsonObject options, ErrorOr<Vector<DevToolsDelegate::StorageItem>> storage_items_or_error)
|
|
{
|
|
if (storage_items_or_error.is_error()) {
|
|
JsonObject response;
|
|
response.set("error"sv, "storageError"sv);
|
|
response.set("message"sv, storage_items_or_error.error().string_literal());
|
|
send_response(message, move(response));
|
|
return;
|
|
}
|
|
|
|
Vector<DevToolsDelegate::StorageItem> storage_items = storage_items_or_error.release_value();
|
|
if (!host().has_value() || !requested_host.has_value() || *host() != *requested_host)
|
|
storage_items.clear();
|
|
|
|
if (requested_names.has_value()) {
|
|
storage_items.remove_all_matching([&](auto const& item) {
|
|
return !requested_names->values().contains([&](auto const& value) {
|
|
return value.is_string() && value.as_string() == item.name;
|
|
});
|
|
});
|
|
}
|
|
|
|
quick_sort(storage_items, [](auto const& a, auto const& b) {
|
|
return a.name < b.name;
|
|
});
|
|
|
|
auto total = storage_items.size();
|
|
auto offset = options.get_integer<size_t>("offset"sv).value_or(0);
|
|
auto size = options.get_integer<size_t>("size"sv).value_or(max_store_object_count);
|
|
if (size > max_store_object_count)
|
|
size = max_store_object_count;
|
|
|
|
JsonArray data;
|
|
if (offset > total) {
|
|
offset = total;
|
|
} else {
|
|
auto end = min(total, offset + size);
|
|
for (auto i = offset; i < end; ++i)
|
|
data.must_append(serialize_storage_item(storage_items[i]));
|
|
}
|
|
|
|
JsonObject response;
|
|
response.set("offset"sv, offset);
|
|
response.set("total"sv, total);
|
|
response.set("data"sv, move(data));
|
|
send_response(message, move(response));
|
|
}
|
|
|
|
void StorageActor::on_storage_changed(DevToolsDelegate::StorageChange change)
|
|
{
|
|
if (change.storage_endpoint != m_storage_endpoint)
|
|
return;
|
|
|
|
auto storage_host = host();
|
|
if (!storage_host.has_value() || *storage_host != change.host)
|
|
return;
|
|
|
|
if (change.type == DevToolsDelegate::StorageChange::Type::Cleared) {
|
|
send_store_cleared(change);
|
|
return;
|
|
}
|
|
|
|
if (!change.key.has_value())
|
|
return;
|
|
|
|
send_store_update(change);
|
|
}
|
|
|
|
void StorageActor::send_store_update(DevToolsDelegate::StorageChange const& change)
|
|
{
|
|
StringView update_type;
|
|
switch (change.type) {
|
|
case DevToolsDelegate::StorageChange::Type::Added:
|
|
update_type = "added"sv;
|
|
break;
|
|
case DevToolsDelegate::StorageChange::Type::Changed:
|
|
update_type = "changed"sv;
|
|
break;
|
|
case DevToolsDelegate::StorageChange::Type::Deleted:
|
|
update_type = "deleted"sv;
|
|
break;
|
|
case DevToolsDelegate::StorageChange::Type::Cleared:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
JsonArray keys;
|
|
keys.must_append(*change.key);
|
|
|
|
JsonObject hosts;
|
|
hosts.set(change.host, move(keys));
|
|
|
|
JsonObject storage_updates;
|
|
storage_updates.set(resource_key(), move(hosts));
|
|
|
|
JsonObject data;
|
|
data.set(update_type, move(storage_updates));
|
|
|
|
JsonObject message;
|
|
message.set("type"sv, "storesUpdate"sv);
|
|
message.set("data"sv, move(data));
|
|
send_message(move(message));
|
|
}
|
|
|
|
void StorageActor::send_store_cleared(DevToolsDelegate::StorageChange const& change)
|
|
{
|
|
JsonArray hosts;
|
|
hosts.must_append(change.host);
|
|
|
|
JsonObject data;
|
|
data.set("clearedHostsOrPaths"sv, move(hosts));
|
|
|
|
JsonObject message;
|
|
message.set("type"sv, "storesCleared"sv);
|
|
message.set("data"sv, move(data));
|
|
send_message(move(message));
|
|
}
|
|
|
|
}
|