diff --git a/Libraries/LibWeb/CookieStore/CookieStore.cpp b/Libraries/LibWeb/CookieStore/CookieStore.cpp index 28569aad6bc..9b3ec27f03f 100644 --- a/Libraries/LibWeb/CookieStore/CookieStore.cpp +++ b/Libraries/LibWeb/CookieStore/CookieStore.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -671,4 +672,171 @@ GC::Ref 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 observable_changes(URL::URL const& url, Vector 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 Algorithm’s 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 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 changed_list; + Vector deleted_list; +}; + +// https://cookiestore.spec.whatwg.org/#prepare-lists +static PreparedLists prepare_lists(Vector const& changes) +{ + // 1. Let changedList be a new list. + Vector changed_list; + + // 2. Let deletedList be a new list. + Vector 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 change’s cookie. + auto item = create_a_cookie_list_item(change.cookie); + + // 2. If change’s 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 const& all_changes) +{ + auto& realm = this->realm(); + + // 1. Let url be window’s relevant settings object’s 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 window’s 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 event’s changed attribute to changedList. + event_init.changed = move(changed_list); + + // 6. Set event’s deleted attribute to deletedList. + event_init.deleted = move(deleted_list); + + // 1. Let event be the result of creating an Event using CookieChangeEvent. + // 2. Set event’s type attribute to type. + auto event = CookieChangeEvent::create(realm, HTML::EventNames::change, event_init); + + // 3. Set event’s bubbles and cancelable attributes to false. + event->set_bubbles(false); + event->set_cancelable(false); + + // 7. Dispatch event at target. + this->dispatch_event(event); + })); +} + } diff --git a/Libraries/LibWeb/CookieStore/CookieStore.h b/Libraries/LibWeb/CookieStore/CookieStore.h index 3e071523974..de6c2b75514 100644 --- a/Libraries/LibWeb/CookieStore/CookieStore.h +++ b/Libraries/LibWeb/CookieStore/CookieStore.h @@ -63,6 +63,11 @@ public: GC::Ref delete_(String name); GC::Ref delete_(CookieStoreDeleteOptions const&); + void set_onchange(WebIDL::CallbackType*); + WebIDL::CallbackType* onchange(); + + void process_cookie_changes(Vector const&); + private: CookieStore(JS::Realm&, PageClient&); diff --git a/Libraries/LibWeb/CookieStore/CookieStore.idl b/Libraries/LibWeb/CookieStore/CookieStore.idl index 1f4c3cc8211..396d05f22ec 100644 --- a/Libraries/LibWeb/CookieStore/CookieStore.idl +++ b/Libraries/LibWeb/CookieStore/CookieStore.idl @@ -53,6 +53,8 @@ interface CookieStore : EventTarget { Promise delete(USVString name); Promise delete(CookieStoreDeleteOptions options); + + [Exposed=Window] attribute EventHandler onchange; }; // https://cookiestore.spec.whatwg.org/#Window diff --git a/Libraries/LibWebView/CookieJar.cpp b/Libraries/LibWebView/CookieJar.cpp index 4b6471c2874..badf943ace9 100644 --- a/Libraries/LibWebView/CookieJar.cpp +++ b/Libraries/LibWebView/CookieJar.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace WebView { @@ -585,9 +586,25 @@ void CookieJar::TransientStorage::set_cookies(Cookies cookies) purge_expired_cookies(); } +static void notify_cookies_changed(Vector 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 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; } diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index f84d73f8ea4..fffbff1d0f9 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -1314,4 +1315,14 @@ void ConnectionFromClient::system_time_zone_changed() Unicode::clear_system_time_zone_cache(); } +void ConnectionFromClient::cookies_changed(Vector cookies) +{ + for (auto& navigable : Web::HTML::all_navigables()) { + auto window = navigable->active_window(); + if (!window) + return; + window->cookie_store()->process_cookie_changes(cookies); + } +} + } diff --git a/Services/WebContent/ConnectionFromClient.h b/Services/WebContent/ConnectionFromClient.h index 2d3c334e7f1..fbed02794ea 100644 --- a/Services/WebContent/ConnectionFromClient.h +++ b/Services/WebContent/ConnectionFromClient.h @@ -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) override; NonnullOwnPtr m_page_host; diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index e7b6e2e6402..89c29d4c86c 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -128,4 +128,5 @@ endpoint WebContentServer set_user_style(u64 page_id, String source) =| system_time_zone_changed() =| + cookies_changed(Vector cookies) =| }