ladybird/Libraries/LibWeb/CookieStore/CookieStore.cpp

895 lines
39 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2025, Idan Horowitz <idan.horowitz@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Array.h>
#include <LibURL/Parser.h>
#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>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::CookieStore {
GC_DEFINE_ALLOCATOR(CookieStore);
CookieStore::CookieStore(JS::Realm& realm, PageClient& client)
: DOM::EventTarget(realm)
, m_client(client)
{
}
void CookieStore::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(CookieStore);
Base::initialize(realm);
}
void CookieStore::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_client);
}
// https://cookiestore.spec.whatwg.org/#create-a-cookielistitem
static CookieListItem create_a_cookie_list_item(Cookie::Cookie const& cookie)
{
// 1. Let name be the result of running UTF-8 decode without BOM on cookies name.
// 2. Let value be the result of running UTF-8 decode without BOM on cookies value.
// 3. Return «[ "name" → name, "value" → value ]»
return CookieListItem {
.name = cookie.name,
.value = cookie.value,
};
}
// https://cookiestore.spec.whatwg.org/#normalize-a-cookie-name-or-value
static String normalize(String const& input)
{
// Remove all U+0009 TAB and U+0020 SPACE that are at the start or end of input.
return MUST(input.trim("\t "sv));
}
// https://cookiestore.spec.whatwg.org/#query-cookies
static Vector<CookieListItem> query_cookies(PageClient& client, URL::URL const& url, Optional<String> const& name)
{
// 1. Perform the steps defined in Cookies § Retrieval Model to compute the "cookie-string from a given cookie store"
// with url as request-uri. The cookie-string itself is ignored, but the intermediate cookie-list is used in subsequent steps.
// For the purposes of the steps, the cookie-string is being generated for a "non-HTTP" API.
auto cookie_list = client.page_did_request_all_cookies_cookiestore(url);
// 2. Let list be a new list.
Vector<CookieListItem> list;
// 3. For each cookie in cookie-list, run these steps:
for (auto const& cookie : cookie_list) {
// 1. Assert: cookies http-only-flag is false.
VERIFY(!cookie.http_only);
// 2. If name is non-null:
if (name.has_value()) {
// 1. Normalize name.
auto normalized_name = normalize(name.value());
// 2. Let cookieName be the result of running UTF-8 decode without BOM on cookies name.
// 3. If cookieName does not equal name, then continue.
if (cookie.name != normalized_name)
continue;
}
// 3. Let item be the result of running create a CookieListItem from cookie.
auto item = create_a_cookie_list_item(cookie);
// 4. Append item to list.
list.append(move(item));
}
// 4. Return list.
return list;
}
// https://cookiestore.spec.whatwg.org/#dom-cookiestore-get
GC::Ref<WebIDL::Promise> CookieStore::get(String name)
{
auto& realm = this->realm();
// 1. Let settings be thiss relevant settings object.
auto const& settings = HTML::relevant_settings_object(*this);
// 2. Let origin be settingss origin.
auto const& origin = settings.origin();
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if (origin.is_opaque())
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));
// 4. Let url be settingss creation URL.
auto url = settings.creation_url;
// 5. Let p be a new promise.
auto promise = WebIDL::create_promise(realm);
// 6. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = move(name)]() {
// 1. Let list be the results of running query cookies with url and name.
auto list = query_cookies(client, url, name);
// AD-HOC: Queue a global task to perform the next steps
// Spec issue: https://github.com/whatwg/cookiestore/issues/239
queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, list = move(list)]() {
HTML::TemporaryExecutionContext execution_context { realm };
// 2. If list is failure, then reject p with a TypeError and abort these steps.
// 3. If list is empty, then resolve p with null.
if (list.is_empty())
WebIDL::resolve_promise(realm, promise, JS::js_null());
// 4. Otherwise, resolve p with the first item of list.
else
WebIDL::resolve_promise(realm, promise, Bindings::cookie_list_item_to_value(realm, list[0]));
}));
}));
// 7. Return p.
return promise;
}
// https://cookiestore.spec.whatwg.org/#dom-cookiestore-get-options
GC::Ref<WebIDL::Promise> CookieStore::get(CookieStoreGetOptions const& options)
{
auto& realm = this->realm();
// 1. Let settings be thiss relevant settings object.
auto const& settings = HTML::relevant_settings_object(*this);
// 2. Let origin be settingss origin.
auto const& origin = settings.origin();
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if (origin.is_opaque())
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));
// 4. Let url be settingss creation URL.
auto url = settings.creation_url;
// 5. If options is empty, then return a promise rejected with a TypeError.
if (!options.name.has_value() && !options.url.has_value())
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "CookieStoreGetOptions is empty"sv));
// 6. If options["url"] is present, then run these steps:
if (options.url.has_value()) {
// 1. Let parsed be the result of parsing options["url"] with settingss API base URL.
auto parsed = URL::Parser::basic_parse(options.url.value(), settings.api_base_url());
// AD-HOC: This isn't explicitly mentioned in the specification, but we have to reject invalid URLs as well
if (!parsed.has_value())
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url is invalid"sv));
// 2. If thiss relevant global object is a Window object and parsed does not equal url with exclude fragments
// set to true, then return a promise rejected with a TypeError.
if (is<HTML::Window>(HTML::relevant_global_object(*this)) && !parsed->equals(url, URL::ExcludeFragment::Yes))
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url does not match creation URL"sv));
// 3. If parseds origin and urls origin are not the same origin, then return a promise rejected with a TypeError.
if (parsed->origin() != url.origin())
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url's origin does not match creation URL's origin"sv));
// 4. Set url to parsed.
url = parsed.value();
}
// 7. Let p be a new promise.
auto promise = WebIDL::create_promise(realm);
// 8. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = options.name]() {
// 1. Let list be the results of running query cookies with url and options["name"] with default null.
auto list = query_cookies(client, url, name);
// AD-HOC: Queue a global task to perform the next steps
// Spec issue: https://github.com/whatwg/cookiestore/issues/239
queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, list = move(list)]() {
HTML::TemporaryExecutionContext execution_context { realm };
// 2. If list is failure, then reject p with a TypeError and abort these steps.
// 3. If list is empty, then resolve p with null.
if (list.is_empty())
WebIDL::resolve_promise(realm, promise, JS::js_null());
// 4. Otherwise, resolve p with the first item of list.
else
WebIDL::resolve_promise(realm, promise, Bindings::cookie_list_item_to_value(realm, list[0]));
}));
}));
// 9. Return p.
return promise;
}
static JS::Value cookie_list_to_value(JS::Realm& realm, Vector<CookieListItem> const& cookie_list)
{
return JS::Array::create_from<CookieListItem>(realm, cookie_list, [&](auto const& cookie) {
return Bindings::cookie_list_item_to_value(realm, cookie);
});
}
// https://cookiestore.spec.whatwg.org/#dom-cookiestore-getall
GC::Ref<WebIDL::Promise> CookieStore::get_all(String name)
{
auto& realm = this->realm();
// 1. Let settings be thiss relevant settings object.
auto const& settings = HTML::relevant_settings_object(*this);
// 2. Let origin be settingss origin.
auto const& origin = settings.origin();
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if (origin.is_opaque())
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));
// 4. Let url be settingss creation URL.
auto url = settings.creation_url;
// 5. Let p be a new promise.
auto promise = WebIDL::create_promise(realm);
// 6. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = move(name)]() {
// 1. Let list be the results of running query cookies with url and name.
auto list = query_cookies(client, url, name);
// AD-HOC: Queue a global task to perform the next steps
// Spec issue: https://github.com/whatwg/cookiestore/issues/239
queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, list = move(list)]() {
HTML::TemporaryExecutionContext execution_context { realm };
// 2. If list is failure, then reject p with a TypeError and abort these steps.
// 3. Otherwise, resolve p with list.
WebIDL::resolve_promise(realm, promise, cookie_list_to_value(realm, list));
}));
}));
// 7. Return p.
return promise;
}
// https://cookiestore.spec.whatwg.org/#dom-cookiestore-getall-options
GC::Ref<WebIDL::Promise> CookieStore::get_all(CookieStoreGetOptions const& options)
{
auto& realm = this->realm();
// 1. Let settings be thiss relevant settings object.
auto const& settings = HTML::relevant_settings_object(*this);
// 2. Let origin be settingss origin.
auto const& origin = settings.origin();
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if (origin.is_opaque())
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));
// 4. Let url be settingss creation URL.
auto url = settings.creation_url;
// 5. If options["url"] is present, then run these steps:
if (options.url.has_value()) {
// 1. Let parsed be the result of parsing options["url"] with settingss API base URL.
auto parsed = URL::Parser::basic_parse(options.url.value(), settings.api_base_url());
// AD-HOC: This isn't explicitly mentioned in the specification, but we have to reject invalid URLs as well
if (!parsed.has_value())
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url is invalid"sv));
// 2. If thiss relevant global object is a Window object and parsed does not equal url with exclude fragments
// set to true, then return a promise rejected with a TypeError.
if (is<HTML::Window>(HTML::relevant_global_object(*this)) && !parsed->equals(url, URL::ExcludeFragment::Yes))
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url does not match creation URL"sv));
// 3. If parseds origin and urls origin are not the same origin, then return a promise rejected with a TypeError.
if (parsed->origin() != url.origin())
return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url's origin does not match creation URL's origin"sv));
// 4. Set url to parsed.
url = parsed.value();
}
// 6. Let p be a new promise.
auto promise = WebIDL::create_promise(realm);
// 7. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = options.name]() {
// 1. Let list be the results of running query cookies with url and options["name"] with default null.
auto list = query_cookies(client, url, name);
// AD-HOC: Queue a global task to perform the next steps
// Spec issue: https://github.com/whatwg/cookiestore/issues/239
queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, list = move(list)]() {
HTML::TemporaryExecutionContext execution_context { realm };
// 2. If list is failure, then reject p with a TypeError and abort these steps.
// 3. Otherwise, resolve p with list.
WebIDL::resolve_promise(realm, promise, cookie_list_to_value(realm, list));
}));
}));
// 8. Return p.
return promise;
}
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-layered-cookies#name-cookie-default-path
static Vector<String> cookie_default_path(Vector<String> path)
{
// 1. Assert: path is a non-empty list.
VERIFY(!path.is_empty());
// 2. If path's size is greater than 1, then remove path's last item.
if (path.size() > 1)
path.take_last();
// 3. Otherwise, set path[0] to the empty string.
else
path[0] = ""_string;
// 4. Return path.
return path;
}
// https://fetch.spec.whatwg.org/#serialized-cookie-default-path
static String serialized_cookie_default_path(URL::URL const& url)
{
// 1. Let cloneURL be a clone of url.
auto clone_url = url;
// 2. Set cloneURLs path to the cookie default path of cloneURLs path.
clone_url.set_raw_paths(cookie_default_path(clone_url.paths()));
// 3. Return the URL path serialization of cloneURL.
return clone_url.serialize_path();
}
static constexpr size_t maximum_name_value_pair_size = 4096;
static constexpr size_t maximum_attribute_value_size = 1024;
// https://cookiestore.spec.whatwg.org/#set-a-cookie
static bool set_a_cookie(PageClient& client, URL::URL const& url, String name, String value, Optional<HighResolutionTime::DOMHighResTimeStamp> expires, Optional<String> const& domain, String path, Bindings::CookieSameSite same_site, bool partitioned)
{
// 1. Normalize name.
name = normalize(name);
// 2. Normalize value.
value = normalize(value);
// 3. If name or value contain U+003B (;), any C0 control character except U+0009 TAB, or U+007F DELETE, then return failure.
if (name.contains(';') || value.contains(';'))
return false;
for (auto c = '\x00'; c <= '\x1F'; ++c) {
if (c == '\t')
continue;
if (name.contains(c) || value.contains(c))
return false;
}
if (name.contains('\x7F') || value.contains('\x7F'))
return false;
// 4. If name contains U+003D (=), then return failure.
if (name.contains('='))
return false;
// 5. If names length is 0:
if (name.is_empty()) {
// 1. If value contains U+003D (=), then return failure.
if (value.contains('='))
return false;
// 2. If values length is 0, then return failure.
if (value.is_empty())
return false;
// 3. If value, byte-lowercased, starts with `__host-`, `__host-http-`, `__http-`, or `__secure-`, then return failure.
auto value_byte_lowercased = value.to_ascii_lowercase();
if (value_byte_lowercased.starts_with_bytes("__host-"sv) || value_byte_lowercased.starts_with_bytes("__host-http-"sv) || value_byte_lowercased.starts_with_bytes("__http-"sv) || value_byte_lowercased.starts_with_bytes("__secure-"sv))
return false;
}
// 6. If name, byte-lowercased, starts with `__host-http-` or `__http-`, then return failure.
auto name_byte_lowercased = name.to_ascii_lowercase();
if (name_byte_lowercased.starts_with_bytes("__host-http-"sv) || name_byte_lowercased.starts_with_bytes("__http-"sv))
return false;
// 7. Let encodedName be the result of UTF-8 encoding name.
// 8. Let encodedValue be the result of UTF-8 encoding value.
// 9. If the byte sequence length of encodedName plus the byte sequence length of encodedValue is greater than the
// maximum name/value pair size, then return failure.
if (name.byte_count() + value.byte_count() > maximum_name_value_pair_size)
return false;
// 10. Let host be urls host
auto const& host = url.host();
// 11. Let attributes be a new list.
Cookie::ParsedCookie parsed_cookie {};
parsed_cookie.name = move(name);
parsed_cookie.value = move(value);
// 12. If domain is not null, then run these steps:
if (domain.has_value()) {
// 1. If domain starts with U+002E (.), then return failure.
if (domain->starts_with('.'))
return false;
// 2. If name, byte-lowercased, starts with `__host-`, then return failure.
if (name_byte_lowercased.starts_with_bytes("__host-"sv))
return false;
// 3. If domain is not a registrable domain suffix of and is not equal to host, then return failure.
if (!host.has_value() || !DOM::is_a_registrable_domain_suffix_of_or_is_equal_to(domain.value(), host.value()))
return false;
// 4. Let parsedDomain be the result of host parsing domain.
auto parsed_domain = URL::Parser::parse_host(domain.value());
// 5. Assert: parsedDomain is not failure.
VERIFY(parsed_domain.has_value());
// 6. Let encodedDomain be the result of UTF-8 encoding parsedDomain.
auto encoded_domain = parsed_domain->serialize();
// 7. If the byte sequence length of encodedDomain is greater than the maximum attribute value size, then return failure.
if (encoded_domain.byte_count() > maximum_attribute_value_size)
return false;
// 8. Append `Domain`/encodedDomain to attributes.
parsed_cookie.domain = move(encoded_domain);
}
// 13. If expires is given, then append `Expires`/expires (date serialized) to attributes.
if (expires.has_value()) {
auto expiry_time = UnixDateTime::from_milliseconds_since_epoch(expires.value());
// https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.1
// 3. Let cookie-age-limit be the maximum age of the cookie (which SHOULD be 400 days in the future or sooner, see
// Section 5.5).
auto cookie_age_limit = UnixDateTime::now() + Cookie::maximum_cookie_age;
// 4. If the expiry-time is more than cookie-age-limit, the user agent MUST set the expiry time to cookie-age-limit
// in seconds.
if (expiry_time.seconds_since_epoch() > cookie_age_limit.seconds_since_epoch())
expiry_time = cookie_age_limit;
parsed_cookie.expiry_time_from_expires_attribute = expiry_time;
}
// 14. If path is the empty string, then set path to the serialized cookie default path of url.
if (path.is_empty())
path = serialized_cookie_default_path(url);
// 15. If path does not start with U+002F (/), then return failure.
if (!path.starts_with('/'))
return false;
// 16. If path is not U+002F (/), and name, byte-lowercased, starts with `__host-`, then return failure.
if (path != "/"sv && name_byte_lowercased.starts_with_bytes("__host-"sv))
return false;
// 17. Let encodedPath be the result of UTF-8 encoding path.
// 18. If the byte sequence length of encodedPath is greater than the maximum attribute value size, then return failure.
if (path.byte_count() > maximum_attribute_value_size)
return false;
// 19. Append `Path`/encodedPath to attributes.
parsed_cookie.path = path;
// 20. Append `Secure`/`` to attributes.
parsed_cookie.secure_attribute_present = true;
// 21. Switch on sameSite:
switch (same_site) {
// -> "none"
case Bindings::CookieSameSite::None:
// Append `SameSite`/`None` to attributes.
parsed_cookie.same_site_attribute = Cookie::SameSite::None;
break;
// -> "strict"
case Bindings::CookieSameSite::Strict:
// Append `SameSite`/`Strict` to attributes.
parsed_cookie.same_site_attribute = Cookie::SameSite::Strict;
break;
// -> "lax"
case Bindings::CookieSameSite::Lax:
// Append `SameSite`/`Lax` to attributes.
parsed_cookie.same_site_attribute = Cookie::SameSite::Lax;
break;
}
// FIXME: 22. If partitioned is true, Append `Partitioned`/`` to attributes.
(void)partitioned;
// 23. Perform the steps defined in Cookies § Storage Model for when the user agent "receives a cookie" with url as
// request-uri, encodedName as cookie-name, encodedValue as cookie-value, and attributes as cookie-attribute-list.
// For the purposes of the steps, the newly-created cookie was received from a "non-HTTP" API.
client.page_did_set_cookie(url, parsed_cookie, Cookie::Source::NonHttp);
// 24. Return success.
return true;
}
// https://cookiestore.spec.whatwg.org/#dom-cookiestore-set
GC::Ref<WebIDL::Promise> CookieStore::set(String name, String value)
{
auto& realm = this->realm();
// 1. Let settings be thiss relevant settings object.
auto const& settings = HTML::relevant_settings_object(*this);
// 2. Let origin be settingss origin.
auto const& origin = settings.origin();
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if (origin.is_opaque())
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));
// 4. Let url be settingss creation URL.
auto url = settings.creation_url;
// 5. Let domain be null.
// 6. Let path be "/".
// 7. Let sameSite be strict.
// 8. Let partitioned be false.
// 9. Let p be a new promise.
auto promise = WebIDL::create_promise(realm);
// 10. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = move(name), value = move(value)]() {
// 1. Let r be the result of running set a cookie with url, name, value, domain, path, sameSite, and partitioned.
auto result = set_a_cookie(client, url, move(name), move(value), {}, {}, "/"_string, Bindings::CookieSameSite::Strict, false);
// AD-HOC: Queue a global task to perform the next steps
// Spec issue: https://github.com/whatwg/cookiestore/issues/239
queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, result]() {
HTML::TemporaryExecutionContext execution_context { realm };
// 2. If r is failure, then reject p with a TypeError and abort these steps.
if (!result)
return WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Name or value are malformed"sv));
// 3. Resolve p with undefined.
WebIDL::resolve_promise(realm, promise);
}));
}));
// 11. Return p.
return promise;
}
// https://cookiestore.spec.whatwg.org/#dom-cookiestore-set-options
GC::Ref<WebIDL::Promise> CookieStore::set(CookieInit const& options)
{
auto& realm = this->realm();
// 1. Let settings be thiss relevant settings object.
auto const& settings = HTML::relevant_settings_object(*this);
// 2. Let origin be settingss origin.
auto const& origin = settings.origin();
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if (origin.is_opaque())
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));
// 4. Let url be settingss creation URL.
auto url = settings.creation_url;
// 5. Let p be a new promise.
auto promise = WebIDL::create_promise(realm);
// 6. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), options = options]() {
// 1. Let r be the result of running set a cookie with url, options["name"], options["value"], options["expires"],
// options["domain"], options["path"], options["sameSite"], and options["partitioned"].
auto result = set_a_cookie(client, url, options.name, options.value, options.expires, options.domain, options.path, options.same_site, options.partitioned);
// AD-HOC: Queue a global task to perform the next steps
// Spec issue: https://github.com/whatwg/cookiestore/issues/239
queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, result]() {
HTML::TemporaryExecutionContext execution_context { realm };
// 2. If r is failure, then reject p with a TypeError and abort these steps.
if (!result)
return WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Name, value, domain or path are malformed"sv));
// 3. Resolve p with undefined.
WebIDL::resolve_promise(realm, promise);
}));
}));
// 7. Return p.
return promise;
}
// https://cookiestore.spec.whatwg.org/#delete-a-cookie
static bool delete_a_cookie(PageClient& client, URL::URL const& url, String name, Optional<String> domain, String path, bool partitioned)
{
// 1. Let expires be the earliest representable date represented as a timestamp.
// NOTE: The exact value of expires is not important for the purposes of this algorithm, as long as it is in the past.
HighResolutionTime::DOMHighResTimeStamp expires = UnixDateTime::earliest().milliseconds_since_epoch();
// 2. Normalize name.
name = normalize(name);
// 3. Let value be the empty string.
String value;
// 4. If names length is 0, then set value to any non-empty implementation-defined string.
if (name.is_empty())
value = "ladybird"_string;
// 5. Return the results of running set a cookie with url, name, value, expires, domain, path, "strict", and partitioned.
return set_a_cookie(client, url, move(name), move(value), expires, move(domain), move(path), Bindings::CookieSameSite::Strict, partitioned);
}
// https://cookiestore.spec.whatwg.org/#dom-cookiestore-delete
GC::Ref<WebIDL::Promise> CookieStore::delete_(String name)
{
auto& realm = this->realm();
// 1. Let settings be thiss relevant settings object.
auto const& settings = HTML::relevant_settings_object(*this);
// 2. Let origin be settingss origin.
auto const& origin = settings.origin();
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if (origin.is_opaque())
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));
// 4. Let url be settingss creation URL.
auto url = settings.creation_url;
// 5. Let p be a new promise.
auto promise = WebIDL::create_promise(realm);
// 6. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = move(name)]() {
// 1. Let r be the result of running delete a cookie with url, name, null, "/", and true.
auto result = delete_a_cookie(client, url, move(name), {}, "/"_string, true);
// AD-HOC: Queue a global task to perform the next steps
// Spec issue: https://github.com/whatwg/cookiestore/issues/239
queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, result]() {
HTML::TemporaryExecutionContext execution_context { realm };
// 2. If r is failure, then reject p with a TypeError and abort these steps.
if (!result)
return WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Name is malformed"sv));
// 3. Resolve p with undefined.
WebIDL::resolve_promise(realm, promise);
}));
}));
// 7. Return p.
return promise;
}
// https://cookiestore.spec.whatwg.org/#dom-cookiestore-delete-options
GC::Ref<WebIDL::Promise> CookieStore::delete_(CookieStoreDeleteOptions const& options)
{
auto& realm = this->realm();
// 1. Let settings be thiss relevant settings object.
auto const& settings = HTML::relevant_settings_object(*this);
// 2. Let origin be settingss origin.
auto const& origin = settings.origin();
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if (origin.is_opaque())
return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));
// 4. Let url be settingss creation URL.
auto url = settings.creation_url;
// 5. Let p be a new promise.
auto promise = WebIDL::create_promise(realm);
// 6. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), options = options]() {
// 1. Let r be the result of running delete a cookie with url, options["name"], options["domain"], options["path"],
// and options["partitioned"].
auto result = delete_a_cookie(client, url, options.name, options.domain, options.path, options.partitioned);
// AD-HOC: Queue a global task to perform the next steps
// Spec issue: https://github.com/whatwg/cookiestore/issues/239
queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, result]() {
HTML::TemporaryExecutionContext execution_context { realm };
// 2. If r is failure, then reject p with a TypeError and abort these steps.
if (!result)
return WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Name is malformed"sv));
// 3. Resolve p with undefined.
WebIDL::resolve_promise(realm, promise);
}));
}));
// 7. Return p.
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);
}));
}
}