LibDevTools+LibWeb: Let DevTools remove IndexedDB data

Firefox exposes removal operations for IndexedDB databases, object
stores, and records from the Storage panel. Route those actor requests
to LibWeb so DevTools can delete the selected data and receive the
update or clear event needed to refresh the panel.
This commit is contained in:
Sam Atkins 2026-06-11 16:59:48 +01:00 committed by Jelle Raaijmakers
parent e667aaaab1
commit fdfe806e68
8 changed files with 376 additions and 0 deletions

View file

@ -99,6 +99,21 @@ void IndexedDBActor::handle_message(Message const& message)
return;
}
if (message.type == "removeDatabase"sv) {
remove_database(message);
return;
}
if (message.type == "removeAll"sv) {
remove_all(message);
return;
}
if (message.type == "removeItem"sv) {
remove_item(message);
return;
}
send_unrecognized_packet_type_error(message);
}
@ -169,6 +184,135 @@ void IndexedDBActor::get_store_objects(Message const& message)
});
}
void IndexedDBActor::remove_database(Message const& message)
{
auto host = get_required_parameter<String>(message, "host"sv);
auto name = get_required_parameter<String>(message, "name"sv);
if (!host.has_value() || !name.has_value())
return;
auto tab = m_tab.strong_ref();
if (!tab) {
send_inspection_error(message, Error::from_string_literal("Unable to locate tab"));
return;
}
devtools().delegate().delete_indexed_database(tab->description(), *host, *name,
[weak_self = make_weak_ptr<IndexedDBActor>(), message_id = message.id, host = *host, name = *name](ErrorOr<JsonObject> result) mutable {
auto self = weak_self.strong_ref();
if (!self)
return;
if (result.is_error()) {
self->send_inspection_error({ .id = message_id }, result.error());
return;
}
auto response = result.release_value();
auto blocked = response.get_bool("blocked"sv).value_or(false);
self->send_response({ .id = message_id }, move(response));
if (!blocked) {
JsonArray path;
path.must_append(name);
self->send_indexed_database_update("deleted"sv, host, path.serialized());
}
});
}
void IndexedDBActor::remove_all(Message const& message)
{
auto host = get_required_parameter<String>(message, "host"sv);
auto name = get_required_parameter<String>(message, "name"sv);
if (!host.has_value() || !name.has_value())
return;
auto tab = m_tab.strong_ref();
if (!tab) {
send_inspection_error(message, Error::from_string_literal("Unable to locate tab"));
return;
}
devtools().delegate().clear_indexed_database_object_store(tab->description(), *host, *name,
[weak_self = make_weak_ptr<IndexedDBActor>(), message_id = message.id, host = *host, name = *name](ErrorOr<JsonObject> result) mutable {
auto self = weak_self.strong_ref();
if (!self)
return;
if (result.is_error()) {
self->send_inspection_error({ .id = message_id }, result.error());
return;
}
auto response = result.release_value();
self->send_response({ .id = message_id }, move(response));
self->send_indexed_database_clear(host, name);
});
}
void IndexedDBActor::remove_item(Message const& message)
{
auto host = get_required_parameter<String>(message, "host"sv);
auto name = get_required_parameter<String>(message, "name"sv);
if (!host.has_value() || !name.has_value())
return;
auto tab = m_tab.strong_ref();
if (!tab) {
send_inspection_error(message, Error::from_string_literal("Unable to locate tab"));
return;
}
devtools().delegate().delete_indexed_database_record(tab->description(), *host, *name,
[weak_self = make_weak_ptr<IndexedDBActor>(), message_id = message.id, host = *host, name = *name](ErrorOr<JsonObject> result) mutable {
auto self = weak_self.strong_ref();
if (!self)
return;
if (result.is_error()) {
self->send_inspection_error({ .id = message_id }, result.error());
return;
}
auto response = result.release_value();
self->send_response({ .id = message_id }, move(response));
self->send_indexed_database_update("deleted"sv, host, name);
});
}
void IndexedDBActor::send_indexed_database_update(StringView update_type, String const& host, String const& name)
{
JsonArray paths;
paths.must_append(name);
JsonObject hosts;
hosts.set(host, move(paths));
JsonObject indexed_database;
indexed_database.set("indexedDB"sv, move(hosts));
JsonObject update;
update.set(update_type, move(indexed_database));
on_indexed_database_changed(move(update));
}
void IndexedDBActor::send_indexed_database_clear(String const& host, String const& name)
{
JsonArray paths;
paths.must_append(name);
JsonObject hosts;
hosts.set(host, move(paths));
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));
}
void IndexedDBActor::on_indexed_database_changed(JsonObject update)
{
JsonObject message;

View file

@ -27,10 +27,15 @@ private:
void get_fields(Message const&);
void get_store_objects(Message const&);
void remove_database(Message const&);
void remove_all(Message const&);
void remove_item(Message const&);
void on_indexed_database_changed(JsonObject);
JsonObject serialize_storage(JsonObject hosts) const;
void send_inspection_error(Message const&, Error const&);
void send_indexed_database_update(StringView update_type, String const& host, String const& name);
void send_indexed_database_clear(String const& host, String const& name);
WeakPtr<TabActor> m_tab;
u64 m_indexed_database_change_listener_id { 0 };

View file

@ -78,6 +78,9 @@ public:
using OnIndexedDBInspectionComplete = Function<void(ErrorOr<JsonObject>)>;
virtual void inspect_indexed_database_storage(TabDescription const&, OnIndexedDBInspectionComplete) const { }
virtual void inspect_indexed_database_objects(TabDescription const&, String const&, Optional<JsonArray>, JsonObject, OnIndexedDBInspectionComplete) const { }
virtual void delete_indexed_database(TabDescription const&, String const&, String const&, OnIndexedDBInspectionComplete) const { }
virtual void clear_indexed_database_object_store(TabDescription const&, String const&, String const&, OnIndexedDBInspectionComplete) const { }
virtual void delete_indexed_database_record(TabDescription const&, String const&, String const&, OnIndexedDBInspectionComplete) const { }
using OnIndexedDatabaseChange = Function<void(JsonObject)>;
virtual u64 add_indexed_database_change_listener(TabDescription const&, OnIndexedDatabaseChange) const { return 0; }
virtual void remove_indexed_database_change_listener(TabDescription const&, u64) const { }

View file

@ -982,6 +982,27 @@ void ViewImplementation::inspect_indexed_database_objects(String const& host, Op
client().async_inspect_indexed_database_objects(page_id(), request_id, host, JsonValue {}, JsonValue { move(options) });
}
void ViewImplementation::delete_indexed_database(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
{
auto request_id = m_next_indexed_database_inspection_request_id++;
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
client().async_delete_indexed_database(page_id(), request_id, host, name);
}
void ViewImplementation::clear_indexed_database_object_store(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
{
auto request_id = m_next_indexed_database_inspection_request_id++;
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
client().async_clear_indexed_database_object_store(page_id(), request_id, host, name);
}
void ViewImplementation::delete_indexed_database_record(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete on_complete)
{
auto request_id = m_next_indexed_database_inspection_request_id++;
m_pending_indexed_database_inspection_requests.set(request_id, move(on_complete));
client().async_delete_indexed_database_record(page_id(), request_id, host, name);
}
void ViewImplementation::did_receive_indexed_database_inspection(u64 request_id, JsonObject result)
{
auto callback = m_pending_indexed_database_inspection_requests.take(request_id);

View file

@ -152,6 +152,9 @@ public:
void inspect_indexed_database_storage(DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete);
void inspect_indexed_database_objects(String const& host, Optional<JsonArray> names, JsonObject options, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete);
void delete_indexed_database(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete);
void clear_indexed_database_object_store(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete);
void delete_indexed_database_record(String const& host, String const& name, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete);
ByteString selected_text();
ByteString cut_selected_text();

View file

@ -113,6 +113,9 @@ private:
virtual void inspect_current_flexbox(u64 page_id, Web::UniqueNodeID node_id, bool only_look_at_parents) override;
virtual void inspect_indexed_database_storage(u64 page_id, u64 request_id) override;
virtual void inspect_indexed_database_objects(u64 page_id, u64 request_id, String host, JsonValue names, JsonValue options) override;
virtual void delete_indexed_database(u64 page_id, u64 request_id, String host, String name) override;
virtual void clear_indexed_database_object_store(u64 page_id, u64 request_id, String host, String name) override;
virtual void delete_indexed_database_record(u64 page_id, u64 request_id, String host, String name) override;
virtual void clear_inspected_dom_node(u64 page_id) override;
virtual void highlight_dom_node(u64 page_id, Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element) override;
virtual void highlight_flexbox(u64 page_id, Web::UniqueNodeID node_id, JsonValue options) override;

View file

@ -84,6 +84,9 @@ endpoint WebContentServer
inspect_current_flexbox(u64 page_id, Web::UniqueNodeID node_id, bool only_look_at_parents) =|
inspect_indexed_database_storage(u64 page_id, u64 request_id) =|
inspect_indexed_database_objects(u64 page_id, u64 request_id, String host, JsonValue names, JsonValue options) =|
delete_indexed_database(u64 page_id, u64 request_id, String host, String name) =|
clear_indexed_database_object_store(u64 page_id, u64 request_id, String host, String name) =|
delete_indexed_database_record(u64 page_id, u64 request_id, String host, String name) =|
clear_inspected_dom_node(u64 page_id) =|
highlight_dom_node(u64 page_id, Web::UniqueNodeID node_id, Optional<Web::CSS::PseudoElement> pseudo_element) =|
highlight_flexbox(u64 page_id, Web::UniqueNodeID node_id, JsonValue options) =|

View file

@ -837,6 +837,10 @@ public:
++delete_indexed_database_call_count;
last_indexed_database_host = host;
last_indexed_database_name = name;
if (fail_delete_indexed_database) {
callback(Error::from_string_literal("IndexedDB operation failed"));
return;
}
callback(JsonObject {});
}
@ -845,6 +849,10 @@ public:
++clear_indexed_database_object_store_call_count;
last_indexed_database_host = host;
last_indexed_database_name = name;
if (fail_clear_indexed_database_object_store) {
callback(Error::from_string_literal("IndexedDB operation failed"));
return;
}
callback(JsonObject {});
}
@ -853,6 +861,10 @@ public:
++delete_indexed_database_record_call_count;
last_indexed_database_host = host;
last_indexed_database_name = name;
if (fail_delete_indexed_database_record) {
callback(Error::from_string_literal("IndexedDB operation failed"));
return;
}
callback(JsonObject {});
}
@ -1340,6 +1352,9 @@ public:
mutable size_t add_indexed_database_change_listener_call_count { 0 };
mutable size_t remove_indexed_database_change_listener_call_count { 0 };
mutable u64 next_indexed_database_change_listener_id { 1 };
mutable bool fail_delete_indexed_database { false };
mutable bool fail_clear_indexed_database_object_store { false };
mutable bool fail_delete_indexed_database_record { false };
mutable size_t inspect_accessibility_tree_call_count { 0 };
mutable size_t listen_for_dom_properties_call_count { 0 };
mutable size_t stop_listening_for_dom_properties_call_count { 0 };
@ -1413,6 +1428,7 @@ public:
mutable Optional<bool> last_reload_bypass_cache;
mutable Optional<int> last_history_delta;
mutable Optional<String> last_indexed_database_host;
mutable Optional<String> last_indexed_database_name;
};
class ProtocolClient {
@ -1439,6 +1455,23 @@ public:
return read_message_from_socket();
}
bool has_pending_message(AK::Duration timeout = 50_ms)
{
if (!m_pending_messages.is_empty())
return true;
for (i64 elapsed_ms = 0; elapsed_ms < timeout.to_milliseconds(); elapsed_ms += 5) {
pump(m_loop);
if (MUST(m_socket->can_read_without_blocking())) {
m_pending_messages.append(read_message_now());
return true;
}
MUST(Core::System::sleep_ms(5));
}
return false;
}
void send(JsonObject message)
{
auto serialized = message.serialized();
@ -2681,6 +2714,167 @@ TEST_CASE(storage_indexed_database_serializes_live_tree_updates)
EXPECT(!update.get_object("deleted"sv).has_value());
}
TEST_CASE(storage_indexed_database_remove_database)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
auto indexed_database_actor = get_indexed_database_actor(client);
JsonObject remove_database;
remove_database.set("to"sv, indexed_database_actor);
remove_database.set("type"sv, "removeDatabase"sv);
remove_database.set("host"sv, "https://example.test"sv);
remove_database.set("name"sv, "fixtures (default)"sv);
EXPECT_EQ(client.request(move(remove_database)).get_string("from"sv).value(), indexed_database_actor);
EXPECT_EQ(session->delegate.delete_indexed_database_call_count, 1u);
EXPECT_EQ(session->delegate.last_indexed_database_host.value(), "https://example.test"sv);
EXPECT_EQ(session->delegate.last_indexed_database_name.value(), "fixtures (default)"sv);
auto stores_update = read_packet_with_type(client, "storesUpdate"sv);
EXPECT_EQ(stores_update.get_string("from"sv).value(), indexed_database_actor);
auto deleted_paths = get_indexed_database_update_paths(stores_update, "deleted"sv);
EXPECT_EQ(deleted_paths.size(), 1u);
EXPECT_EQ(deleted_paths.at(0).as_string(), indexed_database_path("fixtures (default)"sv));
}
TEST_CASE(storage_indexed_database_remove_database_error)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
session->delegate.fail_delete_indexed_database = true;
auto indexed_database_actor = get_indexed_database_actor(client);
JsonObject remove_database;
remove_database.set("to"sv, indexed_database_actor);
remove_database.set("type"sv, "removeDatabase"sv);
remove_database.set("host"sv, "https://example.test"sv);
remove_database.set("name"sv, "fixtures (default)"sv);
auto response = client.request(move(remove_database));
EXPECT_EQ(response.get_string("error"sv).value(), "indexedDBInspectionFailed"sv);
EXPECT_EQ(response.get_string("message"sv).value(), "IndexedDB operation failed"sv);
EXPECT_EQ(session->delegate.delete_indexed_database_call_count, 1u);
EXPECT(!client.has_pending_message());
}
TEST_CASE(storage_indexed_database_remove_all)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
auto indexed_database_actor = get_indexed_database_actor(client);
auto store_path = indexed_database_path("fixtures (default)"sv, "people"sv);
JsonObject remove_all;
remove_all.set("to"sv, indexed_database_actor);
remove_all.set("type"sv, "removeAll"sv);
remove_all.set("host"sv, "https://example.test"sv);
remove_all.set("name"sv, store_path);
EXPECT_EQ(client.request(move(remove_all)).get_string("from"sv).value(), indexed_database_actor);
EXPECT_EQ(session->delegate.clear_indexed_database_object_store_call_count, 1u);
EXPECT_EQ(session->delegate.last_indexed_database_host.value(), "https://example.test"sv);
EXPECT_EQ(session->delegate.last_indexed_database_name.value(), store_path);
auto stores_cleared = read_packet_with_type(client, "storesCleared"sv);
EXPECT_EQ(stores_cleared.get_string("from"sv).value(), indexed_database_actor);
auto cleared_paths = get_indexed_database_cleared_paths(stores_cleared);
EXPECT_EQ(cleared_paths.size(), 1u);
EXPECT_EQ(cleared_paths.at(0).as_string(), store_path);
}
TEST_CASE(storage_indexed_database_remove_all_error)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
session->delegate.fail_clear_indexed_database_object_store = true;
auto indexed_database_actor = get_indexed_database_actor(client);
auto store_path = indexed_database_path("fixtures (default)"sv, "people"sv);
JsonObject remove_all;
remove_all.set("to"sv, indexed_database_actor);
remove_all.set("type"sv, "removeAll"sv);
remove_all.set("host"sv, "https://example.test"sv);
remove_all.set("name"sv, store_path);
auto response = client.request(move(remove_all));
EXPECT_EQ(response.get_string("error"sv).value(), "indexedDBInspectionFailed"sv);
EXPECT_EQ(response.get_string("message"sv).value(), "IndexedDB operation failed"sv);
EXPECT_EQ(session->delegate.clear_indexed_database_object_store_call_count, 1u);
EXPECT(!client.has_pending_message());
}
TEST_CASE(storage_indexed_database_remove_item)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
auto indexed_database_actor = get_indexed_database_actor(client);
JsonArray record_path_array;
record_path_array.must_append("fixtures (default)"sv);
record_path_array.must_append("people"sv);
record_path_array.must_append(1);
auto record_path = record_path_array.serialized();
JsonObject remove_item;
remove_item.set("to"sv, indexed_database_actor);
remove_item.set("type"sv, "removeItem"sv);
remove_item.set("host"sv, "https://example.test"sv);
remove_item.set("name"sv, record_path);
EXPECT_EQ(client.request(move(remove_item)).get_string("from"sv).value(), indexed_database_actor);
EXPECT_EQ(session->delegate.delete_indexed_database_record_call_count, 1u);
EXPECT_EQ(session->delegate.last_indexed_database_host.value(), "https://example.test"sv);
EXPECT_EQ(session->delegate.last_indexed_database_name.value(), record_path);
auto stores_update = read_packet_with_type(client, "storesUpdate"sv);
EXPECT_EQ(stores_update.get_string("from"sv).value(), indexed_database_actor);
auto deleted_paths = get_indexed_database_update_paths(stores_update, "deleted"sv);
EXPECT_EQ(deleted_paths.size(), 1u);
EXPECT_EQ(deleted_paths.at(0).as_string(), record_path);
}
TEST_CASE(storage_indexed_database_remove_item_error)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
session->delegate.fail_delete_indexed_database_record = true;
auto indexed_database_actor = get_indexed_database_actor(client);
JsonArray record_path_array;
record_path_array.must_append("fixtures (default)"sv);
record_path_array.must_append("people"sv);
record_path_array.must_append(1);
auto record_path = record_path_array.serialized();
JsonObject remove_item;
remove_item.set("to"sv, indexed_database_actor);
remove_item.set("type"sv, "removeItem"sv);
remove_item.set("host"sv, "https://example.test"sv);
remove_item.set("name"sv, record_path);
auto response = client.request(move(remove_item));
EXPECT_EQ(response.get_string("error"sv).value(), "indexedDBInspectionFailed"sv);
EXPECT_EQ(response.get_string("message"sv).value(), "IndexedDB operation failed"sv);
EXPECT_EQ(session->delegate.delete_indexed_database_record_call_count, 1u);
EXPECT(!client.has_pending_message());
}
TEST_CASE(storage_cookie_store_objects)
{
auto session = create_session();