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();