/* * 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 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 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; } 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 {}; } }