Serenity Operating System
at master 589 lines 24 kB view raw
1/* 2 * Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org> 5 * 6 * SPDX-License-Identifier: BSD-2-Clause 7 */ 8 9#include "CookieJar.h" 10#include "Database.h" 11#include <AK/IPv4Address.h> 12#include <AK/StringBuilder.h> 13#include <AK/StringView.h> 14#include <AK/Time.h> 15#include <AK/URL.h> 16#include <AK/Vector.h> 17#include <LibCore/Promise.h> 18#include <LibSQL/TupleDescriptor.h> 19#include <LibSQL/Value.h> 20#include <LibWeb/Cookie/ParsedCookie.h> 21 22namespace Browser { 23 24ErrorOr<CookieJar> CookieJar::create(Database& database) 25{ 26 Statements statements {}; 27 28 statements.create_table = TRY(database.prepare_statement(R"#( 29 CREATE TABLE IF NOT EXISTS Cookies ( 30 name TEXT, 31 value TEXT, 32 same_site INTEGER, 33 creation_time INTEGER, 34 last_access_time INTEGER, 35 expiry_time INTEGER, 36 domain TEXT, 37 path TEXT, 38 secure BOOLEAN, 39 http_only BOOLEAN, 40 host_only BOOLEAN, 41 persistent BOOLEAN 42 );)#"sv)); 43 44 statements.update_cookie = TRY(database.prepare_statement(R"#( 45 UPDATE Cookies SET 46 value=?, 47 same_site=?, 48 creation_time=?, 49 last_access_time=?, 50 expiry_time=?, 51 secure=?, 52 http_only=?, 53 host_only=?, 54 persistent=? 55 WHERE ((name = ?) AND (domain = ?) AND (path = ?));)#"sv)); 56 57 statements.insert_cookie = TRY(database.prepare_statement("INSERT INTO Cookies VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"sv)); 58 statements.expire_cookie = TRY(database.prepare_statement("DELETE FROM Cookies WHERE (expiry_time < ?);"sv)); 59 statements.select_cookie = TRY(database.prepare_statement("SELECT * FROM Cookies WHERE ((name = ?) AND (domain = ?) AND (path = ?));"sv)); 60 statements.select_all_cookies = TRY(database.prepare_statement("SELECT * FROM Cookies;"sv)); 61 62 return CookieJar { database, move(statements) }; 63} 64 65CookieJar::CookieJar(Database& database, Statements statements) 66 : m_database(database) 67 , m_statements(move(statements)) 68{ 69 m_database.execute_statement(m_statements.create_table, {}, {}, {}); 70} 71 72DeprecatedString CookieJar::get_cookie(const URL& url, Web::Cookie::Source source) 73{ 74 purge_expired_cookies(); 75 76 auto domain = canonicalize_domain(url); 77 if (!domain.has_value()) 78 return {}; 79 80 auto cookie_list = get_matching_cookies(url, domain.value(), source); 81 StringBuilder builder; 82 83 for (auto const& cookie : cookie_list) { 84 // If there is an unprocessed cookie in the cookie-list, output the characters %x3B and %x20 ("; ") 85 if (!builder.is_empty()) 86 builder.append("; "sv); 87 88 // Output the cookie's name, the %x3D ("=") character, and the cookie's value. 89 builder.appendff("{}={}", cookie.name, cookie.value); 90 } 91 92 return builder.to_deprecated_string(); 93} 94 95void CookieJar::set_cookie(const URL& url, Web::Cookie::ParsedCookie const& parsed_cookie, Web::Cookie::Source source) 96{ 97 auto domain = canonicalize_domain(url); 98 if (!domain.has_value()) 99 return; 100 101 store_cookie(parsed_cookie, url, move(domain.value()), source); 102} 103 104// This is based on https://www.rfc-editor.org/rfc/rfc6265#section-5.3 as store_cookie() below 105// however the whole ParsedCookie->Cookie conversion is skipped. 106void CookieJar::update_cookie(Web::Cookie::Cookie cookie) 107{ 108 select_cookie_from_database( 109 move(cookie), 110 111 // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: 112 [this](auto& cookie, auto old_cookie) { 113 // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. 114 cookie.creation_time = old_cookie.creation_time; 115 116 // Remove the old-cookie from the cookie store. 117 // NOTE: Rather than deleting then re-inserting this cookie, we update it in-place. 118 update_cookie_in_database(cookie); 119 }, 120 121 // 12. Insert the newly created cookie into the cookie store. 122 [this](auto cookie) { 123 insert_cookie_into_database(cookie); 124 }); 125} 126 127void CookieJar::dump_cookies() 128{ 129 static constexpr auto key_color = "\033[34;1m"sv; 130 static constexpr auto attribute_color = "\033[33m"sv; 131 static constexpr auto no_color = "\033[0m"sv; 132 133 StringBuilder builder; 134 size_t total_cookies { 0 }; 135 136 select_all_cookies_from_database([&](auto cookie) { 137 ++total_cookies; 138 139 builder.appendff("{}{}{} - ", key_color, cookie.name, no_color); 140 builder.appendff("{}{}{} - ", key_color, cookie.domain, no_color); 141 builder.appendff("{}{}{}\n", key_color, cookie.path, no_color); 142 143 builder.appendff("\t{}Value{} = {}\n", attribute_color, no_color, cookie.value); 144 builder.appendff("\t{}CreationTime{} = {}\n", attribute_color, no_color, cookie.creation_time_to_string()); 145 builder.appendff("\t{}LastAccessTime{} = {}\n", attribute_color, no_color, cookie.last_access_time_to_string()); 146 builder.appendff("\t{}ExpiryTime{} = {}\n", attribute_color, no_color, cookie.expiry_time_to_string()); 147 builder.appendff("\t{}Secure{} = {:s}\n", attribute_color, no_color, cookie.secure); 148 builder.appendff("\t{}HttpOnly{} = {:s}\n", attribute_color, no_color, cookie.http_only); 149 builder.appendff("\t{}HostOnly{} = {:s}\n", attribute_color, no_color, cookie.host_only); 150 builder.appendff("\t{}Persistent{} = {:s}\n", attribute_color, no_color, cookie.persistent); 151 builder.appendff("\t{}SameSite{} = {:s}\n", attribute_color, no_color, Web::Cookie::same_site_to_string(cookie.same_site)); 152 }); 153 154 dbgln("{} cookies stored\n{}", total_cookies, builder.to_deprecated_string()); 155} 156 157Vector<Web::Cookie::Cookie> CookieJar::get_all_cookies() 158{ 159 Vector<Web::Cookie::Cookie> cookies; 160 161 select_all_cookies_from_database([&](auto cookie) { 162 cookies.append(move(cookie)); 163 }); 164 165 return cookies; 166} 167 168// https://w3c.github.io/webdriver/#dfn-associated-cookies 169Vector<Web::Cookie::Cookie> CookieJar::get_all_cookies(URL const& url) 170{ 171 auto domain = canonicalize_domain(url); 172 if (!domain.has_value()) 173 return {}; 174 175 return get_matching_cookies(url, domain.value(), Web::Cookie::Source::Http, MatchingCookiesSpecMode::WebDriver); 176} 177 178Optional<Web::Cookie::Cookie> CookieJar::get_named_cookie(URL const& url, DeprecatedString const& name) 179{ 180 auto domain = canonicalize_domain(url); 181 if (!domain.has_value()) 182 return {}; 183 184 auto cookie_list = get_matching_cookies(url, domain.value(), Web::Cookie::Source::Http, MatchingCookiesSpecMode::WebDriver); 185 186 for (auto const& cookie : cookie_list) { 187 if (cookie.name == name) 188 return cookie; 189 } 190 191 return {}; 192} 193 194Optional<DeprecatedString> CookieJar::canonicalize_domain(const URL& url) 195{ 196 // https://tools.ietf.org/html/rfc6265#section-5.1.2 197 if (!url.is_valid()) 198 return {}; 199 200 // FIXME: Implement RFC 5890 to "Convert each label that is not a Non-Reserved LDH (NR-LDH) label to an A-label". 201 return url.host().to_lowercase(); 202} 203 204bool CookieJar::domain_matches(DeprecatedString const& string, DeprecatedString const& domain_string) 205{ 206 // https://tools.ietf.org/html/rfc6265#section-5.1.3 207 208 // A string domain-matches a given domain string if at least one of the following conditions hold: 209 210 // The domain string and the string are identical. 211 if (string == domain_string) 212 return true; 213 214 // All of the following conditions hold: 215 // - The domain string is a suffix of the string. 216 // - The last character of the string that is not included in the domain string is a %x2E (".") character. 217 // - The string is a host name (i.e., not an IP address). 218 if (!string.ends_with(domain_string)) 219 return false; 220 if (string[string.length() - domain_string.length() - 1] != '.') 221 return false; 222 if (AK::IPv4Address::from_string(string).has_value()) 223 return false; 224 225 return true; 226} 227 228bool CookieJar::path_matches(DeprecatedString const& request_path, DeprecatedString const& cookie_path) 229{ 230 // https://tools.ietf.org/html/rfc6265#section-5.1.4 231 232 // A request-path path-matches a given cookie-path if at least one of the following conditions holds: 233 234 // The cookie-path and the request-path are identical. 235 if (request_path == cookie_path) 236 return true; 237 238 if (request_path.starts_with(cookie_path)) { 239 // The cookie-path is a prefix of the request-path, and the last character of the cookie-path is %x2F ("/"). 240 if (cookie_path.ends_with('/')) 241 return true; 242 243 // The cookie-path is a prefix of the request-path, and the first character of the request-path that is not included in the cookie-path is a %x2F ("/") character. 244 if (request_path[cookie_path.length()] == '/') 245 return true; 246 } 247 248 return false; 249} 250 251DeprecatedString CookieJar::default_path(const URL& url) 252{ 253 // https://tools.ietf.org/html/rfc6265#section-5.1.4 254 255 // 1. Let uri-path be the path portion of the request-uri if such a portion exists (and empty otherwise). 256 DeprecatedString uri_path = url.path(); 257 258 // 2. If the uri-path is empty or if the first character of the uri-path is not a %x2F ("/") character, output %x2F ("/") and skip the remaining steps. 259 if (uri_path.is_empty() || (uri_path[0] != '/')) 260 return "/"; 261 262 StringView uri_path_view = uri_path; 263 size_t last_separator = uri_path_view.find_last('/').value(); 264 265 // 3. If the uri-path contains no more than one %x2F ("/") character, output %x2F ("/") and skip the remaining step. 266 if (last_separator == 0) 267 return "/"; 268 269 // 4. Output the characters of the uri-path from the first character up to, but not including, the right-most %x2F ("/"). 270 return uri_path.substring(0, last_separator); 271} 272 273void CookieJar::store_cookie(Web::Cookie::ParsedCookie const& parsed_cookie, const URL& url, DeprecatedString canonicalized_domain, Web::Cookie::Source source) 274{ 275 // https://tools.ietf.org/html/rfc6265#section-5.3 276 277 // 2. Create a new cookie with name cookie-name, value cookie-value. Set the creation-time and the last-access-time to the current date and time. 278 Web::Cookie::Cookie cookie { parsed_cookie.name, parsed_cookie.value, parsed_cookie.same_site_attribute }; 279 cookie.creation_time = Time::now_realtime(); 280 cookie.last_access_time = cookie.creation_time; 281 282 if (parsed_cookie.expiry_time_from_max_age_attribute.has_value()) { 283 // 3. If the cookie-attribute-list contains an attribute with an attribute-name of "Max-Age": Set the cookie's persistent-flag to true. 284 // Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Max-Age". 285 cookie.persistent = true; 286 cookie.expiry_time = parsed_cookie.expiry_time_from_max_age_attribute.value(); 287 } else if (parsed_cookie.expiry_time_from_expires_attribute.has_value()) { 288 // If the cookie-attribute-list contains an attribute with an attribute-name of "Expires": Set the cookie's persistent-flag to true. 289 // Set the cookie's expiry-time to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Expires". 290 cookie.persistent = true; 291 cookie.expiry_time = parsed_cookie.expiry_time_from_expires_attribute.value(); 292 } else { 293 // Set the cookie's persistent-flag to false. Set the cookie's expiry-time to the latest representable date. 294 cookie.persistent = false; 295 cookie.expiry_time = Time::max(); 296 } 297 298 // 4. If the cookie-attribute-list contains an attribute with an attribute-name of "Domain": 299 if (parsed_cookie.domain.has_value()) { 300 // Let the domain-attribute be the attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Domain". 301 cookie.domain = parsed_cookie.domain.value(); 302 } 303 304 // 5. If the user agent is configured to reject "public suffixes" and the domain-attribute is a public suffix: 305 // FIXME: Support rejection of public suffixes. The full list is here: https://publicsuffix.org/list/public_suffix_list.dat 306 307 // 6. If the domain-attribute is non-empty: 308 if (!cookie.domain.is_empty()) { 309 // If the canonicalized request-host does not domain-match the domain-attribute: Ignore the cookie entirely and abort these steps. 310 if (!domain_matches(canonicalized_domain, cookie.domain)) 311 return; 312 313 // Set the cookie's host-only-flag to false. Set the cookie's domain to the domain-attribute. 314 cookie.host_only = false; 315 } else { 316 // Set the cookie's host-only-flag to true. Set the cookie's domain to the canonicalized request-host. 317 cookie.host_only = true; 318 cookie.domain = move(canonicalized_domain); 319 } 320 321 // 7. If the cookie-attribute-list contains an attribute with an attribute-name of "Path": 322 if (parsed_cookie.path.has_value()) { 323 // Set the cookie's path to attribute-value of the last attribute in the cookie-attribute-list with an attribute-name of "Path". 324 cookie.path = parsed_cookie.path.value(); 325 } else { 326 cookie.path = default_path(url); 327 } 328 329 // 8. If the cookie-attribute-list contains an attribute with an attribute-name of "Secure", set the cookie's secure-only-flag to true. 330 cookie.secure = parsed_cookie.secure_attribute_present; 331 332 // 9. If the cookie-attribute-list contains an attribute with an attribute-name of "HttpOnly", set the cookie's http-only-flag to false. 333 cookie.http_only = parsed_cookie.http_only_attribute_present; 334 335 // 10. If the cookie was received from a "non-HTTP" API and the cookie's http-only-flag is set, abort these steps and ignore the cookie entirely. 336 if (source != Web::Cookie::Source::Http && cookie.http_only) 337 return; 338 339 select_cookie_from_database( 340 move(cookie), 341 342 // 11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: 343 [this, source](auto& cookie, auto old_cookie) { 344 // If the newly created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is set, abort these 345 // steps and ignore the newly created cookie entirely. 346 if (source != Web::Cookie::Source::Http && old_cookie.http_only) 347 return; 348 349 // Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. 350 cookie.creation_time = old_cookie.creation_time; 351 352 // Remove the old-cookie from the cookie store. 353 // NOTE: Rather than deleting then re-inserting this cookie, we update it in-place. 354 update_cookie_in_database(cookie); 355 }, 356 357 // 12. Insert the newly created cookie into the cookie store. 358 [this](auto cookie) { 359 insert_cookie_into_database(cookie); 360 }); 361} 362 363Vector<Web::Cookie::Cookie> CookieJar::get_matching_cookies(const URL& url, DeprecatedString const& canonicalized_domain, Web::Cookie::Source source, MatchingCookiesSpecMode mode) 364{ 365 // https://tools.ietf.org/html/rfc6265#section-5.4 366 367 // 1. Let cookie-list be the set of cookies from the cookie store that meets all of the following requirements: 368 Vector<Web::Cookie::Cookie> cookie_list; 369 370 select_all_cookies_from_database([&](auto cookie) { 371 // Either: The cookie's host-only-flag is true and the canonicalized request-host is identical to the cookie's domain. 372 // Or: The cookie's host-only-flag is false and the canonicalized request-host domain-matches the cookie's domain. 373 bool is_host_only_and_has_identical_domain = cookie.host_only && (canonicalized_domain == cookie.domain); 374 bool is_not_host_only_and_domain_matches = !cookie.host_only && domain_matches(canonicalized_domain, cookie.domain); 375 if (!is_host_only_and_has_identical_domain && !is_not_host_only_and_domain_matches) 376 return; 377 378 // The request-uri's path path-matches the cookie's path. 379 if (!path_matches(url.path(), cookie.path)) 380 return; 381 382 // If the cookie's secure-only-flag is true, then the request-uri's scheme must denote a "secure" protocol. 383 if (cookie.secure && (url.scheme() != "https")) 384 return; 385 386 // 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. 387 if (cookie.http_only && (source != Web::Cookie::Source::Http)) 388 return; 389 390 // NOTE: The WebDriver spec expects only step 1 above to be executed to match cookies. 391 if (mode == MatchingCookiesSpecMode::WebDriver) { 392 cookie_list.append(move(cookie)); 393 return; 394 } 395 396 // 2. The user agent SHOULD sort the cookie-list in the following order: 397 // - Cookies with longer paths are listed before cookies with shorter paths. 398 // - Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times. 399 auto cookie_path_length = cookie.path.length(); 400 auto cookie_creation_time = cookie.creation_time; 401 402 cookie_list.insert_before_matching(move(cookie), [cookie_path_length, cookie_creation_time](auto const& entry) { 403 if (cookie_path_length > entry.path.length()) { 404 return true; 405 } else if (cookie_path_length == entry.path.length()) { 406 if (cookie_creation_time < entry.creation_time) 407 return true; 408 } 409 return false; 410 }); 411 }); 412 413 // 3. Update the last-access-time of each cookie in the cookie-list to the current date and time. 414 auto now = Time::now_realtime(); 415 416 for (auto& cookie : cookie_list) { 417 cookie.last_access_time = now; 418 update_cookie_in_database(cookie); 419 } 420 421 return cookie_list; 422} 423 424static ErrorOr<Web::Cookie::Cookie> parse_cookie(ReadonlySpan<SQL::Value> row) 425{ 426 if (row.size() != 12) 427 return Error::from_string_view("Incorrect number of columns to parse cookie"sv); 428 429 size_t index = 0; 430 431 auto convert_text = [&](auto& field, StringView name) -> ErrorOr<void> { 432 auto const& value = row[index++]; 433 if (value.type() != SQL::SQLType::Text) 434 return Error::from_string_view(name); 435 436 field = value.to_deprecated_string(); 437 return {}; 438 }; 439 440 auto convert_bool = [&](auto& field, StringView name) -> ErrorOr<void> { 441 auto const& value = row[index++]; 442 if (value.type() != SQL::SQLType::Boolean) 443 return Error::from_string_view(name); 444 445 field = value.to_bool().value(); 446 return {}; 447 }; 448 449 auto convert_time = [&](auto& field, StringView name) -> ErrorOr<void> { 450 auto const& value = row[index++]; 451 if (value.type() != SQL::SQLType::Integer) 452 return Error::from_string_view(name); 453 454 auto time = value.to_int<i64>().value(); 455 field = Time::from_seconds(time); 456 return {}; 457 }; 458 459 auto convert_same_site = [&](auto& field, StringView name) -> ErrorOr<void> { 460 auto const& value = row[index++]; 461 if (value.type() != SQL::SQLType::Integer) 462 return Error::from_string_view(name); 463 464 auto same_site = value.to_int<UnderlyingType<Web::Cookie::SameSite>>().value(); 465 if (same_site > to_underlying(Web::Cookie::SameSite::Lax)) 466 return Error::from_string_view(name); 467 468 field = static_cast<Web::Cookie::SameSite>(same_site); 469 return {}; 470 }; 471 472 Web::Cookie::Cookie cookie; 473 TRY(convert_text(cookie.name, "name"sv)); 474 TRY(convert_text(cookie.value, "value"sv)); 475 TRY(convert_same_site(cookie.same_site, "same_site"sv)); 476 TRY(convert_time(cookie.creation_time, "creation_time"sv)); 477 TRY(convert_time(cookie.last_access_time, "last_access_time"sv)); 478 TRY(convert_time(cookie.expiry_time, "expiry_time"sv)); 479 TRY(convert_text(cookie.domain, "domain"sv)); 480 TRY(convert_text(cookie.path, "path"sv)); 481 TRY(convert_bool(cookie.secure, "secure"sv)); 482 TRY(convert_bool(cookie.http_only, "http_only"sv)); 483 TRY(convert_bool(cookie.host_only, "host_only"sv)); 484 TRY(convert_bool(cookie.persistent, "persistent"sv)); 485 486 return cookie; 487} 488 489void CookieJar::insert_cookie_into_database(Web::Cookie::Cookie const& cookie) 490{ 491 m_database.execute_statement( 492 m_statements.insert_cookie, {}, [this]() { purge_expired_cookies(); }, {}, 493 cookie.name, 494 cookie.value, 495 to_underlying(cookie.same_site), 496 cookie.creation_time.to_seconds(), 497 cookie.last_access_time.to_seconds(), 498 cookie.expiry_time.to_seconds(), 499 cookie.domain, 500 cookie.path, 501 cookie.secure, 502 cookie.http_only, 503 cookie.host_only, 504 cookie.persistent); 505} 506 507void CookieJar::update_cookie_in_database(Web::Cookie::Cookie const& cookie) 508{ 509 m_database.execute_statement( 510 m_statements.update_cookie, {}, [this]() { purge_expired_cookies(); }, {}, 511 cookie.value, 512 to_underlying(cookie.same_site), 513 cookie.creation_time.to_seconds(), 514 cookie.last_access_time.to_seconds(), 515 cookie.expiry_time.to_seconds(), 516 cookie.secure, 517 cookie.http_only, 518 cookie.host_only, 519 cookie.persistent, 520 cookie.name, 521 cookie.domain, 522 cookie.path); 523} 524 525struct WrappedCookie : public RefCounted<WrappedCookie> { 526 explicit WrappedCookie(Web::Cookie::Cookie cookie_) 527 : RefCounted() 528 , cookie(move(cookie_)) 529 { 530 } 531 532 Web::Cookie::Cookie cookie; 533 bool had_any_results { false }; 534}; 535 536void CookieJar::select_cookie_from_database(Web::Cookie::Cookie cookie, OnCookieFound on_result, OnCookieNotFound on_complete_without_results) 537{ 538 auto wrapped_cookie = make_ref_counted<WrappedCookie>(move(cookie)); 539 540 m_database.execute_statement( 541 m_statements.select_cookie, 542 [on_result = move(on_result), wrapped_cookie = wrapped_cookie](auto row) { 543 if (auto selected_cookie = parse_cookie(row); selected_cookie.is_error()) 544 dbgln("Failed to parse cookie '{}': {}", selected_cookie.error(), row); 545 else 546 on_result(wrapped_cookie->cookie, selected_cookie.release_value()); 547 548 wrapped_cookie->had_any_results = true; 549 }, 550 [on_complete_without_results = move(on_complete_without_results), wrapped_cookie = wrapped_cookie]() { 551 if (!wrapped_cookie->had_any_results) 552 on_complete_without_results(move(wrapped_cookie->cookie)); 553 }, 554 {}, 555 wrapped_cookie->cookie.name, 556 wrapped_cookie->cookie.domain, 557 wrapped_cookie->cookie.path); 558} 559 560void CookieJar::select_all_cookies_from_database(OnSelectAllCookiesResult on_result) 561{ 562 // FIXME: Make surrounding APIs asynchronous. 563 auto promise = Core::Promise<Empty>::construct(); 564 565 m_database.execute_statement( 566 m_statements.select_all_cookies, 567 [on_result = move(on_result)](auto row) { 568 if (auto cookie = parse_cookie(row); cookie.is_error()) 569 dbgln("Failed to parse cookie '{}': {}", cookie.error(), row); 570 else 571 on_result(cookie.release_value()); 572 }, 573 [&]() { 574 MUST(promise->resolve({})); 575 }, 576 [&](auto) { 577 MUST(promise->resolve({})); 578 }); 579 580 MUST(promise->await()); 581} 582 583void CookieJar::purge_expired_cookies() 584{ 585 auto now = Time::now_realtime().to_seconds(); 586 m_database.execute_statement(m_statements.expire_cookie, {}, {}, {}, now); 587} 588 589}