2026-06-17 08:39:37 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2026, Ladybird contributors
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <AK/JsonArray.h>
|
|
|
|
|
#include <AK/JsonObject.h>
|
|
|
|
|
#include <LibDevTools/Actors/IndexedDBActor.h>
|
|
|
|
|
#include <LibDevTools/Actors/TabActor.h>
|
|
|
|
|
#include <LibDevTools/DevToolsDelegate.h>
|
|
|
|
|
#include <LibDevTools/DevToolsServer.h>
|
|
|
|
|
#include <LibDevTools/StorageHelpers.h>
|
|
|
|
|
|
|
|
|
|
namespace DevTools {
|
|
|
|
|
|
|
|
|
|
NonnullRefPtr<IndexedDBActor> IndexedDBActor::create(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab)
|
|
|
|
|
{
|
|
|
|
|
return adopt_ref(*new IndexedDBActor(devtools, move(name), move(tab)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IndexedDBActor::IndexedDBActor(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab)
|
|
|
|
|
: Actor(devtools, move(name))
|
|
|
|
|
, m_tab(move(tab))
|
|
|
|
|
{
|
2026-06-11 12:51:05 -03:00
|
|
|
if (auto tab = m_tab.strong_ref()) {
|
|
|
|
|
m_indexed_database_change_listener_id = devtools.delegate().add_indexed_database_change_listener(
|
|
|
|
|
tab->description(),
|
|
|
|
|
weak_callback(*this, [](auto& self, JsonObject update) {
|
|
|
|
|
self.on_indexed_database_changed(move(update));
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-06-17 08:39:37 -03:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 12:51:05 -03:00
|
|
|
IndexedDBActor::~IndexedDBActor()
|
|
|
|
|
{
|
|
|
|
|
if (m_indexed_database_change_listener_id == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (auto tab = m_tab.strong_ref())
|
|
|
|
|
devtools().delegate().remove_indexed_database_change_listener(tab->description(), m_indexed_database_change_listener_id);
|
|
|
|
|
}
|
2026-06-17 08:39:37 -03:00
|
|
|
|
|
|
|
|
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<void(JsonObject)> 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<IndexedDBActor>(), callback = move(callback)](ErrorOr<JsonObject> 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 12:59:48 -03:00
|
|
|
if (message.type == "removeDatabase"sv) {
|
|
|
|
|
remove_database(message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (message.type == "removeAll"sv) {
|
|
|
|
|
remove_all(message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (message.type == "removeItem"sv) {
|
|
|
|
|
remove_item(message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 08:39:37 -03:00
|
|
|
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<String>(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<JsonArray> 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<IndexedDBActor>(), message_id = message.id](ErrorOr<JsonObject> result) mutable {
|
|
|
|
|
auto self = weak_self.strong_ref();
|
|
|
|
|
if (!self)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (result.is_error()) {
|
|
|
|
|
self->send_inspection_error({ .id = message_id }, result.error());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self->send_response({ .id = message_id }, result.release_value());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 12:59:48 -03:00
|
|
|
void IndexedDBActor::remove_database(Message const& message)
|
|
|
|
|
{
|
|
|
|
|
auto host = get_required_parameter<String>(message, "host"sv);
|
|
|
|
|
auto name = get_required_parameter<String>(message, "name"sv);
|
|
|
|
|
if (!host.has_value() || !name.has_value())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto tab = m_tab.strong_ref();
|
|
|
|
|
if (!tab) {
|
|
|
|
|
send_inspection_error(message, Error::from_string_literal("Unable to locate tab"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
devtools().delegate().delete_indexed_database(tab->description(), *host, *name,
|
|
|
|
|
[weak_self = make_weak_ptr<IndexedDBActor>(), message_id = message.id, host = *host, name = *name](ErrorOr<JsonObject> result) mutable {
|
|
|
|
|
auto self = weak_self.strong_ref();
|
|
|
|
|
if (!self)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (result.is_error()) {
|
|
|
|
|
self->send_inspection_error({ .id = message_id }, result.error());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto response = result.release_value();
|
|
|
|
|
auto blocked = response.get_bool("blocked"sv).value_or(false);
|
|
|
|
|
self->send_response({ .id = message_id }, move(response));
|
|
|
|
|
if (!blocked) {
|
|
|
|
|
JsonArray path;
|
|
|
|
|
path.must_append(name);
|
|
|
|
|
self->send_indexed_database_update("deleted"sv, host, path.serialized());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void IndexedDBActor::remove_all(Message const& message)
|
|
|
|
|
{
|
|
|
|
|
auto host = get_required_parameter<String>(message, "host"sv);
|
|
|
|
|
auto name = get_required_parameter<String>(message, "name"sv);
|
|
|
|
|
if (!host.has_value() || !name.has_value())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto tab = m_tab.strong_ref();
|
|
|
|
|
if (!tab) {
|
|
|
|
|
send_inspection_error(message, Error::from_string_literal("Unable to locate tab"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
devtools().delegate().clear_indexed_database_object_store(tab->description(), *host, *name,
|
|
|
|
|
[weak_self = make_weak_ptr<IndexedDBActor>(), message_id = message.id, host = *host, name = *name](ErrorOr<JsonObject> result) mutable {
|
|
|
|
|
auto self = weak_self.strong_ref();
|
|
|
|
|
if (!self)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (result.is_error()) {
|
|
|
|
|
self->send_inspection_error({ .id = message_id }, result.error());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto response = result.release_value();
|
|
|
|
|
self->send_response({ .id = message_id }, move(response));
|
|
|
|
|
self->send_indexed_database_clear(host, name);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void IndexedDBActor::remove_item(Message const& message)
|
|
|
|
|
{
|
|
|
|
|
auto host = get_required_parameter<String>(message, "host"sv);
|
|
|
|
|
auto name = get_required_parameter<String>(message, "name"sv);
|
|
|
|
|
if (!host.has_value() || !name.has_value())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto tab = m_tab.strong_ref();
|
|
|
|
|
if (!tab) {
|
|
|
|
|
send_inspection_error(message, Error::from_string_literal("Unable to locate tab"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
devtools().delegate().delete_indexed_database_record(tab->description(), *host, *name,
|
|
|
|
|
[weak_self = make_weak_ptr<IndexedDBActor>(), message_id = message.id, host = *host, name = *name](ErrorOr<JsonObject> result) mutable {
|
|
|
|
|
auto self = weak_self.strong_ref();
|
|
|
|
|
if (!self)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (result.is_error()) {
|
|
|
|
|
self->send_inspection_error({ .id = message_id }, result.error());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto response = result.release_value();
|
|
|
|
|
self->send_response({ .id = message_id }, move(response));
|
|
|
|
|
self->send_indexed_database_update("deleted"sv, host, name);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void IndexedDBActor::send_indexed_database_update(StringView update_type, String const& host, String const& name)
|
|
|
|
|
{
|
|
|
|
|
JsonArray paths;
|
|
|
|
|
paths.must_append(name);
|
|
|
|
|
|
|
|
|
|
JsonObject hosts;
|
|
|
|
|
hosts.set(host, move(paths));
|
|
|
|
|
|
|
|
|
|
JsonObject indexed_database;
|
|
|
|
|
indexed_database.set("indexedDB"sv, move(hosts));
|
|
|
|
|
|
|
|
|
|
JsonObject update;
|
|
|
|
|
update.set(update_type, move(indexed_database));
|
|
|
|
|
|
|
|
|
|
on_indexed_database_changed(move(update));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void IndexedDBActor::send_indexed_database_clear(String const& host, String const& name)
|
|
|
|
|
{
|
|
|
|
|
JsonArray paths;
|
|
|
|
|
paths.must_append(name);
|
|
|
|
|
|
|
|
|
|
JsonObject hosts;
|
|
|
|
|
hosts.set(host, move(paths));
|
|
|
|
|
|
|
|
|
|
JsonObject data;
|
|
|
|
|
data.set("clearedHostsOrPaths"sv, move(hosts));
|
|
|
|
|
|
|
|
|
|
JsonObject message;
|
|
|
|
|
message.set("type"sv, "storesCleared"sv);
|
|
|
|
|
message.set("data"sv, move(data));
|
|
|
|
|
send_message(move(message));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 12:51:05 -03:00
|
|
|
void IndexedDBActor::on_indexed_database_changed(JsonObject update)
|
|
|
|
|
{
|
|
|
|
|
JsonObject message;
|
|
|
|
|
message.set("type"sv, "storesUpdate"sv);
|
|
|
|
|
message.set("data"sv, move(update));
|
|
|
|
|
send_message(move(message));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 08:39:37 -03:00
|
|
|
}
|