/* * 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 serialize_storage_item(DevToolsDelegate::StorageItem const& item) { JsonObject object; object.set("name"sv, item.name); object.set("value"sv, item.value); return object; } static Optional string_from_devtools_value(JsonValue const& value) { if (value.is_string()) return value.as_string(); if (value.is_bool()) return value.as_bool() ? "true"_string : "false"_string; if (value.is_number()) return MUST(String::formatted("{}", value.get_number_with_precision_loss().release_value())); if (value.is_null()) return {}; return value.serialized(); } 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) { if (auto tab = m_tab.strong_ref()) { m_storage_change_listener_id = devtools.delegate().add_storage_change_listener( tab->description(), weak_callback(*this, [](auto& self, DevToolsDelegate::StorageChange change) { self.on_storage_changed(move(change)); })); } } StorageActor::~StorageActor() { if (m_storage_change_listener_id == 0) return; if (auto tab = m_tab.strong_ref()) devtools().delegate().remove_storage_change_listener(tab->description(), m_storage_change_listener_id); } 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; } if (message.type == "editItem"sv) { edit_item(message); return; } if (message.type == "addItem"sv) { add_item(message); return; } if (message.type == "removeItem"sv) { remove_item(message); return; } if (message.type == "removeAll"sv) { remove_all(message); return; } send_unrecognized_packet_type_error(message); } void StorageActor::get_fields(Message const& message) { JsonArray fields; fields.must_append(define_storage_field("name"sv, StorageFieldType::Mutable)); fields.must_append(define_storage_field("value"sv, StorageFieldType::Mutable)); 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)); } void StorageActor::edit_item(Message const& message) { auto data = get_required_parameter(message, "data"sv); if (!data.has_value()) return; auto requested_host = data->get_string("host"sv); if (!requested_host.has_value()) { send_missing_parameter_error(message, "host"sv); return; } if (!host().has_value() || *host() != *requested_host) { send_response(message, to_storage_operation_result(Error::from_string_literal("Storage host is not available"))); return; } auto field = data->get_string("field"sv); if (!field.has_value()) { send_missing_parameter_error(message, "field"sv); return; } auto items = data->get_object("items"sv); if (!items.has_value()) { send_missing_parameter_error(message, "items"sv); return; } auto key = items->get_string("name"sv); if (!key.has_value()) { send_missing_parameter_error(message, "items.name"sv); return; } auto value_json = items->get("value"sv); if (!value_json.has_value()) { send_missing_parameter_error(message, "items.value"sv); return; } auto value = string_from_devtools_value(*value_json); if (!value.has_value()) { send_response(message, to_storage_operation_result(Error::from_string_literal("Missing storage value"))); return; } auto tab = m_tab.strong_ref(); if (!tab) return; if (*field == "name"sv) { auto old_value_json = data->get("oldValue"sv); if (!old_value_json.has_value()) { send_missing_parameter_error(message, "oldValue"sv); return; } auto old_key = string_from_devtools_value(*old_value_json); if (!old_key.has_value()) { send_response(message, to_storage_operation_result(Error::from_string_literal("Missing old storage key"))); return; } if (*old_key != *key) { auto remove_result = devtools().delegate().remove_storage_item(tab->description(), m_storage_endpoint, *requested_host, *old_key); if (remove_result.is_error()) { send_response(message, to_storage_operation_result(remove_result.release_error())); return; } } } auto result = devtools().delegate().set_storage_item(tab->description(), m_storage_endpoint, *requested_host, *key, *value); if (result.is_error()) send_response(message, to_storage_operation_result(result.release_error())); else send_response(message, to_storage_operation_result(ErrorOr {})); } void StorageActor::add_item(Message const& message) { auto guid = get_required_parameter(message, "guid"sv); if (!guid.has_value()) return; auto requested_host = message.data.get_string("host"sv) .value_or_lazy_evaluated_optional([this] { return host(); }); if (!requested_host.has_value()) { send_response(message, to_storage_operation_result(Error::from_string_literal("No storage host is available for this page"))); return; } if (!host().has_value() || *host() != *requested_host) { send_response(message, to_storage_operation_result(Error::from_string_literal("Storage host is not available"))); return; } auto tab = m_tab.strong_ref(); if (!tab) return; auto result = devtools().delegate().set_storage_item(tab->description(), m_storage_endpoint, *requested_host, *guid, "value"_string); if (result.is_error()) send_response(message, to_storage_operation_result(result.release_error())); else send_response(message, to_storage_operation_result(ErrorOr {})); } void StorageActor::remove_item(Message const& message) { auto requested_host = get_required_parameter(message, "host"sv); if (!requested_host.has_value()) return; auto name = get_required_parameter(message, "name"sv); if (!name.has_value()) return; if (!host().has_value() || *host() != *requested_host) { send_response(message, {}); return; } auto tab = m_tab.strong_ref(); if (!tab) return; auto result = devtools().delegate().remove_storage_item(tab->description(), m_storage_endpoint, *requested_host, *name); if (result.is_error()) send_response(message, to_storage_operation_result(result.release_error())); else send_response(message, {}); } void StorageActor::remove_all(Message const& message) { auto requested_host = get_required_parameter(message, "host"sv); if (!requested_host.has_value()) return; if (!host().has_value() || *host() != *requested_host) { send_response(message, {}); return; } auto tab = m_tab.strong_ref(); if (!tab) return; auto result = devtools().delegate().clear_storage(tab->description(), m_storage_endpoint, *requested_host); if (result.is_error()) send_response(message, to_storage_operation_result(result.release_error())); else send_response(message, {}); } void StorageActor::on_storage_changed(DevToolsDelegate::StorageChange change) { if (change.storage_endpoint != m_storage_endpoint) return; auto storage_host = host(); if (!storage_host.has_value() || *storage_host != change.host) return; if (change.type == DevToolsDelegate::StorageChange::Type::Cleared) { send_store_cleared(change); return; } if (!change.key.has_value()) return; send_store_update(change); } void StorageActor::send_store_update(DevToolsDelegate::StorageChange const& change) { StringView update_type; switch (change.type) { case DevToolsDelegate::StorageChange::Type::Added: update_type = "added"sv; break; case DevToolsDelegate::StorageChange::Type::Changed: update_type = "changed"sv; break; case DevToolsDelegate::StorageChange::Type::Deleted: update_type = "deleted"sv; break; case DevToolsDelegate::StorageChange::Type::Cleared: VERIFY_NOT_REACHED(); } JsonArray keys; keys.must_append(*change.key); JsonObject hosts; hosts.set(change.host, move(keys)); JsonObject storage_updates; storage_updates.set(resource_key(), move(hosts)); JsonObject data; data.set(update_type, move(storage_updates)); JsonObject message; message.set("type"sv, "storesUpdate"sv); message.set("data"sv, move(data)); send_message(move(message)); } void StorageActor::send_store_cleared(DevToolsDelegate::StorageChange const& change) { JsonArray hosts; hosts.must_append(change.host); 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)); } }