2026-06-17 08:39:37 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2026, Ladybird contributors
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <AK/JsonArray.h>
|
|
|
|
|
#include <LibDevTools/IndexedDBSerialization.h>
|
|
|
|
|
#include <LibDevTools/StorageHelpers.h>
|
|
|
|
|
#include <LibURL/URL.h>
|
|
|
|
|
#include <LibWeb/IndexedDB/Inspection.h>
|
|
|
|
|
|
|
|
|
|
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<String const&> object_store_name = {}, Optional<JsonValue const&> 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<Web::IndexedDB::InspectionPath> 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<Vector<Web::IndexedDB::InspectionPath>> parse_indexed_database_paths(Optional<JsonArray> const& names)
|
|
|
|
|
{
|
|
|
|
|
if (!names.has_value())
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
Vector<Web::IndexedDB::InspectionPath> 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<bool(URL::URL const&)> 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<Web::IndexedDB::InspectionObject> rows, JsonObject const& options, String const& host)
|
|
|
|
|
{
|
|
|
|
|
static constexpr auto max_objects_per_page = 50uz;
|
|
|
|
|
|
|
|
|
|
size_t offset = options.get_integer<size_t>("offset"sv).value_or(0);
|
|
|
|
|
auto size = min(options.get_integer<size_t>("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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 12:51:05 -03:00
|
|
|
static void append_indexed_database_update(JsonObject& update, StringView type, JsonArray paths, String const& host)
|
|
|
|
|
{
|
|
|
|
|
if (paths.is_empty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
JsonObject hosts;
|
|
|
|
|
hosts.set(host, move(paths));
|
|
|
|
|
|
|
|
|
|
JsonObject indexed_database;
|
|
|
|
|
indexed_database.set("indexedDB"sv, move(hosts));
|
|
|
|
|
|
|
|
|
|
update.set(type, move(indexed_database));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static JsonArray serialize_update_paths(Vector<Web::IndexedDB::TransactionChange> const& changes)
|
|
|
|
|
{
|
|
|
|
|
JsonArray paths;
|
|
|
|
|
paths.ensure_capacity(changes.size());
|
|
|
|
|
for (auto const& change : changes) {
|
|
|
|
|
// FIXME: Firefox treats added IndexedDB update paths as storage tree additions, so record-level changes must
|
|
|
|
|
// not be sent there or they appear as blank rows in the selected host view.
|
|
|
|
|
// Remove this filter once Firefox supports record updates.
|
|
|
|
|
if (change.key.has_value())
|
|
|
|
|
continue;
|
|
|
|
|
paths.must_append(indexed_database_path(change.database_name, change.object_store_name, change.key));
|
|
|
|
|
}
|
|
|
|
|
return paths;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonObject serialize_update(String const& url, Web::IndexedDB::TransactionChanges const& changes)
|
|
|
|
|
{
|
|
|
|
|
JsonObject update;
|
|
|
|
|
auto host = storage_host_for_url(url);
|
|
|
|
|
if (!host.has_value())
|
|
|
|
|
return update;
|
|
|
|
|
|
|
|
|
|
append_indexed_database_update(update, "added"sv, serialize_update_paths(changes.added), *host);
|
|
|
|
|
append_indexed_database_update(update, "changed"sv, serialize_update_paths(changes.changed), *host);
|
|
|
|
|
append_indexed_database_update(update, "deleted"sv, serialize_update_paths(changes.deleted), *host);
|
|
|
|
|
return update;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 08:39:37 -03:00
|
|
|
static ErrorOr<Web::IndexedDB::InspectionPath> 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<JsonObject> 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<JsonObject> 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<JsonObject> 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 {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|