LibDevTools: Allow adding and removing DevTools cookies

Firefox enables the Storage panel cookie toolbar from the storage traits
and calls addItem, removeItem, removeAll, or removeAllSessionCookies
for the selected host.

Advertise those operations and route them through the existing cookie
mutation delegate. Deletions reuse the visible storage host matching so
bulk actions stay scoped to the cookie tree item Firefox selected.
This commit is contained in:
Sam Atkins 2026-06-10 11:53:53 +01:00
parent 4113968e77
commit aa4139d002
3 changed files with 312 additions and 8 deletions

View file

@ -23,6 +23,7 @@ namespace DevTools {
static constexpr auto cookie_unique_key_separator = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"sv;
static constexpr auto max_store_object_count = 50uz;
static constexpr auto session_cookie_label = "Session"sv;
static constexpr auto default_cookie_value = "value"sv;
static bool cookie_matches_storage_host(HTTP::Cookie::Cookie const& cookie, String const& storage_host)
{
@ -73,6 +74,13 @@ static bool cookie_matches_unique_key(HTTP::Cookie::Cookie const& cookie, Parsed
&& cookie.path == unique_key.path;
}
static bool cookie_matches_requested_domain(HTTP::Cookie::Cookie const& cookie, Optional<String> const& domain)
{
if (!domain.has_value())
return true;
return cookie.domain == *domain;
}
static JsonObject cookie_operation_result(Optional<String> error_string)
{
JsonObject response;
@ -222,10 +230,10 @@ JsonObject CookiesActor::serialize_storage() const
hosts.set(*storage_host, JsonArray {});
JsonObject traits;
traits.set("supportsAddItem"sv, false);
traits.set("supportsRemoveAll"sv, false);
traits.set("supportsRemoveAllSessionCookies"sv, false);
traits.set("supportsRemoveItem"sv, false);
traits.set("supportsAddItem"sv, true);
traits.set("supportsRemoveAll"sv, true);
traits.set("supportsRemoveAllSessionCookies"sv, true);
traits.set("supportsRemoveItem"sv, true);
JsonObject storage;
storage.set("actor"sv, name());
@ -257,6 +265,26 @@ void CookiesActor::handle_message(Message const& 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;
}
if (message.type == "removeAllSessionCookies"sv) {
remove_all_session_cookies(message);
return;
}
send_unrecognized_packet_type_error(message);
}
@ -493,6 +521,137 @@ void CookiesActor::edit_item(Message const& message)
send_response(message, cookie_operation_result(move(result)));
}
void CookiesActor::add_item(Message const& message)
{
auto guid = get_required_parameter<String>(message, "guid"sv);
if (!guid.has_value())
return;
auto storage_host = message.data.get_string("host"sv)
.value_or_lazy_evaluated_optional([this] { return host(); });
if (!storage_host.has_value()) {
send_response(message, cookie_operation_result("No storage host is available for this page"_string));
return;
}
auto storage_url = URL::Parser::basic_parse(*storage_host);
auto host_name = storage_host_name(*storage_host);
if (!storage_url.has_value() || !host_name.has_value()) {
send_response(message, cookie_operation_result("Cannot add a cookie for this storage host"_string));
return;
}
auto tab = m_tab.strong_ref();
if (!tab)
return;
auto now = UnixDateTime::now();
HTTP::Cookie::Cookie cookie;
cookie.name = move(*guid);
cookie.value = MUST(String::from_utf8(default_cookie_value));
cookie.same_site = HTTP::Cookie::SameSite::Lax;
cookie.creation_time = now;
cookie.last_access_time = now;
set_session_cookie_expiry(cookie);
cookie.domain = move(*host_name);
cookie.path = "/"_string;
cookie.secure = storage_url->scheme() == "https"sv;
cookie.host_only = true;
auto result = devtools().delegate().set_cookie(tab->description(), {}, move(cookie));
send_response(message, cookie_operation_result(move(result)));
}
void CookiesActor::remove_item(Message const& message)
{
auto host = get_required_parameter<String>(message, "host"sv);
if (!host.has_value())
return;
auto name = get_required_parameter<String>(message, "name"sv);
if (!name.has_value())
return;
auto unique_key = parse_cookie_unique_key(name->bytes_as_string_view());
if (!unique_key.has_value()) {
send_response(message, {});
return;
}
auto tab = m_tab.strong_ref();
if (!tab)
return;
Vector<HTTP::Cookie::Cookie> cookies_to_delete;
for (auto& cookie : devtools().delegate().cookies(tab->description())) {
if (!cookie_matches_storage_host(cookie, *host))
continue;
if (!cookie_matches_unique_key(cookie, *unique_key))
continue;
cookies_to_delete.append(move(cookie));
}
devtools().delegate().delete_cookies(tab->description(), move(cookies_to_delete));
send_response(message, {});
}
void CookiesActor::remove_all(Message const& message)
{
auto host = get_required_parameter<String>(message, "host"sv);
if (!host.has_value())
return;
Optional<String> domain;
if (auto requested_domain = message.data.get_string("domain"sv); requested_domain.has_value())
domain = *requested_domain;
auto tab = m_tab.strong_ref();
if (!tab)
return;
Vector<HTTP::Cookie::Cookie> cookies_to_delete;
for (auto& cookie : devtools().delegate().cookies(tab->description())) {
if (!cookie_matches_storage_host(cookie, *host))
continue;
if (!cookie_matches_requested_domain(cookie, domain))
continue;
cookies_to_delete.append(move(cookie));
}
devtools().delegate().delete_cookies(tab->description(), move(cookies_to_delete));
send_response(message, {});
}
void CookiesActor::remove_all_session_cookies(Message const& message)
{
auto host = get_required_parameter<String>(message, "host"sv);
if (!host.has_value())
return;
Optional<String> domain;
if (auto requested_domain = message.data.get_string("domain"sv); requested_domain.has_value())
domain = *requested_domain;
auto tab = m_tab.strong_ref();
if (!tab)
return;
Vector<HTTP::Cookie::Cookie> cookies_to_delete;
for (auto& cookie : devtools().delegate().cookies(tab->description())) {
if (!cookie_matches_storage_host(cookie, *host))
continue;
if (!cookie_matches_requested_domain(cookie, domain))
continue;
if (cookie.persistent)
continue;
cookies_to_delete.append(move(cookie));
}
devtools().delegate().delete_cookies(tab->description(), move(cookies_to_delete));
send_response(message, {});
}
HashTable<String> CookiesActor::visible_cookie_unique_keys(String const& host) const
{
HashTable<String> visible_keys;

View file

@ -35,6 +35,10 @@ private:
void get_fields(Message const&);
void get_store_objects(Message const&);
void edit_item(Message const&);
void add_item(Message const&);
void remove_item(Message const&);
void remove_all(Message const&);
void remove_all_session_cookies(Message const&);
HashTable<String> visible_cookie_unique_keys(String const& host) const;
void send_cookie_store_update(String const& host, JsonArray added, JsonArray changed, JsonArray deleted);

View file

@ -620,6 +620,27 @@ public:
return {};
}
virtual void delete_cookies(DevTools::TabDescription const&, Vector<HTTP::Cookie::Cookie> cookies) const override
{
++delete_cookies_call_count;
auto key_matches_cookie = [](HTTP::Cookie::Cookie const& left, HTTP::Cookie::Cookie const& right) {
return left.name == right.name
&& left.domain == right.domain
&& left.path == right.path;
};
for (auto const& cookie_to_delete : cookies) {
for (auto i = fixture_cookies.size(); i > 0; --i) {
if (key_matches_cookie(fixture_cookies[i - 1], cookie_to_delete))
fixture_cookies.remove(i - 1);
}
}
if (on_host_cookie_change)
on_host_cookie_change(move(cookies));
}
virtual void listen_for_host_cookie_changes(DevTools::TabDescription const&, OnHostCookieChange callback) const override
{
++listen_for_cookie_changes_call_count;
@ -1054,6 +1075,7 @@ public:
mutable size_t inspect_tab_call_count { 0 };
mutable size_t cookies_call_count { 0 };
mutable size_t set_cookie_call_count { 0 };
mutable size_t delete_cookies_call_count { 0 };
mutable size_t listen_for_cookie_changes_call_count { 0 };
mutable size_t stop_listening_for_cookie_changes_call_count { 0 };
mutable size_t inspect_accessibility_tree_call_count { 0 };
@ -1429,6 +1451,52 @@ static JsonObject edit_cookie(ProtocolClient& client, StringView cookies_actor,
return client.request(move(request));
}
static JsonObject add_cookie(ProtocolClient& client, StringView cookies_actor, StringView name)
{
JsonObject request;
request.set("to"sv, cookies_actor);
request.set("type"sv, "addItem"sv);
request.set("guid"sv, name);
request.set("host"sv, "https://example.test"sv);
return client.request(move(request));
}
static JsonObject remove_cookie(ProtocolClient& client, StringView cookies_actor, StringView name, StringView domain, StringView path)
{
JsonObject request;
request.set("to"sv, cookies_actor);
request.set("type"sv, "removeItem"sv);
request.set("host"sv, "https://example.test"sv);
request.set("name"sv, cookie_unique_key(name, domain, path));
return client.request(move(request));
}
static JsonObject remove_all_cookies(ProtocolClient& client, StringView cookies_actor, Optional<StringView> domain = {})
{
JsonObject request;
request.set("to"sv, cookies_actor);
request.set("type"sv, "removeAll"sv);
request.set("host"sv, "https://example.test"sv);
if (domain.has_value())
request.set("domain"sv, *domain);
else
request.set("domain"sv, JsonValue {});
return client.request(move(request));
}
static JsonObject remove_all_session_cookies(ProtocolClient& client, StringView cookies_actor, Optional<StringView> domain = {})
{
JsonObject request;
request.set("to"sv, cookies_actor);
request.set("type"sv, "removeAllSessionCookies"sv);
request.set("host"sv, "https://example.test"sv);
if (domain.has_value())
request.set("domain"sv, *domain);
else
request.set("domain"sv, JsonValue {});
return client.request(move(request));
}
TEST_CASE(root_actor_and_connection_errors)
{
auto session = create_session();
@ -1608,10 +1676,10 @@ TEST_CASE(storage_cookie_resource)
auto hosts = cookie_resource.get_object("hosts"sv).release_value();
EXPECT(hosts.has_array("https://example.test"sv));
auto traits = cookie_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());
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 cookies_actor = actor_from(cookie_resource, "actor"sv);
auto fields = client.request(cookies_actor, "getFields"sv).get_array("value"sv).release_value();
@ -1847,6 +1915,79 @@ TEST_CASE(storage_cookie_edit_item)
EXPECT(cookie_matches(session->delegate.fixture_cookies[0], "gamma"sv, "example.test"sv, "/"sv));
}
TEST_CASE(storage_cookie_add_and_remove_items)
{
auto session = create_session();
auto& client = *session->client;
(void)client.read_message();
auto cookies_actor = get_cookies_actor(client);
auto objects = get_cookie_store_objects(client, cookies_actor);
EXPECT_EQ(objects.get_integer<size_t>("total"sv).value(), 0u);
auto response = add_cookie(client, cookies_actor, "devtools-cookie"sv);
EXPECT(response.get("errorString"sv).value().is_null());
EXPECT_EQ(session->delegate.set_cookie_call_count, 1u);
EXPECT_EQ(session->delegate.fixture_cookies.size(), 1u);
EXPECT(cookie_matches(session->delegate.fixture_cookies[0], "devtools-cookie"sv, "example.test"sv, "/"sv));
EXPECT_EQ(session->delegate.fixture_cookies[0].value, "value"sv);
EXPECT(!session->delegate.fixture_cookies[0].persistent);
EXPECT(session->delegate.fixture_cookies[0].secure);
EXPECT_EQ(session->delegate.fixture_cookies[0].same_site, HTTP::Cookie::SameSite::Lax);
auto stores_update = read_packet_with_type(client, "storesUpdate"sv);
auto added_keys = get_cookie_update_keys(stores_update, "added"sv);
EXPECT_EQ(added_keys.size(), 1u);
EXPECT_EQ(added_keys.at(0).as_string(), cookie_unique_key("devtools-cookie"sv, "example.test"sv, "/"sv));
response = remove_cookie(client, cookies_actor, "devtools-cookie"sv, "example.test"sv, "/"sv);
EXPECT_EQ(response.get_string("from"sv).value(), cookies_actor);
EXPECT_EQ(session->delegate.delete_cookies_call_count, 1u);
EXPECT(session->delegate.fixture_cookies.is_empty());
stores_update = read_packet_with_type(client, "storesUpdate"sv);
auto deleted_keys = get_cookie_update_keys(stores_update, "deleted"sv);
EXPECT_EQ(deleted_keys.size(), 1u);
EXPECT_EQ(deleted_keys.at(0).as_string(), cookie_unique_key("devtools-cookie"sv, "example.test"sv, "/"sv));
auto session_cookie = make_cookie("session-cookie"_string, "one"_string, "example.test"_string, "/"_string);
session_cookie.persistent = false;
session->delegate.fixture_cookies.append(session_cookie);
auto persistent_cookie = make_cookie("persistent-cookie"_string, "two"_string, "example.test"_string, "/"_string);
persistent_cookie.persistent = true;
persistent_cookie.expiry_time = UnixDateTime::from_seconds_since_epoch(4000);
session->delegate.fixture_cookies.append(persistent_cookie);
session->delegate.fixture_cookies.append(
make_cookie("ignored-cookie"_string, "three"_string, "other.test"_string, "/"_string));
objects = get_cookie_store_objects(client, cookies_actor);
EXPECT_EQ(objects.get_integer<size_t>("total"sv).value(), 2u);
response = remove_all_session_cookies(client, cookies_actor);
EXPECT_EQ(response.get_string("from"sv).value(), cookies_actor);
EXPECT_EQ(session->delegate.delete_cookies_call_count, 2u);
EXPECT_EQ(session->delegate.fixture_cookies.size(), 2u);
EXPECT(cookie_matches(session->delegate.fixture_cookies[0], "persistent-cookie"sv, "example.test"sv, "/"sv));
stores_update = read_packet_with_type(client, "storesUpdate"sv);
deleted_keys = get_cookie_update_keys(stores_update, "deleted"sv);
EXPECT_EQ(deleted_keys.size(), 1u);
EXPECT_EQ(deleted_keys.at(0).as_string(), cookie_unique_key("session-cookie"sv, "example.test"sv, "/"sv));
response = remove_all_cookies(client, cookies_actor, "example.test"sv);
EXPECT_EQ(response.get_string("from"sv).value(), cookies_actor);
EXPECT_EQ(session->delegate.delete_cookies_call_count, 3u);
EXPECT_EQ(session->delegate.fixture_cookies.size(), 1u);
EXPECT(cookie_matches(session->delegate.fixture_cookies[0], "ignored-cookie"sv, "other.test"sv, "/"sv));
stores_update = read_packet_with_type(client, "storesUpdate"sv);
deleted_keys = get_cookie_update_keys(stores_update, "deleted"sv);
EXPECT_EQ(deleted_keys.size(), 1u);
EXPECT_EQ(deleted_keys.at(0).as_string(), cookie_unique_key("persistent-cookie"sv, "example.test"sv, "/"sv));
}
TEST_CASE(walker_node_picker)
{
auto session = create_session();