ladybird/Libraries/LibWeb/CookieStore/CookieStore.cpp
2025-08-08 13:09:58 -04:00

318 lines
14 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/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/#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 given, then run these steps:
if (name.has_value()) {
// 1. Let cookieName be the result of running UTF-8 decode without BOM on cookies name.
// 2. If cookieName does not equal name, then continue.
if (cookie.name != name.value())
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"_string));
// 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"_string));
// 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"] (if present).
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"_string));
// 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"_string));
// 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"] (if present).
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;
}
}