Skip to content

Commit

Permalink
LibWeb: Correctly initialize Storage objects on the Document
Browse files Browse the repository at this point in the history
Instead of storing all storage objects in static memory, we now
follow the the spec by lazily creating a unique Storage object
on each document object.

Each Storage object now holds a 'proxy' to the underlying backing
storage. For now, this proxy is simply a reference to the backing
object. In the future, it will need to be some type of interface
object that stores on a SQLite database or similar.

Session storage is now correctly stored / tracked as part of the
TraversableNavigable object.

Local storage is still stored in a static map, but eventually this
should be factored into something that is stored at the user agent
level.
  • Loading branch information
shannonbooth authored and awesomekling committed Jan 2, 2025
1 parent c536f65 commit 2066ed2
Show file tree
Hide file tree
Showing 20 changed files with 503 additions and 44 deletions.
4 changes: 4 additions & 0 deletions Libraries/LibWeb/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,12 @@ set(SOURCES
ServiceWorker/ServiceWorkerRegistration.cpp
SRI/SRI.cpp
StorageAPI/NavigatorStorage.cpp
StorageAPI/StorageBottle.cpp
StorageAPI/StorageEndpoint.cpp
StorageAPI/StorageKey.cpp
StorageAPI/StorageManager.cpp
StorageAPI/StorageShed.cpp
StorageAPI/StorageShelf.cpp
Streams/AbstractOperations.cpp
Streams/ByteLengthQueuingStrategy.cpp
Streams/CountQueuingStrategy.cpp
Expand Down
3 changes: 3 additions & 0 deletions Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
#include <LibWeb/HTML/SharedResourceRequest.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WindowProxy.h>
Expand Down Expand Up @@ -537,6 +538,8 @@ void Document::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_top_layer_pending_removals);
visitor.visit(m_console_client);
visitor.visit(m_editing_host_manager);
visitor.visit(m_local_storage_holder);
visitor.visit(m_session_storage_holder);
}

// https://w3c.github.io/selection-api/#dom-document-getselection
Expand Down
14 changes: 14 additions & 0 deletions Libraries/LibWeb/DOM/Document.h
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,12 @@ class Document

GC::Ptr<DOM::Document> container_document() const;

GC::Ptr<HTML::Storage> session_storage_holder() { return m_session_storage_holder; }
void set_session_storage_holder(GC::Ptr<HTML::Storage> storage) { m_session_storage_holder = storage; }

GC::Ptr<HTML::Storage> local_storage_holder() { return m_local_storage_holder; }
void set_local_storage_holder(GC::Ptr<HTML::Storage> storage) { m_local_storage_holder = storage; }

protected:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
Expand Down Expand Up @@ -1073,6 +1079,14 @@ class Document

// https://w3c.github.io/editing/docs/execCommand/#css-styling-flag
bool m_css_styling_flag { false };

// https://html.spec.whatwg.org/multipage/webstorage.html#session-storage-holder
// A Document object has an associated session storage holder, which is null or a Storage object. It is initially null.
GC::Ptr<HTML::Storage> m_session_storage_holder;

// https://html.spec.whatwg.org/multipage/webstorage.html#local-storage-holder
// A Document object has an associated local storage holder, which is null or a Storage object. It is initially null.
GC::Ptr<HTML::Storage> m_local_storage_holder;
};

template<>
Expand Down
6 changes: 6 additions & 0 deletions Libraries/LibWeb/Forward.h
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,12 @@ struct UnderlyingSource;
namespace Web::StorageAPI {
class NavigatorStorage;
class StorageManager;
class StorageShed;

struct StorageBottle;
struct StorageBucket;
struct StorageEndpoint;
struct StorageShelf;
}

namespace Web::SVG {
Expand Down
44 changes: 22 additions & 22 deletions Libraries/LibWeb/HTML/Storage.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2022, Andreas Kling <[email protected]>
* Copyright (c) 2023, Luke Wilde <[email protected]>
* Copyright (c) 2024, Shannon Booth <[email protected]>
* Copyright (c) 2024-2025, Shannon Booth <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand All @@ -25,15 +25,15 @@ static HashTable<GC::RawRef<Storage>>& all_storages()
return storages;
}

GC::Ref<Storage> Storage::create(JS::Realm& realm, Type type, u64 quota_bytes)
GC::Ref<Storage> Storage::create(JS::Realm& realm, Type type, NonnullRefPtr<StorageAPI::StorageBottle> storage_bottle)
{
return realm.create<Storage>(realm, type, quota_bytes);
return realm.create<Storage>(realm, type, move(storage_bottle));
}

Storage::Storage(JS::Realm& realm, Type type, u64 quota_bytes)
Storage::Storage(JS::Realm& realm, Type type, NonnullRefPtr<StorageAPI::StorageBottle> storage_bottle)
: Bindings::PlatformObject(realm)
, m_type(type)
, m_quota_bytes(quota_bytes)
, m_storage_bottle(move(storage_bottle))
{
m_legacy_platform_object_flags = LegacyPlatformObjectFlags {
.supports_indexed_properties = true,
Expand Down Expand Up @@ -66,18 +66,18 @@ void Storage::finalize()
size_t Storage::length() const
{
// The length getter steps are to return this's map's size.
return m_map.size();
return map().size();
}

// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key
Optional<String> Storage::key(size_t index)
{
// 1. If index is greater than or equal to this's map's size, then return null.
if (index >= m_map.size())
if (index >= map().size())
return {};

// 2. Let keys be the result of running get the keys on this's map.
auto keys = m_map.keys();
auto keys = map().keys();

// 3. Return keys[index].
return keys[index];
Expand All @@ -87,8 +87,8 @@ Optional<String> Storage::key(size_t index)
Optional<String> Storage::get_item(StringView key) const
{
// 1. If this's map[key] does not exist, then return null.
auto it = m_map.find(key);
if (it == m_map.end())
auto it = map().find(key);
if (it == map().end())
return {};

// 2. Return this's map[key].
Expand All @@ -108,7 +108,7 @@ WebIDL::ExceptionOr<void> Storage::set_item(String const& key, String const& val

// 3. If this's map[key] exists:
auto new_size = m_stored_bytes;
if (auto it = m_map.find(key); it != m_map.end()) {
if (auto it = map().find(key); it != map().end()) {
// 1. Set oldValue to this's map[key].
old_value = it->value;

Expand All @@ -124,11 +124,11 @@ WebIDL::ExceptionOr<void> Storage::set_item(String const& key, String const& val

// 4. If value cannot be stored, then throw a "QuotaExceededError" DOMException exception.
new_size += value.bytes().size() - old_value.value_or(String {}).bytes().size();
if (new_size > m_quota_bytes)
return WebIDL::QuotaExceededError::create(realm, MUST(String::formatted("Unable to store more than {} bytes in storage"sv, m_quota_bytes)));
if (m_storage_bottle->quota.has_value() && new_size > *m_storage_bottle->quota)
return WebIDL::QuotaExceededError::create(realm, MUST(String::formatted("Unable to store more than {} bytes in storage"sv, *m_storage_bottle->quota)));

// 5. Set this's map[key] to value.
m_map.set(key, value);
map().set(key, value);
m_stored_bytes = new_size;

// 6. If reorder is true, then reorder this.
Expand All @@ -146,15 +146,15 @@ void Storage::remove_item(String const& key)
{
// 1. If this's map[key] does not exist, then return null.
// FIXME: Return null?
auto it = m_map.find(key);
if (it == m_map.end())
auto it = map().find(key);
if (it == map().end())
return;

// 2. Set oldValue to this's map[key].
auto old_value = it->value;

// 3. Remove this's map[key].
m_map.remove(it);
map().remove(it);
m_stored_bytes = m_stored_bytes - key.bytes().size() - old_value.bytes().size();

// 4. Reorder this.
Expand All @@ -168,7 +168,7 @@ void Storage::remove_item(String const& key)
void Storage::clear()
{
// 1. Clear this's map.
m_map.clear();
map().clear();

// 2. Broadcast this with null, null, and null.
broadcast({}, {}, {});
Expand Down Expand Up @@ -245,8 +245,8 @@ Vector<FlyString> Storage::supported_property_names() const
{
// The supported property names on a Storage object storage are the result of running get the keys on storage's map.
Vector<FlyString> names;
names.ensure_capacity(m_map.size());
for (auto const& key : m_map.keys())
names.ensure_capacity(map().size());
for (auto const& key : map().keys())
names.unchecked_append(key);
return names;
}
Expand Down Expand Up @@ -294,9 +294,9 @@ WebIDL::ExceptionOr<void> Storage::set_value_of_named_property(String const& key

void Storage::dump() const
{
dbgln("Storage ({} key(s))", m_map.size());
dbgln("Storage ({} key(s))", map().size());
size_t i = 0;
for (auto const& it : m_map) {
for (auto const& it : map()) {
dbgln("[{}] \"{}\": \"{}\"", i, it.key, it.value);
++i;
}
Expand Down
13 changes: 7 additions & 6 deletions Libraries/LibWeb/HTML/Storage.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2022, Andreas Kling <[email protected]>
* Copyright (c) 2023, Luke Wilde <[email protected]>
* Copyright (c) 2024-2025, Shannon Booth <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand All @@ -9,6 +10,7 @@

#include <AK/HashMap.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/StorageAPI/StorageBottle.h>
#include <LibWeb/WebIDL/ExceptionOr.h>

namespace Web::HTML {
Expand All @@ -25,7 +27,7 @@ class Storage : public Bindings::PlatformObject {
Session,
};

[[nodiscard]] static GC::Ref<Storage> create(JS::Realm&, Type, u64 quota_bytes);
[[nodiscard]] static GC::Ref<Storage> create(JS::Realm&, Type, NonnullRefPtr<StorageAPI::StorageBottle>);

~Storage();

Expand All @@ -35,14 +37,14 @@ class Storage : public Bindings::PlatformObject {
WebIDL::ExceptionOr<void> set_item(String const& key, String const& value);
void remove_item(String const& key);
void clear();

auto const& map() const { return m_map; }
auto const& map() const { return m_storage_bottle->map; }
auto& map() { return m_storage_bottle->map; }
Type type() const { return m_type; }

void dump() const;

private:
Storage(JS::Realm&, Type, u64 quota_limit);
Storage(JS::Realm&, Type, NonnullRefPtr<StorageAPI::StorageBottle>);

virtual void initialize(JS::Realm&) override;
virtual void finalize() override;
Expand All @@ -58,9 +60,8 @@ class Storage : public Bindings::PlatformObject {
void reorder();
void broadcast(Optional<String> const& key, Optional<String> const& old_value, Optional<String> const& new_value);

OrderedHashMap<String, String> m_map;
Type m_type {};
u64 m_quota_bytes { 0 };
NonnullRefPtr<StorageAPI::StorageBottle> m_storage_bottle;
u64 m_stored_bytes { 0 };
};

Expand Down
8 changes: 8 additions & 0 deletions Libraries/LibWeb/HTML/TraversableNavigable.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <LibWeb/HTML/VisibilityState.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/DisplayListPlayerSkia.h>
#include <LibWeb/StorageAPI/StorageShed.h>
#include <WebContent/BackingStoreManager.h>

#ifdef AK_OS_MACOS
Expand Down Expand Up @@ -107,6 +108,9 @@ class TraversableNavigable final : public Navigable {

RefPtr<Gfx::SkiaBackendContext> skia_backend_context() const { return m_skia_backend_context; }

StorageAPI::StorageShed& storage_shed() { return m_storage_shed; }
StorageAPI::StorageShed const& storage_shed() const { return m_storage_shed; }

private:
TraversableNavigable(GC::Ref<Page>);

Expand Down Expand Up @@ -142,6 +146,10 @@ class TraversableNavigable final : public Navigable {
// https://html.spec.whatwg.org/multipage/document-sequences.html#system-visibility-state
VisibilityState m_system_visibility_state { VisibilityState::Hidden };

// https://storage.spec.whatwg.org/#traversable-navigable-storage-shed
// A traversable navigable holds a storage shed, which is a storage shed. A traversable navigable’s storage shed holds all session storage data.
StorageAPI::StorageShed m_storage_shed;

GC::Ref<SessionHistoryTraversalQueue> m_session_history_traversal_queue;

String m_window_handle;
Expand Down
59 changes: 43 additions & 16 deletions Libraries/LibWeb/HTML/Window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/RequestIdleCallback/IdleDeadline.h>
#include <LibWeb/Selection/Selection.h>
#include <LibWeb/StorageAPI/StorageBottle.h>
#include <LibWeb/WebIDL/AbstractOperations.h>

namespace Web::HTML {
Expand Down Expand Up @@ -447,29 +448,55 @@ void Window::fire_a_page_transition_event(FlyString const& event_name, bool pers
// https://html.spec.whatwg.org/multipage/webstorage.html#dom-localstorage
WebIDL::ExceptionOr<GC::Ref<Storage>> Window::local_storage()
{
// See table in: https://storage.spec.whatwg.org/#registered-storage-endpoints
constexpr u64 quota_bytes = 5 * MiB;
auto& realm = this->realm();

// FIXME: Implement according to spec.
static HashMap<URL::Origin, GC::Root<Storage>> local_storage_per_origin;
auto storage = local_storage_per_origin.ensure(associated_document().origin(), [this]() -> GC::Root<Storage> {
return Storage::create(realm(), Storage::Type::Local, quota_bytes);
});
return GC::Ref { *storage };
// 1. If this's associated Document's local storage holder is non-null, then return this's associated Document's local storage holder.
auto& associated_document = this->associated_document();
if (auto storage = associated_document.local_storage_holder())
return GC::Ref { *storage };

// 2. Let map be the result of running obtain a local storage bottle map with this's relevant settings object and "localStorage".
auto map = StorageAPI::obtain_a_local_storage_bottle_map(relevant_settings_object(*this), "localStorage"sv);

// 3. If map is failure, then throw a "SecurityError" DOMException.
if (!map)
return WebIDL::SecurityError::create(realm, "localStorage is not available"_string);

// 4. Let storage be a new Storage object whose map is map.
auto storage = Storage::create(realm, Storage::Type::Session, map.release_nonnull());

// 5. Set this's associated Document's local storage holder to storage.
associated_document.set_local_storage_holder(storage);

// 6. Return storage.
return storage;
}

// https://html.spec.whatwg.org/multipage/webstorage.html#dom-sessionstorage
WebIDL::ExceptionOr<GC::Ref<Storage>> Window::session_storage()
{
// See table in: https://storage.spec.whatwg.org/#registered-storage-endpoints
constexpr u64 quota_bytes = 5 * MiB;
auto& realm = this->realm();

// FIXME: Implement according to spec.
static HashMap<URL::Origin, GC::Root<Storage>> session_storage_per_origin;
auto storage = session_storage_per_origin.ensure(associated_document().origin(), [this]() -> GC::Root<Storage> {
return Storage::create(realm(), Storage::Type::Session, quota_bytes);
});
return GC::Ref { *storage };
// 1. If this's associated Document's session storage holder is non-null, then return this's associated Document's session storage holder.
auto& associated_document = this->associated_document();
if (auto storage = associated_document.session_storage_holder())
return GC::Ref { *storage };

// 2. Let map be the result of running obtain a session storage bottle map with this's relevant settings object and "sessionStorage".
auto map = StorageAPI::obtain_a_session_storage_bottle_map(relevant_settings_object(*this), "sessionStorage"sv);

// 3. If map is failure, then throw a "SecurityError" DOMException.
if (!map)
return WebIDL::SecurityError::create(realm, "sessionStorage is not available"_string);

// 4. Let storage be a new Storage object whose map is map.
auto storage = Storage::create(realm, Storage::Type::Session, map.release_nonnull());

// 5. Set this's associated Document's session storage holder to storage.
associated_document.set_session_storage_holder(storage);

// 6. Return storage.
return storage;
}

// https://html.spec.whatwg.org/multipage/interaction.html#sticky-activation
Expand Down
Loading

0 comments on commit 2066ed2

Please sign in to comment.