LibWeb+LibWebView: Implement emitting CookieChangeEvents
Some checks are pending
CI / macOS, arm64, Sanitizer, Clang (push) Waiting to run
CI / Linux, x86_64, Fuzzers, Clang (push) Waiting to run
CI / Linux, x86_64, Sanitizer, GNU (push) Waiting to run
CI / Linux, x86_64, Sanitizer, Clang (push) Waiting to run
Package the js repl as a binary artifact / Linux, arm64 (push) Waiting to run
Package the js repl as a binary artifact / macOS, arm64 (push) Waiting to run
Package the js repl as a binary artifact / Linux, x86_64 (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

This commit is contained in:
Idan Horowitz 2025-08-07 18:20:13 +03:00 committed by Tim Flynn
commit 81e3afd1fd
Notes: github-actions[bot] 2025-08-08 17:11:07 +00:00
7 changed files with 213 additions and 1 deletions

View file

@ -9,6 +9,7 @@
#include <LibWeb/Bindings/CookieStorePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWeb/CookieStore/CookieChangeEvent.h>
#include <LibWeb/CookieStore/CookieStore.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
@ -671,4 +672,171 @@ GC::Ref<WebIDL::Promise> CookieStore::delete_(CookieStoreDeleteOptions const& op
return promise;
}
void CookieStore::set_onchange(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::change, event_handler);
}
WebIDL::CallbackType* CookieStore::onchange()
{
return event_handler_attribute(HTML::EventNames::change);
}
// https://cookiestore.spec.whatwg.org/#cookie-change
struct CookieChange {
enum class Type {
Changed,
Deleted,
};
Cookie::Cookie cookie;
Type type;
};
// https://cookiestore.spec.whatwg.org/#observable-changes
static Vector<CookieChange> observable_changes(URL::URL const& url, Vector<Cookie::Cookie> const& changes)
{
// The observable changes for url are the set of cookie changes to cookies in a cookie store which meet the
// requirements in step 1 of Cookies § Retrieval Algorithms steps to compute the "cookie-string from a given
// cookie store" with url as request-uri, for a "non-HTTP" API.
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-14#name-retrieval-algorithm
auto canonicalized_domain = Cookie::canonicalize_domain(url);
if (!canonicalized_domain.has_value())
return {};
// FIXME: The retrieval's same-site status is "same-site" if the Document's "site for cookies" is same-site with the
// top-level origin as defined in Section 5.2.1 (otherwise it is "cross-site"), and the retrieval's type is "non-HTTP".
auto is_same_site_retrieval = true;
auto now = UnixDateTime::now();
// 1. Let cookie-list be the set of cookies from the cookie store that meets all of the following requirements:
Vector<CookieChange> observable_changes;
for (auto const& cookie : changes) {
// * Either:
// The cookie's host-only-flag is true and the canonicalized host of the retrieval's URI is identical to
// the cookie's domain.
bool is_host_only_and_has_identical_domain = cookie.host_only && (canonicalized_domain.value() == cookie.domain);
// Or:
// The cookie's host-only-flag is false and the canonicalized host of the retrieval's URI domain-matches
// the cookie's domain.
bool is_not_host_only_and_domain_matches = !cookie.host_only && Web::Cookie::domain_matches(canonicalized_domain.value(), cookie.domain);
if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches)
continue;
// * The retrieval's URI's path path-matches the cookie's path.
if (!Cookie::path_matches(url.serialize_path(), cookie.path))
continue;
// * If the cookie's secure-only-flag is true, then the retrieval's URI must denote a "secure" connection (as
// defined by the user agent).
if (cookie.secure && url.scheme() != "https"sv && url.scheme() != "wss"sv)
continue;
// * If the cookie's http-only-flag is true, then exclude the cookie if the retrieval's type is "non-HTTP".
if (cookie.http_only)
continue;
// * If the cookie's same-site-flag is not "None" and the retrieval's same-site status is "cross-site", then
// exclude the cookie unless all of the following conditions are met:
// * The retrieval's type is "HTTP".
// * The same-site-flag is "Lax" or "Default".
// * The HTTP request associated with the retrieval uses a "safe" method.
// * The target browsing context of the HTTP request associated with the retrieval is the active browsing context
// or a top-level traversable.
if (cookie.same_site != Cookie::SameSite::None && !is_same_site_retrieval)
continue;
// A cookie change is a cookie and a type (either changed or deleted):
// - A cookie which is removed due to an insertion of another cookie with the same name, domain, and path is ignored.
// - A newly-created cookie which is not immediately evicted is considered changed.
// - A newly-created cookie which is immediately evicted is considered deleted.
// - A cookie which is otherwise evicted or removed is considered deleted
observable_changes.append({ cookie, cookie.expiry_time < now ? CookieChange::Type::Deleted : CookieChange::Type::Changed });
}
return observable_changes;
}
struct PreparedLists {
Vector<CookieListItem> changed_list;
Vector<CookieListItem> deleted_list;
};
// https://cookiestore.spec.whatwg.org/#prepare-lists
static PreparedLists prepare_lists(Vector<CookieChange> const& changes)
{
// 1. Let changedList be a new list.
Vector<CookieListItem> changed_list;
// 2. Let deletedList be a new list.
Vector<CookieListItem> deleted_list;
// 3. For each change in changes, run these steps:
for (auto const& change : changes) {
// 1. Let item be the result of running create a CookieListItem from changes cookie.
auto item = create_a_cookie_list_item(change.cookie);
// 2. If changes type is changed, then append item to changedList.
if (change.type == CookieChange::Type::Changed)
changed_list.append(move(item));
// 3. Otherwise, run these steps:
else {
// 1. Set item["value"] to undefined.
item.value.clear();
// 2. Append item to deletedList.
deleted_list.append(move(item));
}
}
// 4. Return changedList and deletedList.
return { move(changed_list), move(deleted_list) };
}
// https://cookiestore.spec.whatwg.org/#process-cookie-changes
void CookieStore::process_cookie_changes(Vector<Cookie::Cookie> const& all_changes)
{
auto& realm = this->realm();
// 1. Let url be windows relevant settings objects creation URL.
auto url = HTML::relevant_settings_object(*this).creation_url;
// 2. Let changes be the observable changes for url.
auto changes = observable_changes(url, all_changes);
// 3. If changes is empty, then continue.
if (changes.is_empty())
return;
// 4. Queue a global task on the DOM manipulation task source given window to fire a change event named "change"
// with changes at windows CookieStore.
queue_global_task(HTML::Task::Source::DOMManipulation, realm.global_object(), GC::create_function(realm.heap(), [this, &realm, changes = move(changes)]() {
HTML::TemporaryExecutionContext execution_context { realm };
// https://cookiestore.spec.whatwg.org/#fire-a-change-event
// 4. Let changedList and deletedList be the result of running prepare lists from changes.
auto [changed_list, deleted_list] = prepare_lists(changes);
CookieChangeEventInit event_init = {};
// 5. Set events changed attribute to changedList.
event_init.changed = move(changed_list);
// 6. Set events deleted attribute to deletedList.
event_init.deleted = move(deleted_list);
// 1. Let event be the result of creating an Event using CookieChangeEvent.
// 2. Set events type attribute to type.
auto event = CookieChangeEvent::create(realm, HTML::EventNames::change, event_init);
// 3. Set events bubbles and cancelable attributes to false.
event->set_bubbles(false);
event->set_cancelable(false);
// 7. Dispatch event at target.
this->dispatch_event(event);
}));
}
}

View file

@ -63,6 +63,11 @@ public:
GC::Ref<WebIDL::Promise> delete_(String name);
GC::Ref<WebIDL::Promise> delete_(CookieStoreDeleteOptions const&);
void set_onchange(WebIDL::CallbackType*);
WebIDL::CallbackType* onchange();
void process_cookie_changes(Vector<Cookie::Cookie> const&);
private:
CookieStore(JS::Realm&, PageClient&);

View file

@ -53,6 +53,8 @@ interface CookieStore : EventTarget {
Promise<undefined> delete(USVString name);
Promise<undefined> delete(CookieStoreDeleteOptions options);
[Exposed=Window] attribute EventHandler onchange;
};
// https://cookiestore.spec.whatwg.org/#Window

View file

@ -14,6 +14,7 @@
#include <LibURL/URL.h>
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWebView/CookieJar.h>
#include <LibWebView/WebContentClient.h>
namespace WebView {
@ -585,9 +586,25 @@ void CookieJar::TransientStorage::set_cookies(Cookies cookies)
purge_expired_cookies();
}
static void notify_cookies_changed(Vector<Web::Cookie::Cookie> cookies)
{
WebContentClient::for_each_client([&](WebContentClient& client) {
client.async_cookies_changed(move(cookies));
return IterationDecision::Continue;
});
}
void CookieJar::TransientStorage::set_cookie(CookieStorageKey key, Web::Cookie::Cookie cookie)
{
auto now = UnixDateTime::now();
// AD-HOC: Skip adding immediately-expiring cookies (i.e., only allow updating to immediately-expiring) to prevent firing deletion events for them
// Spec issue: https://github.com/whatwg/cookiestore/issues/282
if (cookie.expiry_time < now && !m_cookies.contains(key))
return;
m_cookies.set(key, cookie);
// We skip notifying about updating expired cookies, as they will be notified as being expired immediately after instead
if (cookie.expiry_time >= now)
notify_cookies_changed({ cookie });
m_dirty_cookies.set(move(key), move(cookie));
}
@ -607,7 +624,14 @@ UnixDateTime CookieJar::TransientStorage::purge_expired_cookies(Optional<AK::Dur
}
auto is_expired = [&](auto const&, auto const& cookie) { return cookie.expiry_time < now; };
m_cookies.remove_all_matching(is_expired);
auto removed_entries = m_cookies.take_all_matching(is_expired);
if (!removed_entries.is_empty()) {
Vector<Web::Cookie::Cookie> removed_cookies;
removed_cookies.ensure_capacity(removed_entries.size());
for (auto const& entry : removed_entries)
removed_cookies.unchecked_append(move(entry.value));
notify_cookies_changed(move(removed_cookies));
}
return now;
}

View file

@ -24,6 +24,7 @@
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Parser/ErrorReporter.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CookieStore/CookieStore.h>
#include <LibWeb/DOM/Attr.h>
#include <LibWeb/DOM/CharacterData.h>
#include <LibWeb/DOM/Document.h>
@ -1314,4 +1315,14 @@ void ConnectionFromClient::system_time_zone_changed()
Unicode::clear_system_time_zone_cache();
}
void ConnectionFromClient::cookies_changed(Vector<Web::Cookie::Cookie> cookies)
{
for (auto& navigable : Web::HTML::all_navigables()) {
auto window = navigable->active_window();
if (!window)
return;
window->cookie_store()->process_cookie_changes(cookies);
}
}
}

View file

@ -156,6 +156,7 @@ private:
virtual void paste(u64 page_id, String text) override;
virtual void system_time_zone_changed() override;
virtual void cookies_changed(Vector<Web::Cookie::Cookie>) override;
NonnullOwnPtr<PageHost> m_page_host;

View file

@ -128,4 +128,5 @@ endpoint WebContentServer
set_user_style(u64 page_id, String source) =|
system_time_zone_changed() =|
cookies_changed(Vector<Web::Cookie::Cookie> cookies) =|
}