From fdfe806e68a8ea40aaabb687ea18e1b6a59d6a6a Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 11 Jun 2026 16:59:48 +0100 Subject: [PATCH] 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. --- .../LibDevTools/Actors/IndexedDBActor.cpp | 144 +++++++++++++ Libraries/LibDevTools/Actors/IndexedDBActor.h | 5 + Libraries/LibDevTools/DevToolsDelegate.h | 3 + Libraries/LibWebView/ViewImplementation.cpp | 21 ++ Libraries/LibWebView/ViewImplementation.h | 3 + Services/WebContent/ConnectionFromClient.h | 3 + Services/WebContent/WebContentServer.ipc | 3 + Tests/LibDevTools/TestDevToolsProtocol.cpp | 194 ++++++++++++++++++ 8 files changed, 376 insertions(+) diff --git a/Libraries/LibDevTools/Actors/IndexedDBActor.cpp b/Libraries/LibDevTools/Actors/IndexedDBActor.cpp index ed7efd7459..5e521b9d40 100644 --- a/Libraries/LibDevTools/Actors/IndexedDBActor.cpp +++ b/Libraries/LibDevTools/Actors/IndexedDBActor.cpp @@ -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(message, "host"sv); + auto name = get_required_parameter(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(), message_id = message.id, host = *host, name = *name](ErrorOr 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(message, "host"sv); + auto name = get_required_parameter(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(), message_id = message.id, host = *host, name = *name](ErrorOr 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(message, "host"sv); + auto name = get_required_parameter(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(), message_id = message.id, host = *host, name = *name](ErrorOr 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; diff --git a/Libraries/LibDevTools/Actors/IndexedDBActor.h b/Libraries/LibDevTools/Actors/IndexedDBActor.h index 725103d841..5e908ca023 100644 --- a/Libraries/LibDevTools/Actors/IndexedDBActor.h +++ b/Libraries/LibDevTools/Actors/IndexedDBActor.h @@ -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 m_tab; u64 m_indexed_database_change_listener_id { 0 }; diff --git a/Libraries/LibDevTools/DevToolsDelegate.h b/Libraries/LibDevTools/DevToolsDelegate.h index da0f6adb39..a14e407c16 100644 --- a/Libraries/LibDevTools/DevToolsDelegate.h +++ b/Libraries/LibDevTools/DevToolsDelegate.h @@ -78,6 +78,9 @@ public: using OnIndexedDBInspectionComplete = Function)>; virtual void inspect_indexed_database_storage(TabDescription const&, OnIndexedDBInspectionComplete) const { } virtual void inspect_indexed_database_objects(TabDescription const&, String const&, Optional, 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; virtual u64 add_indexed_database_change_listener(TabDescription const&, OnIndexedDatabaseChange) const { return 0; } virtual void remove_indexed_database_change_listener(TabDescription const&, u64) const { } diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index faa089da8c..6cc14bed0d 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -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); diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 8fe866a6a5..9b102809c1 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -152,6 +152,9 @@ public: void inspect_indexed_database_storage(DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete); void inspect_indexed_database_objects(String const& host, Optional 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(); diff --git a/Services/WebContent/ConnectionFromClient.h b/Services/WebContent/ConnectionFromClient.h index aab2cee1d5..7c7b27d808 100644 --- a/Services/WebContent/ConnectionFromClient.h +++ b/Services/WebContent/ConnectionFromClient.h @@ -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 pseudo_element) override; virtual void highlight_flexbox(u64 page_id, Web::UniqueNodeID node_id, JsonValue options) override; diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index 0e05d9ff3c..9b519cb6d3 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -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 pseudo_element) =| highlight_flexbox(u64 page_id, Web::UniqueNodeID node_id, JsonValue options) =| diff --git a/Tests/LibDevTools/TestDevToolsProtocol.cpp b/Tests/LibDevTools/TestDevToolsProtocol.cpp index 458d82c62a..4ffa4c9a00 100644 --- a/Tests/LibDevTools/TestDevToolsProtocol.cpp +++ b/Tests/LibDevTools/TestDevToolsProtocol.cpp @@ -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 last_reload_bypass_cache; mutable Optional last_history_delta; mutable Optional last_indexed_database_host; + mutable Optional 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();