LibDevTools: Report cookie storage changes
Firefox keeps the Storage table current through storesUpdate messages from the storage actor. Forward browser cookie change notifications to the cookies actor and classify visible rows against a fresh cookie snapshot. The existing cookie-change notifications report cookies that are relevant to a specific page, whereas DevTools wants all cookies relevant for a host, so we end up having to provide two different sets of cookies to `notify_cookies_changed()`.
This commit is contained in:
parent
20f922cbdb
commit
47204bd3dd
9 changed files with 258 additions and 8 deletions
|
|
@ -81,9 +81,20 @@ CookiesActor::CookiesActor(DevToolsServer& devtools, String name, WeakPtr<TabAct
|
|||
: Actor(devtools, move(name))
|
||||
, m_tab(move(tab))
|
||||
{
|
||||
if (auto tab = m_tab.strong_ref()) {
|
||||
devtools.delegate().listen_for_host_cookie_changes(
|
||||
tab->description(),
|
||||
weak_callback(*this, [](auto& self, Vector<HTTP::Cookie::Cookie> cookies) {
|
||||
self.on_cookies_changed(move(cookies));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
CookiesActor::~CookiesActor() = default;
|
||||
CookiesActor::~CookiesActor()
|
||||
{
|
||||
if (auto tab = m_tab.strong_ref())
|
||||
devtools().delegate().stop_listening_for_host_cookie_changes(tab->description());
|
||||
}
|
||||
|
||||
Optional<String> CookiesActor::host() const
|
||||
{
|
||||
|
|
@ -201,6 +212,13 @@ void CookiesActor::get_store_objects(Message const& message)
|
|||
cookies.append(move(cookie));
|
||||
}
|
||||
|
||||
if (!requested_names.has_value()) {
|
||||
HashTable<String> visible_keys;
|
||||
for (auto const& cookie : cookies)
|
||||
visible_keys.set(cookie_unique_key(cookie));
|
||||
m_visible_cookie_unique_keys.set(*host, move(visible_keys));
|
||||
}
|
||||
|
||||
quick_sort(cookies, [](auto const& left, auto const& right) {
|
||||
if (left.name != right.name)
|
||||
return left.name < right.name;
|
||||
|
|
@ -234,4 +252,85 @@ void CookiesActor::get_store_objects(Message const& message)
|
|||
send_response(message, move(response));
|
||||
}
|
||||
|
||||
HashTable<String> CookiesActor::visible_cookie_unique_keys(String const& host) const
|
||||
{
|
||||
HashTable<String> visible_keys;
|
||||
auto tab = m_tab.strong_ref();
|
||||
if (!tab)
|
||||
return visible_keys;
|
||||
|
||||
for (auto& cookie : devtools().delegate().cookies(tab->description())) {
|
||||
if (cookie_matches_storage_host(cookie, host))
|
||||
visible_keys.set(cookie_unique_key(cookie));
|
||||
}
|
||||
return visible_keys;
|
||||
}
|
||||
|
||||
void CookiesActor::send_cookie_store_update(String const& host, JsonArray added, JsonArray changed, JsonArray deleted)
|
||||
{
|
||||
JsonObject update;
|
||||
|
||||
auto append_update = [&](StringView update_type, JsonArray keys) {
|
||||
if (keys.is_empty())
|
||||
return;
|
||||
|
||||
JsonObject hosts;
|
||||
hosts.set(host, move(keys));
|
||||
|
||||
JsonObject cookies;
|
||||
cookies.set("cookies"sv, move(hosts));
|
||||
|
||||
update.set(update_type, move(cookies));
|
||||
};
|
||||
|
||||
append_update("added"sv, move(added));
|
||||
append_update("changed"sv, move(changed));
|
||||
append_update("deleted"sv, move(deleted));
|
||||
|
||||
JsonObject message;
|
||||
message.set("type"sv, "storesUpdate"sv);
|
||||
message.set("data"sv, move(update));
|
||||
send_message(move(message));
|
||||
}
|
||||
|
||||
void CookiesActor::on_cookies_changed(Vector<HTTP::Cookie::Cookie> changed_cookies)
|
||||
{
|
||||
auto storage_host = host();
|
||||
if (!storage_host.has_value())
|
||||
return;
|
||||
|
||||
HashTable<String> old_visible_keys;
|
||||
if (auto it = m_visible_cookie_unique_keys.find(*storage_host); it != m_visible_cookie_unique_keys.end())
|
||||
old_visible_keys = it->value;
|
||||
|
||||
auto current_visible_keys = visible_cookie_unique_keys(*storage_host);
|
||||
|
||||
JsonArray added;
|
||||
JsonArray changed;
|
||||
JsonArray deleted;
|
||||
|
||||
for (auto const& cookie : changed_cookies) {
|
||||
if (!cookie_matches_storage_host(cookie, *storage_host))
|
||||
continue;
|
||||
|
||||
auto unique_key = cookie_unique_key(cookie);
|
||||
auto was_visible = old_visible_keys.contains(unique_key);
|
||||
auto is_visible = current_visible_keys.contains(unique_key);
|
||||
|
||||
if (is_visible && was_visible)
|
||||
changed.must_append(unique_key);
|
||||
else if (is_visible)
|
||||
added.must_append(unique_key);
|
||||
else if (was_visible)
|
||||
deleted.must_append(unique_key);
|
||||
}
|
||||
|
||||
m_visible_cookie_unique_keys.set(*storage_host, move(current_visible_keys));
|
||||
|
||||
if (added.is_empty() && changed.is_empty() && deleted.is_empty())
|
||||
return;
|
||||
|
||||
send_cookie_store_update(*storage_host, move(added), move(changed), move(deleted));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/HashTable.h>
|
||||
#include <AK/JsonArray.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <LibDevTools/Actor.h>
|
||||
#include <LibDevTools/Forward.h>
|
||||
#include <LibHTTP/Forward.h>
|
||||
|
||||
namespace DevTools {
|
||||
|
||||
|
|
@ -21,6 +25,7 @@ public:
|
|||
|
||||
Optional<String> host() const;
|
||||
JsonObject serialize_storage() const;
|
||||
void on_cookies_changed(Vector<HTTP::Cookie::Cookie>);
|
||||
|
||||
private:
|
||||
CookiesActor(DevToolsServer&, String name, WeakPtr<TabActor>);
|
||||
|
|
@ -29,8 +34,11 @@ private:
|
|||
|
||||
void get_fields(Message const&);
|
||||
void get_store_objects(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);
|
||||
|
||||
WeakPtr<TabActor> m_tab;
|
||||
HashMap<String, HashTable<String>> m_visible_cookie_unique_keys;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ public:
|
|||
virtual void reload_tab(TabDescription const&, bool bypass_cache) const { (void)bypass_cache; }
|
||||
virtual void traverse_the_history_by_delta(TabDescription const&, int) const { }
|
||||
virtual Vector<HTTP::Cookie::Cookie> cookies(TabDescription const&) const { return {}; }
|
||||
using OnHostCookieChange = Function<void(Vector<HTTP::Cookie::Cookie>)>;
|
||||
virtual void listen_for_host_cookie_changes(TabDescription const&, OnHostCookieChange) const { }
|
||||
virtual void stop_listening_for_host_cookie_changes(TabDescription const&) const { }
|
||||
|
||||
using OnTabInspectionComplete = Function<void(ErrorOr<JsonValue>)>;
|
||||
virtual void inspect_tab(TabDescription const&, OnTabInspectionComplete) const { }
|
||||
|
|
|
|||
|
|
@ -1827,6 +1827,18 @@ Vector<HTTP::Cookie::Cookie> Application::cookies(DevTools::TabDescription const
|
|||
return Application::cookie_jar().get_all_cookies();
|
||||
}
|
||||
|
||||
void Application::listen_for_host_cookie_changes(DevTools::TabDescription const& description, OnHostCookieChange on_host_cookie_change) const
|
||||
{
|
||||
if (auto view = ViewImplementation::find_view_by_id(description.id); view.has_value())
|
||||
view->listen_for_host_cookie_changes(move(on_host_cookie_change));
|
||||
}
|
||||
|
||||
void Application::stop_listening_for_host_cookie_changes(DevTools::TabDescription const& description) const
|
||||
{
|
||||
if (auto view = ViewImplementation::find_view_by_id(description.id); view.has_value())
|
||||
view->stop_listening_for_host_cookie_changes();
|
||||
}
|
||||
|
||||
void Application::inspect_tab(DevTools::TabDescription const& description, OnTabInspectionComplete on_complete) const
|
||||
{
|
||||
auto view = ViewImplementation::find_view_by_id(description.id);
|
||||
|
|
|
|||
|
|
@ -270,6 +270,8 @@ private:
|
|||
virtual void reload_tab(DevTools::TabDescription const&, bool) const override;
|
||||
virtual void traverse_the_history_by_delta(DevTools::TabDescription const&, int) const override;
|
||||
virtual Vector<HTTP::Cookie::Cookie> cookies(DevTools::TabDescription const&) const override;
|
||||
virtual void listen_for_host_cookie_changes(DevTools::TabDescription const&, OnHostCookieChange) const override;
|
||||
virtual void stop_listening_for_host_cookie_changes(DevTools::TabDescription const&) const override;
|
||||
virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete) const override;
|
||||
virtual void inspect_accessibility_tree(DevTools::TabDescription const&, OnAccessibilityTreeInspectionComplete) const override;
|
||||
virtual void listen_for_dom_properties(DevTools::TabDescription const&, OnDOMNodePropertiesReceived) const override;
|
||||
|
|
|
|||
|
|
@ -639,17 +639,25 @@ void CookieJar::TransientStorage::send_cookie_changed_notifications(ReadonlySpan
|
|||
return IterationDecision::Continue;
|
||||
|
||||
HashTable<String> changed_domains;
|
||||
Vector<HTTP::Cookie::Cookie> matching_cookies;
|
||||
Vector<HTTP::Cookie::Cookie> page_cookies;
|
||||
Vector<HTTP::Cookie::Cookie> host_cookies;
|
||||
|
||||
for (auto const& cookie : cookies) {
|
||||
if (inform_web_view_about_changed_domains)
|
||||
changed_domains.set(cookie.value.domain);
|
||||
|
||||
if (HTTP::Cookie::cookie_matches_url(cookie.value, view.url(), *retrieval_host_canonical))
|
||||
matching_cookies.append(cookie.value);
|
||||
page_cookies.append(cookie.value);
|
||||
|
||||
if (cookie.value.host_only) {
|
||||
if (cookie.value.domain == *retrieval_host_canonical)
|
||||
host_cookies.append(cookie.value);
|
||||
} else if (HTTP::Cookie::domain_matches(*retrieval_host_canonical, cookie.value.domain)) {
|
||||
host_cookies.append(cookie.value);
|
||||
}
|
||||
}
|
||||
|
||||
view.notify_cookies_changed(changed_domains, matching_cookies);
|
||||
view.notify_cookies_changed(changed_domains, page_cookies, host_cookies);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -379,15 +379,27 @@ void ViewImplementation::set_preferred_motion(Web::CSS::PreferredMotion motion)
|
|||
client().async_set_preferred_motion(page_id(), motion);
|
||||
}
|
||||
|
||||
void ViewImplementation::notify_cookies_changed(HashTable<String> const& changed_domains, ReadonlySpan<HTTP::Cookie::Cookie> cookies)
|
||||
void ViewImplementation::notify_cookies_changed(HashTable<String> const& changed_domains, ReadonlySpan<HTTP::Cookie::Cookie> page_cookies, ReadonlySpan<HTTP::Cookie::Cookie> host_cookies)
|
||||
{
|
||||
for (auto const& domain : changed_domains) {
|
||||
if (auto document_index = m_document_cookie_version_indices.get(domain); document_index.has_value())
|
||||
Core::increment_shared_version(m_document_cookie_version_buffer, *document_index);
|
||||
}
|
||||
|
||||
if (!cookies.is_empty())
|
||||
client().async_cookies_changed(page_id(), cookies);
|
||||
if (!page_cookies.is_empty())
|
||||
client().async_cookies_changed(page_id(), page_cookies);
|
||||
if (m_on_host_cookie_change)
|
||||
m_on_host_cookie_change(Vector<HTTP::Cookie::Cookie> { host_cookies });
|
||||
}
|
||||
|
||||
void ViewImplementation::listen_for_host_cookie_changes(DevTools::DevToolsDelegate::OnHostCookieChange on_cookie_change)
|
||||
{
|
||||
m_on_host_cookie_change = move(on_cookie_change);
|
||||
}
|
||||
|
||||
void ViewImplementation::stop_listening_for_host_cookie_changes()
|
||||
{
|
||||
m_on_host_cookie_change = nullptr;
|
||||
}
|
||||
|
||||
ErrorOr<Core::SharedVersionIndex> ViewImplementation::ensure_document_cookie_version_index(Badge<WebContentClient>, String const& domain)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,9 @@ public:
|
|||
void set_preferred_contrast(Web::CSS::PreferredContrast);
|
||||
void set_preferred_motion(Web::CSS::PreferredMotion);
|
||||
|
||||
void notify_cookies_changed(HashTable<String> const& changed_domains, ReadonlySpan<HTTP::Cookie::Cookie>);
|
||||
void notify_cookies_changed(HashTable<String> const& changed_domains, ReadonlySpan<HTTP::Cookie::Cookie> page_cookies, ReadonlySpan<HTTP::Cookie::Cookie> host_cookies);
|
||||
void listen_for_host_cookie_changes(DevTools::DevToolsDelegate::OnHostCookieChange);
|
||||
void stop_listening_for_host_cookie_changes();
|
||||
ErrorOr<Core::SharedVersionIndex> ensure_document_cookie_version_index(Badge<WebContentClient>, String const&);
|
||||
Optional<Core::SharedVersion> document_cookie_version(URL::URL const&) const;
|
||||
|
||||
|
|
@ -458,6 +460,7 @@ protected:
|
|||
|
||||
Core::AnonymousBuffer m_document_cookie_version_buffer;
|
||||
HashMap<String, Core::SharedVersionIndex> m_document_cookie_version_indices;
|
||||
DevTools::DevToolsDelegate::OnHostCookieChange m_on_host_cookie_change;
|
||||
|
||||
// FIXME: Reconcile this ID with `page_id`. The latter is only unique per WebContent connection, whereas the view ID
|
||||
// is required to be globally unique for Firefox DevTools.
|
||||
|
|
|
|||
|
|
@ -574,6 +574,18 @@ public:
|
|||
return fixture_cookies;
|
||||
}
|
||||
|
||||
virtual void listen_for_host_cookie_changes(DevTools::TabDescription const&, OnHostCookieChange callback) const override
|
||||
{
|
||||
++listen_for_cookie_changes_call_count;
|
||||
on_host_cookie_change = move(callback);
|
||||
}
|
||||
|
||||
virtual void stop_listening_for_host_cookie_changes(DevTools::TabDescription const&) const override
|
||||
{
|
||||
++stop_listening_for_cookie_changes_call_count;
|
||||
on_host_cookie_change = nullptr;
|
||||
}
|
||||
|
||||
virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete callback) const override
|
||||
{
|
||||
++inspect_tab_call_count;
|
||||
|
|
@ -967,6 +979,12 @@ public:
|
|||
on_node_picker_event(move(event));
|
||||
}
|
||||
|
||||
void emit_cookie_change(Vector<HTTP::Cookie::Cookie> cookies) const
|
||||
{
|
||||
VERIFY(on_host_cookie_change);
|
||||
on_host_cookie_change(move(cookies));
|
||||
}
|
||||
|
||||
mutable Function<void(WebView::DOMNodeProperties)> on_dom_node_properties;
|
||||
mutable Function<void(WebView::Mutation)> on_dom_mutation;
|
||||
mutable Function<void(Web::CSS::StyleSheetIdentifier const&, String)> on_style_sheet_source;
|
||||
|
|
@ -976,6 +994,7 @@ public:
|
|||
mutable Function<void(u64, ByteBuffer)> on_network_response_body_received;
|
||||
mutable Function<void(DevToolsDelegate::NetworkRequestCompleteData)> on_network_request_finished;
|
||||
mutable Function<void(DevToolsDelegate::NodePickerEvent)> on_node_picker_event;
|
||||
mutable Function<void(Vector<HTTP::Cookie::Cookie>)> on_host_cookie_change;
|
||||
|
||||
struct NavigationListener {
|
||||
Function<void(String)> on_navigation_started;
|
||||
|
|
@ -988,6 +1007,8 @@ public:
|
|||
|
||||
mutable size_t inspect_tab_call_count { 0 };
|
||||
mutable size_t 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 };
|
||||
mutable size_t listen_for_dom_properties_call_count { 0 };
|
||||
mutable size_t stop_listening_for_dom_properties_call_count { 0 };
|
||||
|
|
@ -1130,6 +1151,7 @@ private:
|
|||
|| *type == "pickerNodePicked"sv
|
||||
|| *type == "pickerNodePreviewed"sv
|
||||
|| *type == "tabListChanged"sv
|
||||
|| *type == "storesUpdate"sv
|
||||
|| *type == "target-available-form"sv
|
||||
|| *type == "target-destroyed-form"sv;
|
||||
}
|
||||
|
|
@ -1291,6 +1313,45 @@ static JsonObject read_packet_with_type(ProtocolClient& client, StringView packe
|
|||
}
|
||||
}
|
||||
|
||||
static String get_cookies_actor(ProtocolClient& client)
|
||||
{
|
||||
auto tab_actor = actor_from(get_tab(client), "actor"sv);
|
||||
auto watcher_actor = actor_from(client.request(tab_actor, "getWatcher"sv), "actor"sv);
|
||||
|
||||
JsonObject watch_resources;
|
||||
watch_resources.set("to"sv, watcher_actor);
|
||||
watch_resources.set("type"sv, "watchResources"sv);
|
||||
JsonArray resource_types;
|
||||
resource_types.must_append("cookies"sv);
|
||||
watch_resources.set("resourceTypes"sv, move(resource_types));
|
||||
EXPECT_EQ(client.request(move(watch_resources)).get_string("from"sv).value(), watcher_actor);
|
||||
|
||||
auto cookie_resource = read_resource(client, "cookies"sv);
|
||||
return actor_from(cookie_resource, "actor"sv);
|
||||
}
|
||||
|
||||
static JsonObject get_cookie_store_objects(ProtocolClient& client, StringView cookies_actor, StringView host = "https://example.test"sv)
|
||||
{
|
||||
JsonObject get_store_objects;
|
||||
get_store_objects.set("to"sv, cookies_actor);
|
||||
get_store_objects.set("type"sv, "getStoreObjects"sv);
|
||||
get_store_objects.set("host"sv, host);
|
||||
get_store_objects.set("names"sv, JsonValue {});
|
||||
JsonObject options;
|
||||
options.set("sessionString"sv, "Session"sv);
|
||||
get_store_objects.set("options"sv, move(options));
|
||||
return client.request(move(get_store_objects));
|
||||
}
|
||||
|
||||
static JsonArray get_cookie_update_keys(JsonObject const& stores_update, StringView update_type, StringView host = "https://example.test"sv)
|
||||
{
|
||||
return stores_update.get_object("data"sv)
|
||||
->get_object(update_type)
|
||||
->get_object("cookies"sv)
|
||||
->get_array(host)
|
||||
.release_value();
|
||||
}
|
||||
|
||||
TEST_CASE(root_actor_and_connection_errors)
|
||||
{
|
||||
auto session = create_session();
|
||||
|
|
@ -1608,6 +1669,48 @@ TEST_CASE(storage_cookie_store_objects)
|
|||
EXPECT_EQ(filtered_data.at(0).as_object().get_string("name"sv).value(), "beta"sv);
|
||||
}
|
||||
|
||||
TEST_CASE(storage_cookie_change_events)
|
||||
{
|
||||
auto session = create_session();
|
||||
auto& client = *session->client;
|
||||
(void)client.read_message();
|
||||
|
||||
auto initial_cookie = make_cookie("alpha"_string, "one"_string, "example.test"_string, "/"_string);
|
||||
session->delegate.fixture_cookies.append(initial_cookie);
|
||||
|
||||
auto cookies_actor = get_cookies_actor(client);
|
||||
EXPECT_EQ(session->delegate.listen_for_cookie_changes_call_count, 1u);
|
||||
|
||||
auto objects = get_cookie_store_objects(client, cookies_actor);
|
||||
EXPECT_EQ(objects.get_integer<size_t>("total"sv).value(), 1u);
|
||||
|
||||
auto added_cookie = make_cookie("beta"_string, "two"_string, "example.test"_string, "/"_string);
|
||||
session->delegate.fixture_cookies.append(added_cookie);
|
||||
session->delegate.emit_cookie_change({ added_cookie });
|
||||
auto stores_update = read_packet_with_type(client, "storesUpdate"sv);
|
||||
EXPECT_EQ(stores_update.get_string("from"sv).value(), cookies_actor);
|
||||
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("beta"sv, "example.test"sv, "/"sv));
|
||||
|
||||
auto changed_cookie = initial_cookie;
|
||||
changed_cookie.value = "updated"_string;
|
||||
session->delegate.fixture_cookies[0] = changed_cookie;
|
||||
session->delegate.emit_cookie_change({ changed_cookie });
|
||||
stores_update = read_packet_with_type(client, "storesUpdate"sv);
|
||||
auto changed_keys = get_cookie_update_keys(stores_update, "changed"sv);
|
||||
EXPECT_EQ(changed_keys.size(), 1u);
|
||||
EXPECT_EQ(changed_keys.at(0).as_string(), cookie_unique_key("alpha"sv, "example.test"sv, "/"sv));
|
||||
|
||||
auto deleted_cookie = changed_cookie;
|
||||
session->delegate.fixture_cookies.remove(0);
|
||||
session->delegate.emit_cookie_change({ deleted_cookie });
|
||||
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("alpha"sv, "example.test"sv, "/"sv));
|
||||
}
|
||||
|
||||
TEST_CASE(walker_node_picker)
|
||||
{
|
||||
auto session = create_session();
|
||||
|
|
|
|||
Loading…
Reference in a new issue