From cb47dbfc7ad34b1a8a0e0a4ebeb0d13ab4906a3b Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 17 Jun 2026 12:39:37 +0100 Subject: [PATCH] LibDevTools+LibWeb: Show IndexedDB in DevTools Firefox asks the storage watcher for an indexed-db resource before it shows IndexedDB entries in the Storage panel. Add an IndexedDB actor and serialize the live LibWeb database registry on demand, so WebContent can return the host tree and table rows without duplicating database state. Use the LibWeb inspection helpers to read IndexedDB internals, and keep the Firefox protocol shape in LibDevTools. WebContent only forwards the serialized response over the existing DevTools IPC path. --- .../LibDevTools/Actors/IndexedDBActor.cpp | 158 ++++++++++ Libraries/LibDevTools/Actors/IndexedDBActor.h | 37 +++ Libraries/LibDevTools/Actors/WatcherActor.cpp | 46 ++- Libraries/LibDevTools/Actors/WatcherActor.h | 4 + Libraries/LibDevTools/CMakeLists.txt | 2 + Libraries/LibDevTools/DevToolsDelegate.h | 4 + Libraries/LibDevTools/Forward.h | 1 + .../LibDevTools/IndexedDBSerialization.cpp | 228 +++++++++++++++ .../LibDevTools/IndexedDBSerialization.h | 23 ++ Libraries/LibDevTools/StorageHelpers.cpp | 13 +- Libraries/LibDevTools/StorageHelpers.h | 2 + Libraries/LibWebView/Application.cpp | 22 ++ Libraries/LibWebView/Application.h | 2 + Libraries/LibWebView/ViewImplementation.cpp | 31 ++ Libraries/LibWebView/ViewImplementation.h | 7 + Libraries/LibWebView/WebContentClient.cpp | 6 + Libraries/LibWebView/WebContentClient.h | 1 + Services/WebContent/CMakeLists.txt | 2 +- Services/WebContent/ConnectionFromClient.cpp | 88 ++++++ Services/WebContent/ConnectionFromClient.h | 2 + Services/WebContent/WebContentClient.ipc | 1 + Services/WebContent/WebContentServer.ipc | 2 + Tests/LibDevTools/TestDevToolsProtocol.cpp | 274 ++++++++++++++++++ 23 files changed, 949 insertions(+), 7 deletions(-) create mode 100644 Libraries/LibDevTools/Actors/IndexedDBActor.cpp create mode 100644 Libraries/LibDevTools/Actors/IndexedDBActor.h create mode 100644 Libraries/LibDevTools/IndexedDBSerialization.cpp create mode 100644 Libraries/LibDevTools/IndexedDBSerialization.h diff --git a/Libraries/LibDevTools/Actors/IndexedDBActor.cpp b/Libraries/LibDevTools/Actors/IndexedDBActor.cpp new file mode 100644 index 0000000000..3bcb3f81ea --- /dev/null +++ b/Libraries/LibDevTools/Actors/IndexedDBActor.cpp @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace DevTools { + +NonnullRefPtr IndexedDBActor::create(DevToolsServer& devtools, String name, WeakPtr tab) +{ + return adopt_ref(*new IndexedDBActor(devtools, move(name), move(tab))); +} + +IndexedDBActor::IndexedDBActor(DevToolsServer& devtools, String name, WeakPtr tab) + : Actor(devtools, move(name)) + , m_tab(move(tab)) +{ +} + +IndexedDBActor::~IndexedDBActor() = default; + +JsonObject IndexedDBActor::serialize_storage(JsonObject hosts) const +{ + if (hosts.is_empty()) { + if (auto tab = m_tab.strong_ref()) { + if (auto host = storage_host_for_url(tab->description().url); host.has_value()) + hosts.set(*host, JsonArray {}); + } + } + + JsonObject traits; + traits.set("supportsAddItem"sv, false); + 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("indexed-db-{}", tab->inner_window_id()))); + } + storage.set("hosts"sv, move(hosts)); + storage.set("resourceKey"sv, "indexedDB"sv); + storage.set("traits"sv, move(traits)); + return storage; +} + +void IndexedDBActor::get_storage_resource(Function callback) +{ + auto tab = m_tab.strong_ref(); + if (!tab) { + callback(serialize_storage({})); + return; + } + + devtools().delegate().inspect_indexed_database_storage(tab->description(), + [weak_self = make_weak_ptr(), callback = move(callback)](ErrorOr hosts_or_error) mutable { + auto self = weak_self.strong_ref(); + if (!self) + return; + + callback(self->serialize_storage(hosts_or_error.is_error() ? JsonObject {} : hosts_or_error.release_value())); + }); +} + +void IndexedDBActor::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 IndexedDBActor::get_fields(Message const& message) +{ + auto sub_type = message.data.get_string("subType"sv); + + JsonArray fields; + if (sub_type == "database"sv) { + fields.must_append(define_storage_field("objectStore"sv, StorageFieldType::Immutable)); + fields.must_append(define_storage_field("keyPath"sv, StorageFieldType::Immutable)); + fields.must_append(define_storage_field("autoIncrement"sv, StorageFieldType::Immutable)); + fields.must_append(define_storage_field("indexes"sv, StorageFieldType::Immutable)); + } else if (sub_type == "object store"sv) { + fields.must_append(define_storage_field("name"sv, StorageFieldType::Immutable)); + fields.must_append(define_storage_field("value"sv, StorageFieldType::Immutable)); + } else { + fields.must_append(define_storage_field("uniqueKey"sv, StorageFieldType::Private)); + fields.must_append(define_storage_field("db"sv, StorageFieldType::Immutable)); + fields.must_append(define_storage_field("storage"sv, StorageFieldType::Immutable)); + fields.must_append(define_storage_field("origin"sv, StorageFieldType::Immutable)); + fields.must_append(define_storage_field("version"sv, StorageFieldType::Immutable)); + fields.must_append(define_storage_field("objectStores"sv, StorageFieldType::Immutable)); + } + + JsonObject response; + response.set("value"sv, move(fields)); + send_response(message, move(response)); +} + +void IndexedDBActor::send_inspection_error(Message const& message, Error const& error) +{ + JsonObject response; + response.set("error"sv, "indexedDBInspectionFailed"sv); + response.set("message"sv, error.string_literal()); + send_response(message, move(response)); +} + +void IndexedDBActor::get_store_objects(Message const& message) +{ + auto host = get_required_parameter(message, "host"sv); + if (!host.has_value()) + return; + + auto tab = m_tab.strong_ref(); + if (!tab) { + send_inspection_error(message, Error::from_string_literal("Unable to locate tab")); + return; + } + + Optional names; + if (auto names_array = message.data.get_array("names"sv); names_array.has_value()) + names = *names_array; + auto options = message.data.get_object("options"sv).value_or({}); + + devtools().delegate().inspect_indexed_database_objects(tab->description(), *host, move(names), move(options), + [weak_self = make_weak_ptr(), message_id = message.id](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; + } + + self->send_response({ .id = message_id }, result.release_value()); + }); +} + +} diff --git a/Libraries/LibDevTools/Actors/IndexedDBActor.h b/Libraries/LibDevTools/Actors/IndexedDBActor.h new file mode 100644 index 0000000000..417714feda --- /dev/null +++ b/Libraries/LibDevTools/Actors/IndexedDBActor.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace DevTools { + +class DEVTOOLS_API IndexedDBActor final : public Actor { +public: + static constexpr auto base_name = "indexed-db"sv; + + static NonnullRefPtr create(DevToolsServer&, String name, WeakPtr); + virtual ~IndexedDBActor() override; + + void get_storage_resource(Function); + +private: + IndexedDBActor(DevToolsServer&, String name, WeakPtr); + + virtual void handle_message(Message const&) override; + + void get_fields(Message const&); + void get_store_objects(Message const&); + + JsonObject serialize_storage(JsonObject hosts) const; + void send_inspection_error(Message const&, Error const&); + + WeakPtr m_tab; +}; + +} diff --git a/Libraries/LibDevTools/Actors/WatcherActor.cpp b/Libraries/LibDevTools/Actors/WatcherActor.cpp index 7893c6ccc7..99bf6be918 100644 --- a/Libraries/LibDevTools/Actors/WatcherActor.cpp +++ b/Libraries/LibDevTools/Actors/WatcherActor.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -92,13 +93,14 @@ void WatcherActor::handle_message(Message const& message) return; bool should_send_cookie_resources = false; + bool should_send_indexed_db_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 (!first_is_one_of(resource_type.as_string(), "console-message"sv, "cookies"sv, "local-storage"sv, "session-storage"sv)) + if (!first_is_one_of(resource_type.as_string(), "console-message"sv, "cookies"sv, "indexed-db"sv, "local-storage"sv, "session-storage"sv)) dbgln("Unrecognized `watchResources` resource type: '{}'", resource_type.as_string()); } } @@ -108,6 +110,9 @@ 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() == "indexed-db"sv) { + m_is_watching_indexed_db_resources = true; + should_send_indexed_db_resources = true; } else if (resource_type.as_string() == "local-storage"sv) { m_is_watching_local_storage_resources = true; should_send_local_storage_resources = true; @@ -120,6 +125,8 @@ void WatcherActor::handle_message(Message const& message) send_response(message, move(response)); if (should_send_cookie_resources) send_cookies_resource_available_message(); + if (should_send_indexed_db_resources) + send_indexed_db_resource_available_message(); if (should_send_local_storage_resources) send_storage_resource_available_message(local_storage_actor()); if (should_send_session_storage_resources) @@ -164,7 +171,7 @@ JsonObject WatcherActor::serialize_description() const resources.set("document-event"sv, true); resources.set("error-message"sv, false); resources.set("extension-storage"sv, false); - resources.set("indexed-db"sv, false); + resources.set("indexed-db"sv, true); resources.set("jstracer-state"sv, false); resources.set("jstracer-trace"sv, false); resources.set("last-private-context-exit"sv, false); @@ -227,6 +234,8 @@ 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_indexed_db_resources) + send_indexed_db_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) @@ -263,6 +272,15 @@ CookiesActor& WatcherActor::cookies_actor() return *m_cookies.strong_ref(); } +IndexedDBActor& WatcherActor::indexed_db_actor() +{ + if (auto indexed_db = m_indexed_db.strong_ref()) + return *indexed_db; + + m_indexed_db = devtools().register_actor(m_tab); + return *m_indexed_db.strong_ref(); +} + void WatcherActor::send_cookies_resource_available_message() { JsonArray cookies; @@ -317,4 +335,28 @@ void WatcherActor::send_storage_resource_available_message(StorageActor& storage send_message(move(message)); } +void WatcherActor::send_indexed_db_resource_available_message() +{ + indexed_db_actor().get_storage_resource([weak_self = make_weak_ptr()](JsonObject indexed_db) mutable { + auto self = weak_self.strong_ref(); + if (!self) + return; + + JsonArray indexed_databases; + indexed_databases.must_append(move(indexed_db)); + + JsonArray indexed_database_resources; + indexed_database_resources.must_append("indexed-db"sv); + indexed_database_resources.must_append(move(indexed_databases)); + + JsonArray array; + array.must_append(move(indexed_database_resources)); + + JsonObject message; + message.set("type"sv, "resources-available-array"sv); + message.set("array"sv, move(array)); + self->send_message(move(message)); + }); +} + } diff --git a/Libraries/LibDevTools/Actors/WatcherActor.h b/Libraries/LibDevTools/Actors/WatcherActor.h index 6606b2095f..6bcd2690d7 100644 --- a/Libraries/LibDevTools/Actors/WatcherActor.h +++ b/Libraries/LibDevTools/Actors/WatcherActor.h @@ -36,10 +36,13 @@ private: StorageActor& local_storage_actor(); StorageActor& session_storage_actor(); void send_storage_resource_available_message(StorageActor&); + IndexedDBActor& indexed_db_actor(); + void send_indexed_db_resource_available_message(); WeakPtr m_tab; WeakPtr m_target; WeakPtr m_cookies; + WeakPtr m_indexed_db; WeakPtr m_local_storage; WeakPtr m_session_storage; WeakPtr m_target_configuration; @@ -47,6 +50,7 @@ private: WeakPtr m_network_parent; bool m_is_watching_frame_targets { false }; bool m_is_watching_cookie_resources { false }; + bool m_is_watching_indexed_db_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 6176b59e43..08bc844856 100644 --- a/Libraries/LibDevTools/CMakeLists.txt +++ b/Libraries/LibDevTools/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES Actors/DeviceActor.cpp Actors/FrameActor.cpp Actors/HighlighterActor.cpp + Actors/IndexedDBActor.cpp Actors/InspectorActor.cpp Actors/LayoutInspectorActor.cpp Actors/NetworkEventActor.cpp @@ -30,6 +31,7 @@ set(SOURCES Actors/WatcherActor.cpp Connection.cpp DevToolsServer.cpp + IndexedDBSerialization.cpp StorageHelpers.cpp ) diff --git a/Libraries/LibDevTools/DevToolsDelegate.h b/Libraries/LibDevTools/DevToolsDelegate.h index 93ca35ab5e..db93abe17d 100644 --- a/Libraries/LibDevTools/DevToolsDelegate.h +++ b/Libraries/LibDevTools/DevToolsDelegate.h @@ -75,6 +75,10 @@ public: virtual u64 add_storage_change_listener(TabDescription const&, OnStorageChange) const { return 0; } virtual void remove_storage_change_listener(TabDescription const&, u64) const { } + 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 { } + using OnTabInspectionComplete = Function)>; virtual void inspect_tab(TabDescription const&, OnTabInspectionComplete) const { } diff --git a/Libraries/LibDevTools/Forward.h b/Libraries/LibDevTools/Forward.h index ece05d1db5..0fb21784ae 100644 --- a/Libraries/LibDevTools/Forward.h +++ b/Libraries/LibDevTools/Forward.h @@ -23,6 +23,7 @@ class DevToolsDelegate; class DevToolsServer; class FrameActor; class HighlighterActor; +class IndexedDBActor; class InspectorActor; class LayoutInspectorActor; class NetworkEventActor; diff --git a/Libraries/LibDevTools/IndexedDBSerialization.cpp b/Libraries/LibDevTools/IndexedDBSerialization.cpp new file mode 100644 index 0000000000..6648f48e90 --- /dev/null +++ b/Libraries/LibDevTools/IndexedDBSerialization.cpp @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace DevTools::IndexedDB { + +static constexpr auto indexed_database_default_storage_name = "default"sv; +static constexpr auto indexed_database_default_storage_suffix = " (default)"sv; + +static String database_name_for_devtools(String const& database_name) +{ + return MUST(String::formatted("{}{}", database_name, indexed_database_default_storage_suffix)); +} + +static String database_name_from_devtools(String const& name) +{ + auto view = name.bytes_as_string_view(); + if (view.ends_with(indexed_database_default_storage_suffix)) + return MUST(String::from_utf8(view.substring_view(0, view.length() - indexed_database_default_storage_suffix.length()))); + return name; +} + +static String indexed_database_path(String const& database_name, Optional object_store_name = {}, Optional key = {}) +{ + JsonArray path; + path.must_append(database_name_for_devtools(database_name)); + if (object_store_name.has_value()) + path.must_append(*object_store_name); + if (key.has_value()) + path.must_append(*key); + return path.serialized(); +} + +JsonObject serialize_storage(Web::DOM::Document& document) +{ + JsonObject hosts; + for (auto const& storage_host : Web::IndexedDB::inspect_indexed_database_storage(document)) { + auto host = storage_host_for_url(storage_host.url); + if (!host.has_value()) + continue; + + JsonArray names; + for (auto const& database : storage_host.databases) { + if (database.object_store_names.is_empty()) { + names.must_append(indexed_database_path(database.name)); + continue; + } + + for (auto const& object_store_name : database.object_store_names) + names.must_append(indexed_database_path(database.name, object_store_name)); + } + + hosts.set(*host, move(names)); + } + return hosts; +} + +static Optional parse_indexed_database_path(JsonValue const& name) +{ + if (!name.is_string()) + return {}; + + auto parsed = JsonValue::from_string(name.as_string()); + if (parsed.is_error() || !parsed.value().is_array()) + return {}; + + auto array = parsed.release_value().as_array(); + if (array.is_empty() || !array.at(0).is_string()) + return {}; + + Web::IndexedDB::InspectionPath path; + path.database_name = database_name_from_devtools(array.at(0).as_string()); + if (array.size() >= 2) { + if (!array.at(1).is_string()) + return {}; + path.object_store_name = array.at(1).as_string(); + } + if (array.size() >= 3) + path.key = array.at(2); + return path; +} + +static Optional> parse_indexed_database_paths(Optional const& names) +{ + if (!names.has_value()) + return {}; + + Vector paths; + for (auto const& name : names->values()) { + auto path = parse_indexed_database_path(name); + if (path.has_value()) + paths.append(path.release_value()); + } + if (paths.is_empty()) + return {}; + return paths; +} + +static Function host_filter(String const& host) +{ + return [host](URL::URL const& url) { + auto storage_host = storage_host_for_url(url); + return storage_host.has_value() && *storage_host == host; + }; +} + +static JsonObject serialize_object_row(Web::IndexedDB::InspectionObject const& inspection_object, String const& host) +{ + return inspection_object.visit( + [&](Web::IndexedDB::InspectionDatabaseRow const& row) { + JsonObject object; + object.set("uniqueKey"sv, database_name_for_devtools(row.name)); + object.set("db"sv, row.name); + object.set("storage"sv, indexed_database_default_storage_name); + object.set("origin"sv, host); + object.set("version"sv, row.version); + object.set("objectStores"sv, row.object_store_count); + return object; + }, + [](Web::IndexedDB::InspectionObjectStore const& store) { + JsonArray indexes; + for (auto const& index : store.indexes) { + JsonObject object; + object.set("name"sv, index.name); + object.set("keyPath"sv, index.key_path); + object.set("unique"sv, index.unique); + object.set("multiEntry"sv, index.multi_entry); + indexes.must_append(move(object)); + } + JsonObject object; + object.set("objectStore"sv, store.name); + if (store.key_path.has_value()) + object.set("keyPath"sv, *store.key_path); + else + object.set("keyPath"sv, JsonValue {}); + object.set("autoIncrement"sv, store.auto_increment); + object.set("indexes"sv, indexes.serialized()); + return object; + }, + [](Web::IndexedDB::InspectionRecord const& record) { + JsonObject object; + object.set("name"sv, record.key); + object.set("value"sv, record.value); + return object; + }); +} + +static JsonObject paginated_indexed_database_response(Vector rows, JsonObject const& options, String const& host) +{ + static constexpr auto max_objects_per_page = 50uz; + + size_t offset = options.get_integer("offset"sv).value_or(0); + auto size = min(options.get_integer("size"sv).value_or(max_objects_per_page), max_objects_per_page); + auto total = rows.size(); + + JsonArray data; + if (offset < total) { + auto end = min(total, offset + size); + for (size_t i = offset; i < end; ++i) + data.must_append(serialize_object_row(rows.at(i), host)); + } else { + offset = total; + } + + JsonObject response; + response.set("offset"sv, offset); + response.set("total"sv, total); + response.set("data"sv, move(data)); + return response; +} + +JsonObject serialize_objects(Web::DOM::Document& document, String const& host, JsonValue const& names, JsonValue const& options) +{ + auto parsed_options = options.is_object() ? options.as_object() : JsonObject {}; + auto parsed_named = names.is_array() ? names.as_array() : JsonArray {}; + return paginated_indexed_database_response( + Web::IndexedDB::inspect_indexed_database_objects(document, host_filter(host), parse_indexed_database_paths(parsed_named)), + parsed_options, + host); +} + +static ErrorOr parse_required_indexed_database_path(String const& name) +{ + auto path = parse_indexed_database_path(JsonValue { name }); + if (!path.has_value()) + return Error::from_string_literal("Invalid IndexedDB path"); + return path.release_value(); +} + +ErrorOr delete_database(Web::DOM::Document& document, String const& host, String const& name) +{ + auto blocked = TRY(Web::IndexedDB::delete_indexed_database_for_inspection(document, host_filter(host), database_name_from_devtools(name))); + + JsonObject response; + if (blocked) + response.set("blocked"sv, true); + return response; +} + +ErrorOr clear_object_store(Web::DOM::Document& document, String const& host, String const& name) +{ + auto path = TRY(parse_required_indexed_database_path(name)); + if (!path.object_store_name.has_value() || path.key.has_value()) + return Error::from_string_literal("Invalid IndexedDB object store path"); + + TRY(Web::IndexedDB::clear_indexed_database_object_store_for_inspection(document, host_filter(host), path.database_name, *path.object_store_name)); + return JsonObject {}; +} + +ErrorOr delete_record(Web::DOM::Document& document, String const& host, String const& name) +{ + auto path = TRY(parse_required_indexed_database_path(name)); + if (!path.object_store_name.has_value() || !path.key.has_value()) + return Error::from_string_literal("Invalid IndexedDB record path"); + + TRY(Web::IndexedDB::delete_indexed_database_record_for_inspection(document, host_filter(host), path.database_name, *path.object_store_name, *path.key)); + return JsonObject {}; +} + +} diff --git a/Libraries/LibDevTools/IndexedDBSerialization.h b/Libraries/LibDevTools/IndexedDBSerialization.h new file mode 100644 index 0000000000..a87d57ccbd --- /dev/null +++ b/Libraries/LibDevTools/IndexedDBSerialization.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace DevTools::IndexedDB { + +DEVTOOLS_API JsonObject serialize_storage(Web::DOM::Document&); +DEVTOOLS_API JsonObject serialize_objects(Web::DOM::Document&, String const& host, JsonValue const& names, JsonValue const& options); +DEVTOOLS_API ErrorOr delete_database(Web::DOM::Document&, String const& host, String const& name); +DEVTOOLS_API ErrorOr clear_object_store(Web::DOM::Document&, String const& host, String const& name); +DEVTOOLS_API ErrorOr delete_record(Web::DOM::Document&, String const& host, String const& name); + +} diff --git a/Libraries/LibDevTools/StorageHelpers.cpp b/Libraries/LibDevTools/StorageHelpers.cpp index 3a77b6abbc..be4c16e8b4 100644 --- a/Libraries/LibDevTools/StorageHelpers.cpp +++ b/Libraries/LibDevTools/StorageHelpers.cpp @@ -31,19 +31,24 @@ Optional storage_host_for_url(String const& url_string) if (!url.has_value()) return {}; - auto const& scheme = url->scheme(); + return storage_host_for_url(url.value()); +} + +Optional storage_host_for_url(URL::URL const& url) +{ + auto const& scheme = url.scheme(); if (scheme == "http"sv || scheme == "https"sv) { StringBuilder builder; builder.append(scheme); builder.append("://"sv); - builder.append(url->serialized_host()); - if (auto port = url->port(); port.has_value()) + builder.append(url.serialized_host()); + if (auto port = url.port(); port.has_value()) builder.appendff(":{}", *port); return builder.to_string_without_validation(); } if (scheme == "about"sv || scheme == "file"sv || scheme == "javascript"sv || scheme == "resource"sv) - return url->serialize(); + return url.serialize(); return {}; } diff --git a/Libraries/LibDevTools/StorageHelpers.h b/Libraries/LibDevTools/StorageHelpers.h index 0f91c80a54..a98bf56080 100644 --- a/Libraries/LibDevTools/StorageHelpers.h +++ b/Libraries/LibDevTools/StorageHelpers.h @@ -11,10 +11,12 @@ #include #include #include +#include namespace DevTools { DEVTOOLS_API Optional storage_host_for_url(String const&); +DEVTOOLS_API Optional storage_host_for_url(URL::URL const& url); DEVTOOLS_API Optional storage_host_name(String const&); enum class StorageFieldType : u8 { diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index b7d3dd4061..3e56aeea60 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -1976,6 +1976,28 @@ void Application::remove_storage_change_listener(DevTools::TabDescription const& view->remove_storage_change_listener(listener_id); } +void Application::inspect_indexed_database_storage(DevTools::TabDescription const& description, OnIndexedDBInspectionComplete on_complete) const +{ + auto view = ViewImplementation::find_view_by_id(description.id); + if (!view.has_value()) { + on_complete(Error::from_string_literal("Unable to locate tab")); + return; + } + + view->inspect_indexed_database_storage(move(on_complete)); +} + +void Application::inspect_indexed_database_objects(DevTools::TabDescription const& description, String const& host, Optional names, JsonObject options, OnIndexedDBInspectionComplete on_complete) const +{ + auto view = ViewImplementation::find_view_by_id(description.id); + if (!view.has_value()) { + on_complete(Error::from_string_literal("Unable to locate tab")); + return; + } + + view->inspect_indexed_database_objects(host, move(names), move(options), move(on_complete)); +} + 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 8102287ae1..e695cd61ce 100644 --- a/Libraries/LibWebView/Application.h +++ b/Libraries/LibWebView/Application.h @@ -283,6 +283,8 @@ private: virtual ErrorOr clear_storage(DevTools::TabDescription const&, Web::StorageAPI::StorageEndpointType, String const&) const override; virtual u64 add_storage_change_listener(DevTools::TabDescription const&, OnStorageChange) const override; virtual void remove_storage_change_listener(DevTools::TabDescription const&, u64) const override; + virtual void inspect_indexed_database_storage(DevTools::TabDescription const&, OnIndexedDBInspectionComplete) const override; + virtual void inspect_indexed_database_objects(DevTools::TabDescription const&, String const&, Optional, JsonObject, OnIndexedDBInspectionComplete) 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 12b79688c7..26b5ddce74 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -947,6 +947,37 @@ void ViewImplementation::inspect_current_flexbox(Web::UniqueNodeID node_id, bool client().async_inspect_current_flexbox(page_id(), node_id, only_look_at_parents); } +void ViewImplementation::inspect_indexed_database_storage(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_inspect_indexed_database_storage(page_id(), request_id); +} + +void ViewImplementation::inspect_indexed_database_objects(String const& host, Optional names, JsonObject options, 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)); + if (names.has_value()) + client().async_inspect_indexed_database_objects(page_id(), request_id, host, JsonValue { names.release_value() }, JsonValue { move(options) }); + else + client().async_inspect_indexed_database_objects(page_id(), request_id, host, JsonValue {}, JsonValue { move(options) }); +} + +void ViewImplementation::did_receive_indexed_database_inspection(u64 request_id, JsonObject result) +{ + auto callback = m_pending_indexed_database_inspection_requests.take(request_id); + if (!callback.has_value()) + return; + + if (result.has_string("error"sv)) { + (*callback)(Error::from_string_literal("IndexedDB operation failed")); + return; + } + + (*callback)(move(result)); +} + void ViewImplementation::clear_inspected_dom_node() { client().async_clear_inspected_dom_node(page_id()); diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 1130dceb29..6cc5f568b5 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -147,6 +147,9 @@ public: u64 add_storage_change_listener(DevTools::DevToolsDelegate::OnStorageChange); void remove_storage_change_listener(u64 listener_id); + void inspect_indexed_database_storage(DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete); + void inspect_indexed_database_objects(String const& host, Optional names, JsonObject options, DevTools::DevToolsDelegate::OnIndexedDBInspectionComplete); + ByteString selected_text(); ByteString cut_selected_text(); Optional selected_text_with_whitespace_collapsed(); @@ -605,6 +608,9 @@ protected: HashMap m_storage_change_listeners; u64 m_next_storage_change_listener_id { 1 }; + HashMap m_pending_indexed_database_inspection_requests; + u64 m_next_indexed_database_inspection_request_id { 1 }; + // FIXME: Reconcile this ID with `page_id`. The latter is only unique per WebContent connection, whereas the view ID // is required to be globally unique for Firefox DevTools. u64 m_view_id { 0 }; @@ -619,6 +625,7 @@ protected: }; void request_node_picker_hit_test(NodePickerRequestType, Web::DevicePixelPoint); void did_receive_node_picker_hit_test(u64 request_id, Web::UniqueNodeID); + void did_receive_indexed_database_inspection(u64 request_id, JsonObject); bool m_node_picker_active { false }; Optional m_node_picker_hovered_node_id; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index a98b3e48f3..5bee424747 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -777,6 +777,12 @@ void WebContentClient::did_inspect_current_flexbox(u64 page_id, String flexbox_l } } +void WebContentClient::did_inspect_indexed_database(u64 page_id, u64 request_id, String result) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + view->did_receive_indexed_database_inspection(request_id, parse_json(result, "IndexedDB inspection result"sv)); +} + void WebContentClient::did_inspect_accessibility_tree(u64 page_id, String accessibility_tree) { 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 06f23484b0..5ded9edd0e 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -127,6 +127,7 @@ private: virtual void did_inspect_grid_layouts(u64 page_id, String) override; virtual void did_inspect_current_grid(u64 page_id, String) override; virtual void did_inspect_current_flexbox(u64 page_id, String) override; + virtual void did_inspect_indexed_database(u64 page_id, u64 request_id, String) override; virtual void did_inspect_accessibility_tree(u64 page_id, String) override; virtual void did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID node_id) override; virtual void did_get_node_id_at_position(u64 page_id, u64 request_id, Web::UniqueNodeID node_id) override; diff --git a/Services/WebContent/CMakeLists.txt b/Services/WebContent/CMakeLists.txt index 7aea6f54a1..c3e897b5d7 100644 --- a/Services/WebContent/CMakeLists.txt +++ b/Services/WebContent/CMakeLists.txt @@ -31,7 +31,7 @@ target_include_directories(webcontentservice PUBLIC $) target_include_directories(webcontentservice PUBLIC $) -target_link_libraries(webcontentservice PUBLIC LibCore LibCrypto LibFileSystem LibGfx LibHTTP LibIPC LibJS LibMain LibMedia LibWasm LibWeb LibWebSocket LibRequests LibWebView LibImageDecoderClient LibGC) +target_link_libraries(webcontentservice PUBLIC LibCore LibCrypto LibDevTools LibFileSystem LibGfx LibHTTP LibIPC LibJS LibMain LibMedia LibWasm LibWeb LibWebSocket LibRequests LibWebView LibImageDecoderClient LibGC) target_link_libraries(webcontentservice PRIVATE OpenSSL::Crypto OpenSSL::SSL) target_link_libraries(webcontentservice PRIVATE SDL3::SDL3) target_compile_options(webcontentservice PRIVATE $<$:-Wexit-time-destructors>) diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index 4e740c82b8..b36b3c1c46 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -1236,6 +1237,93 @@ void ConnectionFromClient::inspect_current_flexbox(u64 page_id, Web::UniqueNodeI async_did_inspect_current_flexbox(page_id, "null"_string); } +void ConnectionFromClient::inspect_indexed_database_storage(u64 page_id, u64 request_id) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + auto* document = page->page().top_level_browsing_context().active_document(); + if (!document) { + async_did_inspect_indexed_database(page_id, request_id, "{}"_string); + return; + } + + async_did_inspect_indexed_database(page_id, request_id, DevTools::IndexedDB::serialize_storage(*document).serialized()); +} + +void ConnectionFromClient::inspect_indexed_database_objects(u64 page_id, u64 request_id, String host, JsonValue names, JsonValue options) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + auto* document = page->page().top_level_browsing_context().active_document(); + if (!document) { + async_did_inspect_indexed_database(page_id, request_id, "{}"_string); + return; + } + + async_did_inspect_indexed_database(page_id, request_id, DevTools::IndexedDB::serialize_objects(*document, host, names, options).serialized()); +} + +static void send_indexed_database_operation_result(ConnectionFromClient& connection, u64 page_id, u64 request_id, ErrorOr result) +{ + if (result.is_error()) { + JsonObject error; + error.set("error"sv, result.error().string_literal()); + connection.async_did_inspect_indexed_database(page_id, request_id, error.serialized()); + return; + } + + connection.async_did_inspect_indexed_database(page_id, request_id, result.release_value().serialized()); +} + +void ConnectionFromClient::delete_indexed_database(u64 page_id, u64 request_id, String host, String name) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + auto* document = page->page().top_level_browsing_context().active_document(); + if (!document) { + async_did_inspect_indexed_database(page_id, request_id, "{}"_string); + return; + } + + send_indexed_database_operation_result(*this, page_id, request_id, DevTools::IndexedDB::delete_database(*document, host, name)); +} + +void ConnectionFromClient::clear_indexed_database_object_store(u64 page_id, u64 request_id, String host, String name) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + auto* document = page->page().top_level_browsing_context().active_document(); + if (!document) { + async_did_inspect_indexed_database(page_id, request_id, "{}"_string); + return; + } + + send_indexed_database_operation_result(*this, page_id, request_id, DevTools::IndexedDB::clear_object_store(*document, host, name)); +} + +void ConnectionFromClient::delete_indexed_database_record(u64 page_id, u64 request_id, String host, String name) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + auto* document = page->page().top_level_browsing_context().active_document(); + if (!document) { + async_did_inspect_indexed_database(page_id, request_id, "{}"_string); + return; + } + + send_indexed_database_operation_result(*this, page_id, request_id, DevTools::IndexedDB::delete_record(*document, host, name)); +} + void ConnectionFromClient::clear_inspected_dom_node(u64 page_id) { auto page = this->page(page_id); diff --git a/Services/WebContent/ConnectionFromClient.h b/Services/WebContent/ConnectionFromClient.h index 3788c194fc..aab2cee1d5 100644 --- a/Services/WebContent/ConnectionFromClient.h +++ b/Services/WebContent/ConnectionFromClient.h @@ -111,6 +111,8 @@ private: 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; 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 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/WebContentClient.ipc b/Services/WebContent/WebContentClient.ipc index 0a9ae507af..78085e3d44 100644 --- a/Services/WebContent/WebContentClient.ipc +++ b/Services/WebContent/WebContentClient.ipc @@ -78,6 +78,7 @@ endpoint WebContentClient did_inspect_grid_layouts(u64 page_id, String grid_layouts) =| did_inspect_current_grid(u64 page_id, String grid_layout) =| did_inspect_current_flexbox(u64 page_id, String flexbox_layout) =| + did_inspect_indexed_database(u64 page_id, u64 request_id, String result) =| did_inspect_accessibility_tree(u64 page_id, String accessibility_tree) =| did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID node_id) =| did_get_node_id_at_position(u64 page_id, u64 request_id, Web::UniqueNodeID node_id) =| diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index 542b85d76f..0e05d9ff3c 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -82,6 +82,8 @@ endpoint WebContentServer inspect_grid_layouts(u64 page_id, Web::UniqueNodeID root_node_id) =| inspect_current_grid(u64 page_id, Web::UniqueNodeID node_id) =| 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) =| 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 00ad4cb89f..95e52e87c4 100644 --- a/Tests/LibDevTools/TestDevToolsProtocol.cpp +++ b/Tests/LibDevTools/TestDevToolsProtocol.cpp @@ -545,6 +545,91 @@ static bool cookie_matches(HTTP::Cookie::Cookie const& cookie, StringView name, && cookie.path == path; } +static String indexed_database_path(StringView database, Optional object_store = {}) +{ + JsonArray path; + path.must_append(database); + if (object_store.has_value()) + path.must_append(*object_store); + return path.serialized(); +} + +static JsonObject make_indexed_database_storage_hosts() +{ + JsonArray names; + names.must_append(indexed_database_path("fixtures (default)"sv, "people"sv)); + names.must_append(indexed_database_path("empty (default)"sv)); + + JsonObject hosts; + hosts.set("https://example.test"sv, move(names)); + return hosts; +} + +static JsonObject make_indexed_database_store_objects(Optional const& names) +{ + JsonArray data; + + bool should_return_databases = !names.has_value() || names->is_empty(); + Vector paths; + if (names.has_value()) { + for (auto const& name : names->values()) { + if (!name.is_string()) + continue; + auto parsed = JsonValue::from_string(name.as_string()); + if (parsed.is_error() || !parsed.value().is_array()) + continue; + auto path = parsed.release_value().as_array(); + if (path.is_empty()) + continue; + paths.append(move(path)); + } + if (paths.is_empty()) + should_return_databases = true; + } + + if (should_return_databases) { + JsonObject fixtures; + fixtures.set("uniqueKey"sv, "fixtures (default)"sv); + fixtures.set("db"sv, "fixtures"sv); + fixtures.set("storage"sv, "default"sv); + fixtures.set("origin"sv, "https://example.test"sv); + fixtures.set("version"sv, 1); + fixtures.set("objectStores"sv, 1); + data.must_append(move(fixtures)); + + JsonObject empty; + empty.set("uniqueKey"sv, "empty (default)"sv); + empty.set("db"sv, "empty"sv); + empty.set("storage"sv, "default"sv); + empty.set("origin"sv, "https://example.test"sv); + empty.set("version"sv, 2); + empty.set("objectStores"sv, 0); + data.must_append(move(empty)); + } else { + for (auto const& path : paths) { + if (path.size() == 1) { + JsonObject store; + store.set("objectStore"sv, "people"sv); + store.set("keyPath"sv, "id"sv); + store.set("autoIncrement"sv, true); + store.set("indexes"sv, "[]"_string); + data.must_append(move(store)); + } else if (path.size() >= 2) { + JsonObject record; + record.set("name"sv, 1); + record.set("value"sv, "{\"name\":\"Ada\"}"sv); + data.must_append(move(record)); + } + } + } + + JsonObject response; + response.set("offset"sv, 0); + response.set("total"sv, data.size()); + response.set("data"sv, move(data)); + return response; +} + class TestDevToolsDelegate final : public DevTools::DevToolsDelegate { public: virtual Vector tab_list() const override @@ -733,6 +818,19 @@ public: ++remove_storage_change_listener_call_count; } + virtual void inspect_indexed_database_storage(DevTools::TabDescription const&, OnIndexedDBInspectionComplete callback) const override + { + ++inspect_indexed_database_storage_call_count; + callback(make_indexed_database_storage_hosts()); + } + + virtual void inspect_indexed_database_objects(DevTools::TabDescription const&, String const& host, Optional names, JsonObject, OnIndexedDBInspectionComplete callback) const override + { + ++inspect_indexed_database_objects_call_count; + last_indexed_database_host = host; + callback(make_indexed_database_store_objects(names)); + } + virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete callback) const override { ++inspect_tab_call_count; @@ -1187,6 +1285,8 @@ public: mutable bool fail_set_storage_item { false }; mutable bool fail_remove_storage_item { false }; mutable bool fail_clear_storage { false }; + mutable size_t inspect_indexed_database_storage_call_count { 0 }; + mutable size_t inspect_indexed_database_objects_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 }; @@ -1259,6 +1359,7 @@ public: mutable Optional last_navigated_url; mutable Optional last_reload_bypass_cache; mutable Optional last_history_delta; + mutable Optional last_indexed_database_host; }; class ProtocolClient { @@ -1646,6 +1747,68 @@ static JsonObject remove_all_storage_items(ProtocolClient& client, StringView st return client.request(move(request)); } +static String get_indexed_database_actor(ProtocolClient& client) +{ + 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("indexed-db"sv); + watch_resources.set("resourceTypes"sv, move(resource_types)); + EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor); + + auto indexed_database_resource = read_resource(client, "indexed-db"sv); + return actor_from(indexed_database_resource, "actor"sv); +} + +static JsonObject get_indexed_database_store_objects(ProtocolClient& client, StringView indexed_database_actor, Optional name = {}) +{ + JsonObject get_store_objects; + get_store_objects.set("to"sv, indexed_database_actor); + get_store_objects.set("type"sv, "getStoreObjects"sv); + get_store_objects.set("host"sv, "https://example.test"sv); + if (name.has_value()) { + JsonArray names; + names.must_append(*name); + get_store_objects.set("names"sv, move(names)); + } else { + get_store_objects.set("names"sv, JsonValue {}); + } + get_store_objects.set("options"sv, JsonObject {}); + return client.request(move(get_store_objects)); +} + +static JsonObject get_indexed_database_store_objects(ProtocolClient& client, StringView indexed_database_actor, JsonArray names) +{ + JsonObject get_store_objects; + get_store_objects.set("to"sv, indexed_database_actor); + get_store_objects.set("type"sv, "getStoreObjects"sv); + get_store_objects.set("host"sv, "https://example.test"sv); + get_store_objects.set("names"sv, move(names)); + get_store_objects.set("options"sv, JsonObject {}); + return client.request(move(get_store_objects)); +} + +static JsonArray get_indexed_database_update_paths(JsonObject const& stores_update, StringView update_type, StringView host = "https://example.test"sv) +{ + return stores_update.get_object("data"sv) + ->get_object(update_type) + ->get_object("indexedDB"sv) + ->get_array(host) + .release_value(); +} + +static JsonArray get_indexed_database_cleared_paths(JsonObject const& stores_cleared, StringView host = "https://example.test"sv) +{ + return stores_cleared.get_object("data"sv) + ->get_object("clearedHostsOrPaths"sv) + ->get_array(host) + .release_value(); +} + static JsonObject make_cookie_edit_items(HTTP::Cookie::Cookie const& cookie) { JsonObject items; @@ -2275,6 +2438,117 @@ TEST_CASE(storage_web_storage_mutation_errors) EXPECT_EQ(session->delegate.fixture_local_storage_items.size(), 1u); } +TEST_CASE(storage_indexed_database_resource) +{ + 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("indexed-db"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("indexed-db"sv); + watch_resources.set("resourceTypes"sv, move(resource_types)); + EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor); + + auto indexed_database_resource = read_resource(client, "indexed-db"sv); + EXPECT_EQ(indexed_database_resource.get_string("resourceKey"sv).value(), "indexedDB"sv); + EXPECT_EQ(indexed_database_resource.get_integer("browsingContextID"sv).value(), 1u); + EXPECT_EQ(indexed_database_resource.get_integer("innerWindowId"sv).value(), 1u); + EXPECT_EQ(indexed_database_resource.get_string("resourceId"sv).value(), "indexed-db-1"sv); + + auto hosts = indexed_database_resource.get_object("hosts"sv).release_value(); + auto names = hosts.get_array("https://example.test"sv).release_value(); + EXPECT_EQ(names.size(), 2u); + EXPECT_EQ(names.at(0).as_string(), "[\"fixtures (default)\",\"people\"]"sv); + EXPECT_EQ(names.at(1).as_string(), "[\"empty (default)\"]"sv); + + auto traits = indexed_database_resource.get_object("traits"sv).release_value(); + EXPECT(!traits.get_bool("supportsAddItem"sv).value()); + EXPECT(traits.get_bool("supportsRemoveAll"sv).value()); + EXPECT(!traits.get_bool("supportsRemoveAllSessionCookies"sv).value()); + EXPECT(traits.get_bool("supportsRemoveItem"sv).value()); + + auto indexed_database_actor = actor_from(indexed_database_resource, "actor"sv); + auto fields = client.request(indexed_database_actor, "getFields"sv).get_array("value"sv).release_value(); + EXPECT_EQ(fields.at(0).as_object().get_string("name"sv).value(), "uniqueKey"sv); + EXPECT(fields.at(0).as_object().get_bool("private"sv).value()); + EXPECT_EQ(fields.at(1).as_object().get_string("name"sv).value(), "db"sv); + + JsonObject database_fields_request; + database_fields_request.set("to"sv, indexed_database_actor); + database_fields_request.set("type"sv, "getFields"sv); + database_fields_request.set("subType"sv, "database"sv); + auto database_fields = client.request(move(database_fields_request)).get_array("value"sv).release_value(); + EXPECT_EQ(database_fields.at(0).as_object().get_string("name"sv).value(), "objectStore"sv); + + JsonObject object_store_fields_request; + object_store_fields_request.set("to"sv, indexed_database_actor); + object_store_fields_request.set("type"sv, "getFields"sv); + object_store_fields_request.set("subType"sv, "object store"sv); + auto object_store_fields = client.request(move(object_store_fields_request)).get_array("value"sv).release_value(); + EXPECT_EQ(object_store_fields.at(0).as_object().get_string("name"sv).value(), "name"sv); + EXPECT_EQ(object_store_fields.at(1).as_object().get_string("name"sv).value(), "value"sv); + + EXPECT_EQ(session->delegate.inspect_indexed_database_storage_call_count, 1u); +} + +TEST_CASE(storage_indexed_database_store_objects) +{ + auto session = create_session(); + auto& client = *session->client; + (void)client.read_message(); + + auto indexed_database_actor = get_indexed_database_actor(client); + + auto databases = get_indexed_database_store_objects(client, indexed_database_actor); + EXPECT_EQ(session->delegate.inspect_indexed_database_objects_call_count, 1u); + EXPECT_EQ(session->delegate.last_indexed_database_host.value(), "https://example.test"sv); + EXPECT_EQ(databases.get_integer("offset"sv).value(), 0u); + EXPECT_EQ(databases.get_integer("total"sv).value(), 2u); + + auto database_rows = databases.get_array("data"sv).release_value(); + EXPECT_EQ(database_rows.size(), 2u); + auto fixtures_database = database_rows.at(0).as_object(); + EXPECT_EQ(fixtures_database.get_string("uniqueKey"sv).value(), "fixtures (default)"sv); + EXPECT_EQ(fixtures_database.get_string("db"sv).value(), "fixtures"sv); + EXPECT_EQ(fixtures_database.get_string("storage"sv).value(), "default"sv); + EXPECT_EQ(fixtures_database.get_string("origin"sv).value(), "https://example.test"sv); + EXPECT_EQ(fixtures_database.get_integer("version"sv).value(), 1); + EXPECT_EQ(fixtures_database.get_integer("objectStores"sv).value(), 1); + + auto databases_from_empty_names = get_indexed_database_store_objects(client, indexed_database_actor, JsonArray {}); + EXPECT_EQ(databases_from_empty_names.get_integer("total"sv).value(), 2u); + + JsonArray empty_path_names; + empty_path_names.must_append("[]"sv); + auto databases_from_empty_path = get_indexed_database_store_objects(client, indexed_database_actor, move(empty_path_names)); + EXPECT_EQ(databases_from_empty_path.get_integer("total"sv).value(), 2u); + + auto object_stores = get_indexed_database_store_objects(client, indexed_database_actor, indexed_database_path("fixtures (default)"sv)); + EXPECT_EQ(object_stores.get_integer("total"sv).value(), 1u); + auto object_store_rows = object_stores.get_array("data"sv).release_value(); + auto people_store = object_store_rows.at(0).as_object(); + EXPECT_EQ(people_store.get_string("objectStore"sv).value(), "people"sv); + EXPECT_EQ(people_store.get_string("keyPath"sv).value(), "id"sv); + EXPECT(people_store.get_bool("autoIncrement"sv).value()); + EXPECT_EQ(people_store.get_string("indexes"sv).value(), "[]"sv); + + auto records = get_indexed_database_store_objects(client, indexed_database_actor, indexed_database_path("fixtures (default)"sv, "people"sv)); + EXPECT_EQ(records.get_integer("total"sv).value(), 1u); + auto record_rows = records.get_array("data"sv).release_value(); + auto record = record_rows.at(0).as_object(); + EXPECT_EQ(record.get_integer("name"sv).value(), 1); + EXPECT_EQ(record.get_string("value"sv).value(), "{\"name\":\"Ada\"}"sv); +} + TEST_CASE(storage_cookie_store_objects) { auto session = create_session();