From fef255ee2264a41a0c1b397c3a90456afc46ee46 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 27 Oct 2022 12:56:22 -0400 Subject: [PATCH] Browser: Store cookies in a LibSQL database :^) Currently, cookies are ephemeral and only survive for the lifetime of Browser instance. This will make Browser instead store cookies in a SQL database for persisted access. --- Userland/Applications/Browser/CMakeLists.txt | 3 +- Userland/Applications/Browser/CookieJar.cpp | 391 ++++++++++++++----- Userland/Applications/Browser/CookieJar.h | 62 +-- Userland/Applications/Browser/Database.cpp | 96 +++++ Userland/Applications/Browser/Database.h | 77 ++++ Userland/Applications/Browser/main.cpp | 5 +- 6 files changed, 513 insertions(+), 121 deletions(-) create mode 100644 Userland/Applications/Browser/Database.cpp create mode 100644 Userland/Applications/Browser/Database.h diff --git a/Userland/Applications/Browser/CMakeLists.txt b/Userland/Applications/Browser/CMakeLists.txt index 265b0bc5e1..df474a785f 100644 --- a/Userland/Applications/Browser/CMakeLists.txt +++ b/Userland/Applications/Browser/CMakeLists.txt @@ -16,6 +16,7 @@ set(SOURCES ConsoleWidget.cpp CookieJar.cpp CookiesModel.cpp + Database.cpp DownloadWidget.cpp ElementSizePreviewWidget.cpp History.cpp @@ -36,5 +37,5 @@ set(GENERATED_SOURCES ) serenity_app(Browser ICON app-browser) -target_link_libraries(Browser PRIVATE LibCore LibWebView LibWeb LibProtocol LibGUI LibDesktop LibConfig LibGfx LibIPC LibJS LibLocale LibMain LibSyntax) +target_link_libraries(Browser PRIVATE LibCore LibWebView LibWeb LibProtocol LibGUI LibDesktop LibConfig LibGfx LibIPC LibJS LibLocale LibMain LibSyntax LibSQL) link_with_locale_data(Browser) diff --git a/Userland/Applications/Browser/CookieJar.cpp b/Userland/Applications/Browser/CookieJar.cpp index 681370339e..494839c545 100644 --- a/Userland/Applications/Browser/CookieJar.cpp +++ b/Userland/Applications/Browser/CookieJar.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Tim Flynn + * Copyright (c) 2021-2022, Tim Flynn * Copyright (c) 2022, the SerenityOS developers. * Copyright (c) 2022, Tobias Christiansen * @@ -7,15 +7,67 @@ */ #include "CookieJar.h" +#include "Database.h" #include #include #include #include #include +#include +#include +#include #include namespace Browser { +ErrorOr CookieJar::create(Database& database) +{ + Statements statements {}; + + statements.create_table = TRY(database.prepare_statement(R"#( + CREATE TABLE IF NOT EXISTS Cookies ( + name TEXT, + value TEXT, + same_site INTEGER, + creation_time INTEGER, + last_access_time INTEGER, + expiry_time INTEGER, + domain TEXT, + path TEXT, + secure BOOLEAN, + http_only BOOLEAN, + host_only BOOLEAN, + persistent BOOLEAN + );)#"sv)); + + statements.update_cookie = TRY(database.prepare_statement(R"#( + UPDATE Cookies SET + value=?, + same_site=?, + creation_time=?, + last_access_time=?, + expiry_time=?, + secure=?, + http_only=?, + host_only=?, + persistent=? + WHERE ((name = ?) AND (domain = ?) AND (path = ?));)#"sv)); + + statements.insert_cookie = TRY(database.prepare_statement("INSERT INTO Cookies VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"sv)); + statements.expire_cookie = TRY(database.prepare_statement("DELETE FROM Cookies WHERE (expiry_time < ?);"sv)); + statements.select_cookie = TRY(database.prepare_statement("SELECT * FROM Cookies WHERE ((name = ?) AND (domain = ?) AND (path = ?));"sv)); + statements.select_all_cookies = TRY(database.prepare_statement("SELECT * FROM Cookies;"sv)); + + return CookieJar { database, move(statements) }; +} + +CookieJar::CookieJar(Database& database, Statements statements) + : m_database(database) + , m_statements(move(statements)) +{ + m_database.execute_statement(m_statements.create_table, {}, {}, {}); +} + DeprecatedString CookieJar::get_cookie(const URL& url, Web::Cookie::Source source) { purge_expired_cookies(); @@ -46,64 +98,68 @@ void CookieJar::set_cookie(const URL& url, Web::Cookie::ParsedCookie const& pars return; store_cookie(parsed_cookie, url, move(domain.value()), source); - purge_expired_cookies(); } // This is based on https://www.rfc-editor.org/rfc/rfc6265#section-5.3 as store_cookie() below // however the whole ParsedCookie->Cookie conversion is skipped. void CookieJar::update_cookie(Web::Cookie::Cookie cookie) { - // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: - CookieStorageKey key { cookie.name, cookie.domain, cookie.path }; + select_cookie_from_database( + move(cookie), - if (auto old_cookie = m_cookies.find(key); old_cookie != m_cookies.end()) { - // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. - cookie.creation_time = old_cookie->value.creation_time; - // Remove the old-cookie from the cookie store. - m_cookies.remove(old_cookie); - } + // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: + [this](auto& cookie, auto old_cookie) { + // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. + cookie.creation_time = old_cookie.creation_time; - // 12. Insert the newly created cookie into the cookie store. - m_cookies.set(key, move(cookie)); + // Remove the old-cookie from the cookie store. + // NOTE: Rather than deleting then re-inserting this cookie, we update it in-place. + update_cookie_in_database(cookie); + }, - purge_expired_cookies(); + // 12. Insert the newly created cookie into the cookie store. + [this](auto cookie) { + insert_cookie_into_database(cookie); + }); } -void CookieJar::dump_cookies() const +void CookieJar::dump_cookies() { - constexpr auto key_color = "\033[34;1m"sv; - constexpr auto attribute_color = "\033[33m"sv; - constexpr auto no_color = "\033[0m"sv; + static constexpr auto key_color = "\033[34;1m"sv; + static constexpr auto attribute_color = "\033[33m"sv; + static constexpr auto no_color = "\033[0m"sv; StringBuilder builder; - builder.appendff("{} cookies stored\n", m_cookies.size()); + size_t total_cookies { 0 }; - for (auto const& cookie : m_cookies) { - builder.appendff("{}{}{} - ", key_color, cookie.key.name, no_color); - builder.appendff("{}{}{} - ", key_color, cookie.key.domain, no_color); - builder.appendff("{}{}{}\n", key_color, cookie.key.path, no_color); + select_all_cookies_from_database([&](auto cookie) { + ++total_cookies; - builder.appendff("\t{}Value{} = {}\n", attribute_color, no_color, cookie.value.value); - builder.appendff("\t{}CreationTime{} = {}\n", attribute_color, no_color, cookie.value.creation_time.to_deprecated_string()); - builder.appendff("\t{}LastAccessTime{} = {}\n", attribute_color, no_color, cookie.value.last_access_time.to_deprecated_string()); - builder.appendff("\t{}ExpiryTime{} = {}\n", attribute_color, no_color, cookie.value.expiry_time.to_deprecated_string()); - builder.appendff("\t{}Secure{} = {:s}\n", attribute_color, no_color, cookie.value.secure); - builder.appendff("\t{}HttpOnly{} = {:s}\n", attribute_color, no_color, cookie.value.http_only); - builder.appendff("\t{}HostOnly{} = {:s}\n", attribute_color, no_color, cookie.value.host_only); - builder.appendff("\t{}Persistent{} = {:s}\n", attribute_color, no_color, cookie.value.persistent); - builder.appendff("\t{}SameSite{} = {:s}\n", attribute_color, no_color, Web::Cookie::same_site_to_string(cookie.value.same_site)); - } + builder.appendff("{}{}{} - ", key_color, cookie.name, no_color); + builder.appendff("{}{}{} - ", key_color, cookie.domain, no_color); + builder.appendff("{}{}{}\n", key_color, cookie.path, no_color); - dbgln("{}", builder.build()); + builder.appendff("\t{}Value{} = {}\n", attribute_color, no_color, cookie.value); + builder.appendff("\t{}CreationTime{} = {}\n", attribute_color, no_color, cookie.creation_time.to_deprecated_string()); + builder.appendff("\t{}LastAccessTime{} = {}\n", attribute_color, no_color, cookie.last_access_time.to_deprecated_string()); + builder.appendff("\t{}ExpiryTime{} = {}\n", attribute_color, no_color, cookie.expiry_time.to_deprecated_string()); + builder.appendff("\t{}Secure{} = {:s}\n", attribute_color, no_color, cookie.secure); + builder.appendff("\t{}HttpOnly{} = {:s}\n", attribute_color, no_color, cookie.http_only); + builder.appendff("\t{}HostOnly{} = {:s}\n", attribute_color, no_color, cookie.host_only); + builder.appendff("\t{}Persistent{} = {:s}\n", attribute_color, no_color, cookie.persistent); + builder.appendff("\t{}SameSite{} = {:s}\n", attribute_color, no_color, Web::Cookie::same_site_to_string(cookie.same_site)); + }); + + dbgln("{} cookies stored\n{}", total_cookies, builder.build()); } -Vector CookieJar::get_all_cookies() const +Vector CookieJar::get_all_cookies() { Vector cookies; - cookies.ensure_capacity(m_cookies.size()); - for (auto const& cookie : m_cookies) - cookies.unchecked_append(cookie.value); + select_all_cookies_from_database([&](auto cookie) { + cookies.append(move(cookie)); + }); return cookies; } @@ -115,15 +171,7 @@ Vector CookieJar::get_all_cookies(URL const& url) if (!domain.has_value()) return {}; - auto cookie_list = get_matching_cookies(url, domain.value(), Web::Cookie::Source::Http, MatchingCookiesSpecMode::WebDriver); - - Vector cookies; - cookies.ensure_capacity(cookie_list.size()); - - for (auto const& cookie : cookie_list) - cookies.unchecked_append(cookie); - - return cookies; + return get_matching_cookies(url, domain.value(), Web::Cookie::Source::Http, MatchingCookiesSpecMode::WebDriver); } Optional CookieJar::get_named_cookie(URL const& url, DeprecatedString const& name) @@ -287,93 +335,256 @@ void CookieJar::store_cookie(Web::Cookie::ParsedCookie const& parsed_cookie, con if (source != Web::Cookie::Source::Http && cookie.http_only) return; - // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: - CookieStorageKey key { cookie.name, cookie.domain, cookie.path }; + select_cookie_from_database( + move(cookie), - if (auto old_cookie = m_cookies.find(key); old_cookie != m_cookies.end()) { - // If the newly created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is set, abort these - // steps and ignore the newly created cookie entirely. - if (source != Web::Cookie::Source::Http && old_cookie->value.http_only) - return; + // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: + [this, source](auto& cookie, auto old_cookie) { + // If the newly created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is set, abort these + // steps and ignore the newly created cookie entirely. + if (source != Web::Cookie::Source::Http && old_cookie.http_only) + return; - // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. - cookie.creation_time = old_cookie->value.creation_time; + // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. + cookie.creation_time = old_cookie.creation_time; - // Remove the old-cookie from the cookie store. - m_cookies.remove(old_cookie); - } + // Remove the old-cookie from the cookie store. + // NOTE: Rather than deleting then re-inserting this cookie, we update it in-place. + update_cookie_in_database(cookie); + }, - // 12. Insert the newly created cookie into the cookie store. - m_cookies.set(key, move(cookie)); + // 12. Insert the newly created cookie into the cookie store. + [this](auto cookie) { + insert_cookie_into_database(cookie); + }); } -Vector CookieJar::get_matching_cookies(const URL& url, DeprecatedString const& canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode) +Vector CookieJar::get_matching_cookies(const URL& url, DeprecatedString const& canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode) { // https://tools.ietf.org/html/rfc6265#section-5.4 - auto now = Core::DateTime::now(); - // 1. Let cookie-list be the set of cookies from the cookie store that meets all of the following requirements: - Vector cookie_list; + Vector cookie_list; - for (auto& cookie : m_cookies) { + select_all_cookies_from_database([&](auto cookie) { // Either: The cookie's host-only-flag is true and the canonicalized request-host is identical to the cookie's domain. // Or: The cookie's host-only-flag is false and the canonicalized request-host domain-matches the cookie's domain. - bool is_host_only_and_has_identical_domain = cookie.value.host_only && (canonicalized_domain == cookie.value.domain); - bool is_not_host_only_and_domain_matches = !cookie.value.host_only && domain_matches(canonicalized_domain, cookie.value.domain); + bool is_host_only_and_has_identical_domain = cookie.host_only && (canonicalized_domain == cookie.domain); + bool is_not_host_only_and_domain_matches = !cookie.host_only && domain_matches(canonicalized_domain, cookie.domain); if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches) - continue; + return; // The request-uri's path path-matches the cookie's path. - if (!path_matches(url.path(), cookie.value.path)) - continue; + if (!path_matches(url.path(), cookie.path)) + return; // If the cookie's secure-only-flag is true, then the request-uri's scheme must denote a "secure" protocol. - if (cookie.value.secure && (url.scheme() != "https")) - continue; + if (cookie.secure && (url.scheme() != "https")) + return; // If the cookie's http-only-flag is true, then exclude the cookie if the cookie-string is being generated for a "non-HTTP" API. - if (cookie.value.http_only && (source != Web::Cookie::Source::Http)) - continue; + if (cookie.http_only && (source != Web::Cookie::Source::Http)) + return; // NOTE: The WebDriver spec expects only step 1 above to be executed to match cookies. if (mode == MatchingCookiesSpecMode::WebDriver) { - cookie_list.append(cookie.value); - continue; + cookie_list.append(move(cookie)); + return; } // 2. The user agent SHOULD sort the cookie-list in the following order: // - Cookies with longer paths are listed before cookies with shorter paths. // - Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times. - cookie_list.insert_before_matching(cookie.value, [&cookie](auto& entry) { - if (cookie.value.path.length() > entry.path.length()) { + auto cookie_path_length = cookie.path.length(); + auto cookie_creation_time = cookie.creation_time.timestamp(); + + cookie_list.insert_before_matching(move(cookie), [cookie_path_length, cookie_creation_time](auto const& entry) { + if (cookie_path_length > entry.path.length()) { return true; - } else if (cookie.value.path.length() == entry.path.length()) { - if (cookie.value.creation_time.timestamp() < entry.creation_time.timestamp()) + } else if (cookie_path_length == entry.path.length()) { + if (cookie_creation_time < entry.creation_time.timestamp()) return true; } return false; }); + }); - // 3. Update the last-access-time of each cookie in the cookie-list to the current date and time. - cookie.value.last_access_time = now; + // 3. Update the last-access-time of each cookie in the cookie-list to the current date and time. + auto now = Core::DateTime::now(); + + for (auto& cookie : cookie_list) { + cookie.last_access_time = now; + update_cookie_in_database(cookie); } return cookie_list; } -void CookieJar::purge_expired_cookies() +static ErrorOr parse_cookie(Span row) { - time_t now = Core::DateTime::now().timestamp(); - Vector keys_to_evict; + if (row.size() != 12) + return Error::from_string_view("Incorrect number of columns to parse cookie"sv); - for (auto const& cookie : m_cookies) { - if (cookie.value.expiry_time.timestamp() < now) - keys_to_evict.append(cookie.key); + size_t index = 0; + + auto convert_text = [&](auto& field, StringView name) -> ErrorOr { + auto const& value = row[index++]; + if (value.type() != SQL::SQLType::Text) + return Error::from_string_view(name); + + field = value.to_deprecated_string(); + return {}; + }; + + auto convert_bool = [&](auto& field, StringView name) -> ErrorOr { + auto const& value = row[index++]; + if (value.type() != SQL::SQLType::Boolean) + return Error::from_string_view(name); + + field = value.to_bool().value(); + return {}; + }; + + auto convert_time = [&](auto& field, StringView name) -> ErrorOr { + auto const& value = row[index++]; + if (value.type() != SQL::SQLType::Float) + return Error::from_string_view(name); + + // FIXME: Support 64-bit types in LibSQL::Value. + auto time = static_cast(value.to_double().value()); + field = Core::DateTime::from_timestamp(time); + return {}; + }; + + auto convert_same_site = [&](auto& field, StringView name) -> ErrorOr { + auto const& value = row[index++]; + if (value.type() != SQL::SQLType::Integer) + return Error::from_string_view(name); + + auto same_site = value.to_int().value(); + if (same_site > to_underlying(Web::Cookie::SameSite::Lax)) + return Error::from_string_view(name); + + field = static_cast(same_site); + return {}; + }; + + Web::Cookie::Cookie cookie; + TRY(convert_text(cookie.name, "name"sv)); + TRY(convert_text(cookie.value, "value"sv)); + TRY(convert_same_site(cookie.same_site, "same_site"sv)); + TRY(convert_time(cookie.creation_time, "creation_time"sv)); + TRY(convert_time(cookie.last_access_time, "last_access_time"sv)); + TRY(convert_time(cookie.expiry_time, "expiry_time"sv)); + TRY(convert_text(cookie.domain, "domain"sv)); + TRY(convert_text(cookie.path, "path"sv)); + TRY(convert_bool(cookie.secure, "secure"sv)); + TRY(convert_bool(cookie.http_only, "http_only"sv)); + TRY(convert_bool(cookie.host_only, "host_only"sv)); + TRY(convert_bool(cookie.persistent, "persistent"sv)); + + return cookie; +} + +void CookieJar::insert_cookie_into_database(Web::Cookie::Cookie const& cookie) +{ + m_database.execute_statement( + m_statements.insert_cookie, {}, [this]() { purge_expired_cookies(); }, {}, + cookie.name, + cookie.value, + to_underlying(cookie.same_site), + static_cast(cookie.creation_time.timestamp()), // FIXME: Support 64-bit types in LibSQL::Value. + static_cast(cookie.last_access_time.timestamp()), // FIXME: Support 64-bit types in LibSQL::Value. + static_cast(cookie.expiry_time.timestamp()), // FIXME: Support 64-bit types in LibSQL::Value. + cookie.domain, + cookie.path, + cookie.secure, + cookie.http_only, + cookie.host_only, + cookie.persistent); +} + +void CookieJar::update_cookie_in_database(Web::Cookie::Cookie const& cookie) +{ + m_database.execute_statement( + m_statements.update_cookie, {}, [this]() { purge_expired_cookies(); }, {}, + cookie.value, + to_underlying(cookie.same_site), + static_cast(cookie.creation_time.timestamp()), // FIXME: Support 64-bit types in LibSQL::Value. + static_cast(cookie.last_access_time.timestamp()), // FIXME: Support 64-bit types in LibSQL::Value. + static_cast(cookie.expiry_time.timestamp()), // FIXME: Support 64-bit types in LibSQL::Value. + cookie.secure, + cookie.http_only, + cookie.host_only, + cookie.persistent, + cookie.name, + cookie.domain, + cookie.path); +} + +struct WrappedCookie : public RefCounted { + explicit WrappedCookie(Web::Cookie::Cookie cookie_) + : RefCounted() + , cookie(move(cookie_)) + { } - for (auto const& key : keys_to_evict) - m_cookies.remove(key); + Web::Cookie::Cookie cookie; + bool had_any_results { false }; +}; + +void CookieJar::select_cookie_from_database(Web::Cookie::Cookie cookie, OnCookieFound on_result, OnCookieNotFound on_complete_without_results) +{ + auto wrapped_cookie = make_ref_counted(move(cookie)); + + m_database.execute_statement( + m_statements.select_cookie, + [on_result = move(on_result), wrapped_cookie = wrapped_cookie](auto row) { + if (auto selected_cookie = parse_cookie(row); selected_cookie.is_error()) + dbgln("Failed to parse cookie '{}': {}", selected_cookie.error(), row); + else + on_result(wrapped_cookie->cookie, selected_cookie.release_value()); + + wrapped_cookie->had_any_results = true; + }, + [on_complete_without_results = move(on_complete_without_results), wrapped_cookie = wrapped_cookie]() { + if (!wrapped_cookie->had_any_results) + on_complete_without_results(move(wrapped_cookie->cookie)); + }, + {}, + wrapped_cookie->cookie.name, + wrapped_cookie->cookie.domain, + wrapped_cookie->cookie.path); } +void CookieJar::select_all_cookies_from_database(OnSelectAllCookiesResult on_result) +{ + // FIXME: Make surrounding APIs asynchronous. + auto promise = Core::Promise::construct(); + + m_database.execute_statement( + m_statements.select_all_cookies, + [on_result = move(on_result)](auto row) { + if (auto cookie = parse_cookie(row); cookie.is_error()) + dbgln("Failed to parse cookie '{}': {}", cookie.error(), row); + else + on_result(cookie.release_value()); + }, + [&]() { + promise->resolve({}); + }, + [&](auto) { + promise->resolve({}); + }); + + promise->await(); +} + +void CookieJar::purge_expired_cookies() +{ + auto now = Core::DateTime::now().timestamp(); + + // FIXME: Support 64-bit types in LibSQL::Value. + m_database.execute_statement(m_statements.expire_cookie, {}, {}, {}, static_cast(now)); +} } diff --git a/Userland/Applications/Browser/CookieJar.h b/Userland/Applications/Browser/CookieJar.h index 6019ffa65f..8c9e7d7854 100644 --- a/Userland/Applications/Browser/CookieJar.h +++ b/Userland/Applications/Browser/CookieJar.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Tim Flynn + * Copyright (c) 2021-2022, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -7,34 +7,42 @@ #pragma once #include +#include #include #include -#include #include +#include #include #include namespace Browser { -struct CookieStorageKey { - bool operator==(CookieStorageKey const&) const = default; - - DeprecatedString name; - DeprecatedString domain; - DeprecatedString path; -}; +class Database; class CookieJar { + struct Statements { + SQL::StatementID create_table { 0 }; + SQL::StatementID insert_cookie { 0 }; + SQL::StatementID update_cookie { 0 }; + SQL::StatementID expire_cookie { 0 }; + SQL::StatementID select_cookie { 0 }; + SQL::StatementID select_all_cookies { 0 }; + }; + public: + static ErrorOr create(Database& database); + DeprecatedString get_cookie(const URL& url, Web::Cookie::Source source); void set_cookie(const URL& url, Web::Cookie::ParsedCookie const& parsed_cookie, Web::Cookie::Source source); void update_cookie(Web::Cookie::Cookie); - void dump_cookies() const; - Vector get_all_cookies() const; + void dump_cookies(); + Vector get_all_cookies(); Vector get_all_cookies(URL const& url); Optional get_named_cookie(URL const& url, DeprecatedString const& name); private: + CookieJar(Database& database, Statements statements); + static Optional canonicalize_domain(const URL& url); static bool domain_matches(DeprecatedString const& string, DeprecatedString const& domain_string); static bool path_matches(DeprecatedString const& request_path, DeprecatedString const& cookie_path); @@ -46,26 +54,22 @@ private: }; void store_cookie(Web::Cookie::ParsedCookie const& parsed_cookie, const URL& url, DeprecatedString canonicalized_domain, Web::Cookie::Source source); - Vector get_matching_cookies(const URL& url, DeprecatedString const& canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode = MatchingCookiesSpecMode::RFC6265); + Vector get_matching_cookies(const URL& url, DeprecatedString const& canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode = MatchingCookiesSpecMode::RFC6265); + + void insert_cookie_into_database(Web::Cookie::Cookie const& cookie); + void update_cookie_in_database(Web::Cookie::Cookie const& cookie); + + using OnCookieFound = Function; + using OnCookieNotFound = Function; + void select_cookie_from_database(Web::Cookie::Cookie cookie, OnCookieFound on_result, OnCookieNotFound on_complete_without_results); + + using OnSelectAllCookiesResult = Function; + void select_all_cookies_from_database(OnSelectAllCookiesResult on_result); + void purge_expired_cookies(); - HashMap m_cookies; -}; - -} - -namespace AK { - -template<> -struct Traits : public GenericTraits { - static unsigned hash(Browser::CookieStorageKey const& key) - { - unsigned hash = 0; - hash = pair_int_hash(hash, string_hash(key.name.characters(), key.name.length())); - hash = pair_int_hash(hash, string_hash(key.domain.characters(), key.domain.length())); - hash = pair_int_hash(hash, string_hash(key.path.characters(), key.path.length())); - return hash; - } + Database& m_database; + Statements m_statements; }; } diff --git a/Userland/Applications/Browser/Database.cpp b/Userland/Applications/Browser/Database.cpp new file mode 100644 index 0000000000..12a05628d3 --- /dev/null +++ b/Userland/Applications/Browser/Database.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Database.h" +#include +#include + +namespace Browser { + +static constexpr auto database_name = "Browser"sv; + +ErrorOr> Database::create() +{ + auto sql_client = TRY(SQL::SQLClient::try_create()); + return create(move(sql_client)); +} + +ErrorOr> Database::create(NonnullRefPtr sql_client) +{ + auto connection_id = sql_client->connect(database_name); + if (!connection_id.has_value()) + return Error::from_string_view("Could not connect to SQL database"sv); + + return adopt_nonnull_ref_or_enomem(new (nothrow) Database(move(sql_client), *connection_id)); +} + +Database::Database(NonnullRefPtr sql_client, SQL::ConnectionID connection_id) + : m_sql_client(move(sql_client)) + , m_connection_id(connection_id) +{ + m_sql_client->on_execution_success = [this](auto statement_id, auto execution_id, auto has_results, auto, auto, auto) { + if (has_results) + return; + + if (auto it = m_pending_executions.find({ statement_id, execution_id }); it != m_pending_executions.end()) { + auto in_progress_statement = move(it->value); + m_pending_executions.remove(it); + + if (in_progress_statement.on_complete) + in_progress_statement.on_complete(); + } + }; + + m_sql_client->on_next_result = [this](auto statement_id, auto execution_id, auto row) { + if (auto it = m_pending_executions.find({ statement_id, execution_id }); it != m_pending_executions.end()) { + if (it->value.on_result) + it->value.on_result(row); + } + }; + + m_sql_client->on_results_exhausted = [this](auto statement_id, auto execution_id, auto) { + if (auto it = m_pending_executions.find({ statement_id, execution_id }); it != m_pending_executions.end()) { + auto in_progress_statement = move(it->value); + m_pending_executions.remove(it); + + if (in_progress_statement.on_complete) + in_progress_statement.on_complete(); + } + }; + + m_sql_client->on_execution_error = [this](auto statement_id, auto execution_id, auto, auto const& message) { + if (auto it = m_pending_executions.find({ statement_id, execution_id }); it != m_pending_executions.end()) { + auto in_progress_statement = move(it->value); + m_pending_executions.remove(it); + + if (in_progress_statement.on_error) + in_progress_statement.on_error(message); + } + }; +} + +ErrorOr Database::prepare_statement(StringView statement) +{ + if (auto statement_id = m_sql_client->prepare_statement(m_connection_id, statement); statement_id.has_value()) + return *statement_id; + return Error::from_string_view(statement); +} + +void Database::execute_statement(SQL::StatementID statement_id, Vector placeholder_values, PendingExecution pending_execution) +{ + Core::deferred_invoke([this, statement_id, placeholder_values = move(placeholder_values), pending_execution = move(pending_execution)]() mutable { + auto execution_id = m_sql_client->execute_statement(statement_id, move(placeholder_values)); + if (!execution_id.has_value()) { + if (pending_execution.on_error) + pending_execution.on_error("Could not execute statement"sv); + return; + } + + m_pending_executions.set({ statement_id, *execution_id }, move(pending_execution)); + }); +} + +} diff --git a/Userland/Applications/Browser/Database.h b/Userland/Applications/Browser/Database.h new file mode 100644 index 0000000000..74eb58bac3 --- /dev/null +++ b/Userland/Applications/Browser/Database.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Browser { + +class Database : public RefCounted { + using OnResult = Function)>; + using OnComplete = Function; + using OnError = Function; + +public: + static ErrorOr> create(); + static ErrorOr> create(NonnullRefPtr); + + ErrorOr prepare_statement(StringView statement); + + template + void execute_statement(SQL::StatementID statement_id, OnResult on_result, OnComplete on_complete, OnError on_error, PlaceholderValues&&... placeholder_values) + { + PendingExecution pending_execution { + .on_result = move(on_result), + .on_complete = move(on_complete), + .on_error = move(on_error), + }; + + Vector values { SQL::Value(forward(placeholder_values))... }; + execute_statement(statement_id, move(values), move(pending_execution)); + } + +private: + struct ExecutionKey { + constexpr bool operator==(ExecutionKey const&) const = default; + + SQL::StatementID statement_id { 0 }; + SQL::ExecutionID execution_id { 0 }; + }; + + struct PendingExecution { + OnResult on_result { nullptr }; + OnComplete on_complete { nullptr }; + OnError on_error { nullptr }; + }; + + struct ExecutionKeyTraits : public Traits { + static constexpr unsigned hash(ExecutionKey const& key) + { + return pair_int_hash(u64_hash(key.statement_id), u64_hash(key.execution_id)); + } + }; + + Database(NonnullRefPtr sql_client, SQL::ConnectionID connection_id); + void execute_statement(SQL::StatementID statement_id, Vector placeholder_values, PendingExecution pending_execution); + + NonnullRefPtr m_sql_client; + SQL::ConnectionID m_connection_id { 0 }; + + HashMap m_pending_executions; +}; + +} diff --git a/Userland/Applications/Browser/main.cpp b/Userland/Applications/Browser/main.cpp index a133d30565..a1ab997c41 100644 --- a/Userland/Applications/Browser/main.cpp +++ b/Userland/Applications/Browser/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -94,6 +95,7 @@ ErrorOr serenity_main(Main::Arguments arguments) TRY(Core::System::unveil("/tmp/session/%sid/portal/image", "rw")); TRY(Core::System::unveil("/tmp/session/%sid/portal/webcontent", "rw")); TRY(Core::System::unveil("/tmp/session/%sid/portal/request", "rw")); + TRY(Core::System::unveil("/tmp/session/%sid/portal/sql", "rw")); TRY(Core::System::unveil("/home", "rwc")); TRY(Core::System::unveil("/res", "r")); TRY(Core::System::unveil("/etc/passwd", "r")); @@ -113,6 +115,7 @@ ErrorOr serenity_main(Main::Arguments arguments) Browser::g_icon_bag = TRY(Browser::IconBag::try_create()); + auto database = TRY(Browser::Database::create()); TRY(load_content_filters()); for (auto& group : Config::list_groups("Browser"sv)) { @@ -140,7 +143,7 @@ ErrorOr serenity_main(Main::Arguments arguments) if (!specified_urls.is_empty()) first_url = url_from_argument_string(specified_urls.first()); - Browser::CookieJar cookie_jar; + auto cookie_jar = TRY(Browser::CookieJar::create(*database)); auto window = Browser::BrowserWindow::construct(cookie_jar, first_url); auto content_filters_watcher = TRY(Core::FileWatcher::create());