From ddb4bdadd779096ff3f07855b8b2d00a1d211b20 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 10 Jun 2026 16:15:21 +0100 Subject: [PATCH] LibDevTools: Populate DevTools Web Storage Firefox exposes localStorage and sessionStorage through the same storage actor protocol used by cookies. Add matching resources for the current tab so the Storage panel can list key/value pairs. Read the values through WebContent rather than directly from the browser process. Session storage lives in LibWeb, and using the same path for both stores keeps the actor independent of the backing store. --- Libraries/LibDevTools/Actors/StorageActor.cpp | 204 ++++++++++++++++++ Libraries/LibDevTools/Actors/StorageActor.h | 42 ++++ Libraries/LibDevTools/Actors/WatcherActor.cpp | 59 ++++- Libraries/LibDevTools/Actors/WatcherActor.h | 7 + Libraries/LibDevTools/CMakeLists.txt | 1 + Libraries/LibDevTools/DevToolsDelegate.h | 9 + Libraries/LibDevTools/Forward.h | 1 + Libraries/LibWebView/Application.cpp | 15 ++ Libraries/LibWebView/Application.h | 1 + Libraries/LibWebView/ViewImplementation.cpp | 5 + Libraries/LibWebView/ViewImplementation.h | 2 + Libraries/LibWebView/WebContentClient.cpp | 33 +++ Libraries/LibWebView/WebContentClient.h | 1 + Services/WebContent/ConnectionFromClient.cpp | 46 ++++ Services/WebContent/ConnectionFromClient.h | 1 + Services/WebContent/WebContentClient.ipc | 1 + Services/WebContent/WebContentServer.ipc | 2 + Tests/LibDevTools/TestDevToolsProtocol.cpp | 152 +++++++++++++ 18 files changed, 579 insertions(+), 3 deletions(-) create mode 100644 Libraries/LibDevTools/Actors/StorageActor.cpp create mode 100644 Libraries/LibDevTools/Actors/StorageActor.h diff --git a/Libraries/LibDevTools/Actors/StorageActor.cpp b/Libraries/LibDevTools/Actors/StorageActor.cpp new file mode 100644 index 0000000000..c4e8fad0ba --- /dev/null +++ b/Libraries/LibDevTools/Actors/StorageActor.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +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::create(DevToolsServer& devtools, String name, WeakPtr 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 tab, Web::StorageAPI::StorageEndpointType storage_endpoint) + : Actor(devtools, move(name)) + , m_tab(move(tab)) + , m_storage_endpoint(storage_endpoint) +{ +} + +StorageActor::~StorageActor() = default; + +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 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(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 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 {}); + return; + } + + devtools().delegate().inspect_storage( + tab->description(), + m_storage_endpoint, + weak_callback(*this, [message, requested_host, requested_names, options](auto& self, ErrorOr> storage_items) { + self.send_store_objects(message, requested_host, requested_names, options, move(storage_items)); + })); +} + +void StorageActor::send_store_objects(Message const& message, Optional requested_host, Optional requested_names, JsonObject options, ErrorOr> 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 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("offset"sv).value_or(0); + auto size = options.get_integer("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)); +} + +} diff --git a/Libraries/LibDevTools/Actors/StorageActor.h b/Libraries/LibDevTools/Actors/StorageActor.h new file mode 100644 index 0000000000..0c5d82fcb8 --- /dev/null +++ b/Libraries/LibDevTools/Actors/StorageActor.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace DevTools { + +class DEVTOOLS_API StorageActor final : public Actor { +public: + static constexpr auto base_name = "storage"sv; + + static NonnullRefPtr create(DevToolsServer&, String name, WeakPtr, Web::StorageAPI::StorageEndpointType); + virtual ~StorageActor() override; + + StringView resource_type() const; + StringView resource_key() const; + Optional host() const; + JsonObject serialize_storage() const; + +private: + StorageActor(DevToolsServer&, String name, WeakPtr, Web::StorageAPI::StorageEndpointType); + + virtual void handle_message(Message const&) override; + + void get_fields(Message const&); + void get_store_objects(Message const&); + void send_store_objects(Message const&, Optional requested_host, Optional requested_names, JsonObject options, ErrorOr>); + + WeakPtr m_tab; + Web::StorageAPI::StorageEndpointType m_storage_endpoint; +}; + +} diff --git a/Libraries/LibDevTools/Actors/WatcherActor.cpp b/Libraries/LibDevTools/Actors/WatcherActor.cpp index a721e832ec..7893c6ccc7 100644 --- a/Libraries/LibDevTools/Actors/WatcherActor.cpp +++ b/Libraries/LibDevTools/Actors/WatcherActor.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -91,11 +92,13 @@ void WatcherActor::handle_message(Message const& message) return; bool should_send_cookie_resources = false; + bool should_send_local_storage_resources = false; + bool should_send_session_storage_resources = false; if constexpr (DEVTOOLS_DEBUG) { for (auto const& resource_type : resource_types->values()) { if (!resource_type.is_string()) continue; - if (resource_type.as_string() != "console-message"sv && resource_type.as_string() != "cookies"sv) + if (!first_is_one_of(resource_type.as_string(), "console-message"sv, "cookies"sv, "local-storage"sv, "session-storage"sv)) dbgln("Unrecognized `watchResources` resource type: '{}'", resource_type.as_string()); } } @@ -105,12 +108,22 @@ void WatcherActor::handle_message(Message const& message) if (resource_type.as_string() == "cookies"sv) { m_is_watching_cookie_resources = true; should_send_cookie_resources = true; + } else if (resource_type.as_string() == "local-storage"sv) { + m_is_watching_local_storage_resources = true; + should_send_local_storage_resources = true; + } else if (resource_type.as_string() == "session-storage"sv) { + m_is_watching_session_storage_resources = true; + should_send_session_storage_resources = true; } } send_response(message, move(response)); if (should_send_cookie_resources) send_cookies_resource_available_message(); + if (should_send_local_storage_resources) + send_storage_resource_available_message(local_storage_actor()); + if (should_send_session_storage_resources) + send_storage_resource_available_message(session_storage_actor()); return; } @@ -155,13 +168,13 @@ JsonObject WatcherActor::serialize_description() const resources.set("jstracer-state"sv, false); resources.set("jstracer-trace"sv, false); resources.set("last-private-context-exit"sv, false); - resources.set("local-storage"sv, false); + resources.set("local-storage"sv, true); resources.set("network-event"sv, true); resources.set("network-event-stacktrace"sv, false); resources.set("platform-message"sv, false); resources.set("reflow"sv, false); resources.set("server-sent-event"sv, false); - resources.set("session-storage"sv, false); + resources.set("session-storage"sv, true); resources.set("source"sv, false); resources.set("stylesheet"sv, false); resources.set("thread-state"sv, false); @@ -214,6 +227,10 @@ void WatcherActor::switch_frame_target(FrameActor& previous_target, String const target.set_pending_navigation_document_events_after_target_switch(url, title); if (m_is_watching_cookie_resources) send_cookies_resource_available_message(); + if (m_is_watching_local_storage_resources) + send_storage_resource_available_message(local_storage_actor()); + if (m_is_watching_session_storage_resources) + send_storage_resource_available_message(session_storage_actor()); } void WatcherActor::send_frame_target_available_message(FrameActor& target) @@ -264,4 +281,40 @@ void WatcherActor::send_cookies_resource_available_message() send_message(move(message)); } +StorageActor& WatcherActor::local_storage_actor() +{ + if (auto storage = m_local_storage.strong_ref()) + return *storage; + + m_local_storage = devtools().register_actor(m_tab, Web::StorageAPI::StorageEndpointType::LocalStorage); + return *m_local_storage.strong_ref(); +} + +StorageActor& WatcherActor::session_storage_actor() +{ + if (auto storage = m_session_storage.strong_ref()) + return *storage; + + m_session_storage = devtools().register_actor(m_tab, Web::StorageAPI::StorageEndpointType::SessionStorage); + return *m_session_storage.strong_ref(); +} + +void WatcherActor::send_storage_resource_available_message(StorageActor& storage) +{ + JsonArray resources; + resources.must_append(storage.serialize_storage()); + + JsonArray typed_resources; + typed_resources.must_append(storage.resource_type()); + typed_resources.must_append(move(resources)); + + JsonArray array; + array.must_append(move(typed_resources)); + + JsonObject message; + message.set("type"sv, "resources-available-array"sv); + message.set("array"sv, move(array)); + send_message(move(message)); +} + } diff --git a/Libraries/LibDevTools/Actors/WatcherActor.h b/Libraries/LibDevTools/Actors/WatcherActor.h index b800bf84e0..6606b2095f 100644 --- a/Libraries/LibDevTools/Actors/WatcherActor.h +++ b/Libraries/LibDevTools/Actors/WatcherActor.h @@ -33,15 +33,22 @@ private: void send_frame_target_destroyed_message(FrameActor&); CookiesActor& cookies_actor(); void send_cookies_resource_available_message(); + StorageActor& local_storage_actor(); + StorageActor& session_storage_actor(); + void send_storage_resource_available_message(StorageActor&); WeakPtr m_tab; WeakPtr m_target; WeakPtr m_cookies; + WeakPtr m_local_storage; + WeakPtr m_session_storage; WeakPtr m_target_configuration; WeakPtr m_thread_configuration; WeakPtr m_network_parent; bool m_is_watching_frame_targets { false }; bool m_is_watching_cookie_resources { false }; + bool m_is_watching_local_storage_resources { false }; + bool m_is_watching_session_storage_resources { false }; }; } diff --git a/Libraries/LibDevTools/CMakeLists.txt b/Libraries/LibDevTools/CMakeLists.txt index 3af8d65065..6176b59e43 100644 --- a/Libraries/LibDevTools/CMakeLists.txt +++ b/Libraries/LibDevTools/CMakeLists.txt @@ -21,6 +21,7 @@ set(SOURCES Actors/RootActor.cpp Actors/StyleRuleActor.cpp Actors/StyleSheetsActor.cpp + Actors/StorageActor.cpp Actors/TabActor.cpp Actors/TargetConfigurationActor.cpp Actors/ThreadActor.cpp diff --git a/Libraries/LibDevTools/DevToolsDelegate.h b/Libraries/LibDevTools/DevToolsDelegate.h index 834127dc8b..4955bca8f1 100644 --- a/Libraries/LibDevTools/DevToolsDelegate.h +++ b/Libraries/LibDevTools/DevToolsDelegate.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -45,6 +46,14 @@ public: virtual void listen_for_host_cookie_changes(TabDescription const&, OnHostCookieChange) const { } virtual void stop_listening_for_host_cookie_changes(TabDescription const&) const { } + struct StorageItem { + String name; + String value; + }; + + using OnStorageItemsReceived = Function>)>; + virtual void inspect_storage(TabDescription const&, Web::StorageAPI::StorageEndpointType, OnStorageItemsReceived) const { } + using OnTabInspectionComplete = Function)>; virtual void inspect_tab(TabDescription const&, OnTabInspectionComplete) const { } diff --git a/Libraries/LibDevTools/Forward.h b/Libraries/LibDevTools/Forward.h index e1c081cab5..ece05d1db5 100644 --- a/Libraries/LibDevTools/Forward.h +++ b/Libraries/LibDevTools/Forward.h @@ -35,6 +35,7 @@ class ProcessActor; class RootActor; class StyleRuleActor; class StyleSheetsActor; +class StorageActor; class TabActor; class TargetConfigurationActor; class ThreadActor; diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index f2225655fa..2307c21a81 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -1867,6 +1867,21 @@ void Application::stop_listening_for_host_cookie_changes(DevTools::TabDescriptio view->stop_listening_for_host_cookie_changes(); } +void Application::inspect_storage(DevTools::TabDescription const& description, Web::StorageAPI::StorageEndpointType storage_endpoint, OnStorageItemsReceived on_complete) const +{ + static u64 next_request_id = 0; + + auto view = ViewImplementation::find_view_by_id(description.id); + if (!view.has_value()) { + on_complete(Error::from_string_literal("Unable to locate tab")); + return; + } + + auto request_id = next_request_id++; + view->on_received_storage_items.set(request_id, move(on_complete)); + view->inspect_storage(storage_endpoint, request_id); +} + void Application::inspect_tab(DevTools::TabDescription const& description, OnTabInspectionComplete on_complete) const { auto view = ViewImplementation::find_view_by_id(description.id); diff --git a/Libraries/LibWebView/Application.h b/Libraries/LibWebView/Application.h index 1fb30dfc70..9cb545308c 100644 --- a/Libraries/LibWebView/Application.h +++ b/Libraries/LibWebView/Application.h @@ -274,6 +274,7 @@ private: virtual void delete_cookies(DevTools::TabDescription const&, Vector) const override; virtual void listen_for_host_cookie_changes(DevTools::TabDescription const&, OnHostCookieChange) const override; virtual void stop_listening_for_host_cookie_changes(DevTools::TabDescription const&) const override; + virtual void inspect_storage(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType, OnStorageItemsReceived) const override; virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete) const override; virtual void inspect_accessibility_tree(DevTools::TabDescription const&, OnAccessibilityTreeInspectionComplete) const override; virtual void listen_for_dom_properties(DevTools::TabDescription const&, OnDOMNodePropertiesReceived) const override; diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index f57d3bff26..e94399b465 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -477,6 +477,11 @@ void ViewImplementation::inspect_dom_tree() client().async_inspect_dom_tree(page_id()); } +void ViewImplementation::inspect_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id) +{ + client().async_inspect_storage(page_id(), storage_endpoint, request_id); +} + void ViewImplementation::inspect_accessibility_tree() { client().async_inspect_accessibility_tree(page_id()); diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 54f4e1ba9b..0b10f43b92 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -122,6 +122,7 @@ public: void get_source(); void inspect_dom_tree(); + void inspect_storage(Web::StorageAPI::StorageEndpointType, u64 request_id); void inspect_accessibility_tree(); void get_hovered_node_id(); void start_node_picker(DevTools::DevToolsDelegate::OnNodePickerEvent); @@ -258,6 +259,7 @@ public: Function on_request_dismiss_dialog; Function on_received_dom_tree; Function on_received_dom_node_properties; + HashMap>)>> on_received_storage_items; Function on_received_grid_layouts; Function)> on_received_current_grid; Function)> on_received_current_flexbox; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index ab82198e30..56269d4818 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -680,6 +680,39 @@ void WebContentClient::did_inspect_dom_tree(u64 page_id, String dom_tree) } } +static ErrorOr> parse_storage_items(String const& storage_items) +{ + auto parsed_items = JsonValue::from_string(storage_items); + if (parsed_items.is_error()) + return Error::from_string_literal("Unable to parse storage items"); + + if (!parsed_items.value().is_array()) + return Error::from_string_literal("Expected storage items to be an array"); + + Vector items; + parsed_items.value().as_array().for_each([&](auto const& item) { + if (!item.is_object()) + return; + + auto name = item.as_object().get_string("name"sv); + auto value = item.as_object().get_string("value"sv); + if (!name.has_value() || !value.has_value()) + return; + + items.append({ name.release_value(), value.release_value() }); + }); + return items; +} + +void WebContentClient::did_inspect_storage(u64 page_id, u64 request_id, String storage_items) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) { + auto handler = view->on_received_storage_items.take(request_id); + if (handler.has_value()) + (*handler)(parse_storage_items(storage_items)); + } +} + void WebContentClient::did_inspect_dom_node(u64 page_id, DOMNodeProperties properties) { if (auto view = view_for_page_id(page_id); view.has_value()) { diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index 7426a9ab5a..8f14df456e 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -117,6 +117,7 @@ private: virtual void did_request_media_context_menu(u64 page_id, Gfx::IntPoint, ByteString, unsigned, Web::Page::MediaContextMenu) override; virtual void did_get_source(u64 page_id, URL::URL, URL::URL, String) override; virtual void did_inspect_dom_tree(u64 page_id, String) override; + virtual void did_inspect_storage(u64 page_id, u64 request_id, String) override; virtual void did_inspect_dom_node(u64 page_id, DOMNodeProperties) override; virtual void did_inspect_grid_layouts(u64 page_id, String) override; virtual void did_inspect_current_grid(u64 page_id, String) override; diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index 541804e0d2..a6fc4941c8 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -558,6 +558,52 @@ void ConnectionFromClient::inspect_dom_tree(u64 page_id) } } +void ConnectionFromClient::inspect_storage(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id) +{ + auto page = this->page(page_id); + if (!page.has_value()) { + async_did_inspect_storage(page_id, request_id, "[]"_string); + return; + } + + auto* document = page->page().top_level_browsing_context().active_document(); + if (!document || !document->window()) { + async_did_inspect_storage(page_id, request_id, "[]"_string); + return; + } + + Web::WebIDL::ExceptionOr> storage_or_error = [&]() -> Web::WebIDL::ExceptionOr> { + if (storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage) + return document->window()->local_storage(); + VERIFY(storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage); + return document->window()->session_storage(); + }(); + + if (storage_or_error.is_error()) { + async_did_inspect_storage(page_id, request_id, "[]"_string); + return; + } + + auto storage = storage_or_error.release_value(); + JsonArray storage_items; + for (auto i = 0uz; i < storage->length(); ++i) { + auto name = storage->key(i); + if (!name.has_value()) + continue; + + auto value = storage->get_item(*name); + if (!value.has_value()) + continue; + + JsonObject item; + item.set("name"sv, name.release_value()); + item.set("value"sv, value.release_value()); + storage_items.must_append(move(item)); + } + + async_did_inspect_storage(page_id, request_id, storage_items.serialized()); +} + void ConnectionFromClient::inspect_dom_node(u64 page_id, WebView::DOMNodeProperties::Type property_type, Web::UniqueNodeID node_id, Optional pseudo_element, JsonValue options_value) { auto page = this->page(page_id); diff --git a/Services/WebContent/ConnectionFromClient.h b/Services/WebContent/ConnectionFromClient.h index d22f5499e5..443c7f3f46 100644 --- a/Services/WebContent/ConnectionFromClient.h +++ b/Services/WebContent/ConnectionFromClient.h @@ -92,6 +92,7 @@ private: virtual void debug_request(u64 page_id, ByteString, ByteString) override; virtual void get_source(u64 page_id) override; virtual void inspect_dom_tree(u64 page_id) override; + virtual void inspect_storage(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id) override; virtual void inspect_dom_node(u64 page_id, WebView::DOMNodeProperties::Type, Web::UniqueNodeID node_id, Optional pseudo_element, JsonValue options) override; virtual void inspect_grid_layouts(u64 page_id, Web::UniqueNodeID root_node_id) override; virtual void inspect_current_grid(u64 page_id, Web::UniqueNodeID node_id) override; diff --git a/Services/WebContent/WebContentClient.ipc b/Services/WebContent/WebContentClient.ipc index 5e8e32943a..c547beb878 100644 --- a/Services/WebContent/WebContentClient.ipc +++ b/Services/WebContent/WebContentClient.ipc @@ -67,6 +67,7 @@ endpoint WebContentClient did_get_source(u64 page_id, URL::URL url, URL::URL base_url, String source) =| did_inspect_dom_tree(u64 page_id, String dom_tree) =| + did_inspect_storage(u64 page_id, u64 request_id, String storage_items) =| did_inspect_dom_node(u64 page_id, WebView::DOMNodeProperties properties) =| did_inspect_grid_layouts(u64 page_id, String grid_layouts) =| did_inspect_current_grid(u64 page_id, String grid_layout) =| diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index 6bc6614a0a..471e3647c1 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,7 @@ endpoint WebContentServer debug_request(u64 page_id, ByteString request, ByteString argument) =| get_source(u64 page_id) =| inspect_dom_tree(u64 page_id) =| + inspect_storage(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id) =| inspect_dom_node(u64 page_id, WebView::DOMNodeProperties::Type property_type, Web::UniqueNodeID node_id, Optional pseudo_element, JsonValue options) =| inspect_grid_layouts(u64 page_id, Web::UniqueNodeID root_node_id) =| inspect_current_grid(u64 page_id, Web::UniqueNodeID node_id) =| diff --git a/Tests/LibDevTools/TestDevToolsProtocol.cpp b/Tests/LibDevTools/TestDevToolsProtocol.cpp index 7b1713e013..2e17ef5b28 100644 --- a/Tests/LibDevTools/TestDevToolsProtocol.cpp +++ b/Tests/LibDevTools/TestDevToolsProtocol.cpp @@ -653,6 +653,18 @@ public: on_host_cookie_change = nullptr; } + virtual void inspect_storage(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType storage_endpoint, OnStorageItemsReceived callback) const override + { + ++inspect_storage_call_count; + if (storage_endpoint == Web::StorageAPI::StorageEndpointType::LocalStorage) { + callback(fixture_local_storage_items); + return; + } + + VERIFY(storage_endpoint == Web::StorageAPI::StorageEndpointType::SessionStorage); + callback(fixture_session_storage_items); + } + virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete callback) const override { ++inspect_tab_call_count; @@ -1071,6 +1083,8 @@ public: mutable bool use_navigation_dom_tree { false }; mutable Vector fixture_cookies; + mutable Vector fixture_local_storage_items; + mutable Vector fixture_session_storage_items; mutable size_t inspect_tab_call_count { 0 }; mutable size_t cookies_call_count { 0 }; @@ -1078,6 +1092,7 @@ public: mutable size_t delete_cookies_call_count { 0 }; mutable size_t listen_for_cookie_changes_call_count { 0 }; mutable size_t stop_listening_for_cookie_changes_call_count { 0 }; + mutable size_t inspect_storage_call_count { 0 }; 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 }; @@ -1412,6 +1427,36 @@ static JsonObject get_cookie_store_objects(ProtocolClient& client, StringView co return client.request(move(get_store_objects)); } +static String get_storage_actor(ProtocolClient& client, StringView resource_type) +{ + auto tab_actor = actor_from(get_tab(client), "actor"sv); + auto watcher_actor = actor_from(client.request(tab_actor, "getWatcher"sv), "actor"sv); + + JsonObject watch_resources; + watch_resources.set("to"sv, watcher_actor); + watch_resources.set("type"sv, "watchResources"sv); + JsonArray resource_types; + resource_types.must_append(resource_type); + watch_resources.set("resourceTypes"sv, move(resource_types)); + EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor); + + return actor_from(read_resource(client, resource_type), "actor"sv); +} + +static JsonObject get_storage_store_objects(ProtocolClient& client, StringView storage_actor, StringView host = "https://example.test"sv, Optional names = {}) +{ + JsonObject get_store_objects; + get_store_objects.set("to"sv, storage_actor); + get_store_objects.set("type"sv, "getStoreObjects"sv); + get_store_objects.set("host"sv, host); + if (names.has_value()) + get_store_objects.set("names"sv, names.release_value()); + else + get_store_objects.set("names"sv, JsonValue {}); + get_store_objects.set("options"sv, JsonObject {}); + return client.request(move(get_store_objects)); +} + static JsonArray get_cookie_update_keys(JsonObject const& stores_update, StringView update_type, StringView host = "https://example.test"sv) { return stores_update.get_object("data"sv) @@ -1702,6 +1747,113 @@ TEST_CASE(storage_cookie_resource) EXPECT(objects.get_array("data"sv)->is_empty()); } +TEST_CASE(storage_web_storage_resources) +{ + auto session = create_session(); + auto& client = *session->client; + (void)client.read_message(); + + auto tab_actor = actor_from(get_tab(client), "actor"sv); + auto watcher_response = client.request(tab_actor, "getWatcher"sv); + auto watcher_actor = actor_from(watcher_response, "actor"sv); + auto resources = watcher_response.get_object("traits"sv)->get_object("resources"sv).release_value(); + EXPECT(resources.get_bool("local-storage"sv).value()); + EXPECT(resources.get_bool("session-storage"sv).value()); + + JsonObject watch_resources; + watch_resources.set("to"sv, watcher_actor); + watch_resources.set("type"sv, "watchResources"sv); + JsonArray resource_types; + resource_types.must_append("local-storage"sv); + resource_types.must_append("session-storage"sv); + watch_resources.set("resourceTypes"sv, move(resource_types)); + EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor); + + auto local_storage_resource = read_resource(client, "local-storage"sv); + EXPECT_EQ(local_storage_resource.get_string("resourceKey"sv).value(), "localStorage"sv); + EXPECT_EQ(local_storage_resource.get_integer("browsingContextID"sv).value(), 1u); + EXPECT_EQ(local_storage_resource.get_integer("innerWindowId"sv).value(), 1u); + EXPECT_EQ(local_storage_resource.get_string("resourceId"sv).value(), "localStorage-1"sv); + EXPECT(local_storage_resource.get_object("hosts"sv)->has_array("https://example.test"sv)); + auto local_traits = local_storage_resource.get_object("traits"sv).release_value(); + EXPECT(local_traits.get_bool("supportsAddItem"sv).value()); + EXPECT(local_traits.get_bool("supportsRemoveAll"sv).value()); + EXPECT(!local_traits.get_bool("supportsRemoveAllSessionCookies"sv).value()); + EXPECT(local_traits.get_bool("supportsRemoveItem"sv).value()); + + auto local_storage_actor = actor_from(local_storage_resource, "actor"sv); + auto fields = client.request(local_storage_actor, "getFields"sv).get_array("value"sv).release_value(); + EXPECT_EQ(fields.size(), 2u); + EXPECT_EQ(fields.at(0).as_object().get_string("name"sv).value(), "name"sv); + EXPECT(fields.at(0).as_object().get_bool("editable"sv).value()); + EXPECT_EQ(fields.at(1).as_object().get_string("name"sv).value(), "value"sv); + EXPECT(fields.at(1).as_object().get_bool("editable"sv).value()); + + auto session_storage_resource = read_resource(client, "session-storage"sv); + EXPECT_EQ(session_storage_resource.get_string("resourceKey"sv).value(), "sessionStorage"sv); + EXPECT_EQ(session_storage_resource.get_string("resourceId"sv).value(), "sessionStorage-1"sv); + EXPECT(session_storage_resource.get_object("hosts"sv)->has_array("https://example.test"sv)); +} + +TEST_CASE(storage_web_storage_store_objects) +{ + auto session = create_session(); + auto& client = *session->client; + (void)client.read_message(); + + session->delegate.fixture_local_storage_items.append({ "beta"_string, "two"_string }); + session->delegate.fixture_local_storage_items.append({ "alpha"_string, "one"_string }); + session->delegate.fixture_session_storage_items.append({ "session-key"_string, "session-value"_string }); + + auto local_storage_actor = get_storage_actor(client, "local-storage"sv); + auto objects = get_storage_store_objects(client, local_storage_actor); + EXPECT_EQ(session->delegate.inspect_storage_call_count, 1u); + EXPECT_EQ(objects.get_integer("offset"sv).value(), 0u); + EXPECT_EQ(objects.get_integer("total"sv).value(), 2u); + + auto data = objects.get_array("data"sv).release_value(); + EXPECT_EQ(data.size(), 2u); + EXPECT_EQ(data.at(0).as_object().get_string("name"sv).value(), "alpha"sv); + EXPECT_EQ(data.at(0).as_object().get_string("value"sv).value(), "one"sv); + EXPECT_EQ(data.at(1).as_object().get_string("name"sv).value(), "beta"sv); + EXPECT_EQ(data.at(1).as_object().get_string("value"sv).value(), "two"sv); + + JsonArray names; + names.must_append("beta"sv); + auto filtered_objects = get_storage_store_objects(client, local_storage_actor, "https://example.test"sv, move(names)); + EXPECT_EQ(filtered_objects.get_integer("total"sv).value(), 1u); + auto filtered_data = filtered_objects.get_array("data"sv).release_value(); + EXPECT_EQ(filtered_data.size(), 1u); + EXPECT_EQ(filtered_data.at(0).as_object().get_string("name"sv).value(), "beta"sv); + + JsonObject paginated_request; + paginated_request.set("to"sv, local_storage_actor); + paginated_request.set("type"sv, "getStoreObjects"sv); + paginated_request.set("host"sv, "https://example.test"sv); + paginated_request.set("names"sv, JsonValue {}); + JsonObject options; + options.set("offset"sv, 1); + options.set("size"sv, 1); + paginated_request.set("options"sv, move(options)); + auto paginated_objects = client.request(move(paginated_request)); + EXPECT_EQ(paginated_objects.get_integer("offset"sv).value(), 1u); + EXPECT_EQ(paginated_objects.get_integer("total"sv).value(), 2u); + auto paginated_data = paginated_objects.get_array("data"sv).release_value(); + EXPECT_EQ(paginated_data.size(), 1u); + EXPECT_EQ(paginated_data.at(0).as_object().get_string("name"sv).value(), "beta"sv); + + auto session_storage_actor = get_storage_actor(client, "session-storage"sv); + objects = get_storage_store_objects(client, session_storage_actor); + EXPECT_EQ(objects.get_integer("total"sv).value(), 1u); + data = objects.get_array("data"sv).release_value(); + EXPECT_EQ(data.at(0).as_object().get_string("name"sv).value(), "session-key"sv); + EXPECT_EQ(data.at(0).as_object().get_string("value"sv).value(), "session-value"sv); + + objects = get_storage_store_objects(client, session_storage_actor, "https://other.test"sv); + EXPECT_EQ(objects.get_integer("total"sv).value(), 0u); + EXPECT(objects.get_array("data"sv)->is_empty()); +} + TEST_CASE(storage_cookie_store_objects) { auto session = create_session();