LibWeb: Correctly initialize Storage objects on the Document

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.
This commit is contained in:
Shannon Booth 2024-12-26 11:56:03 +13:00 committed by Andreas Kling
commit 2066ed2318
Notes: github-actions[bot] 2025-01-02 10:39:14 +00:00
20 changed files with 503 additions and 44 deletions

View file

@ -690,8 +690,12 @@ set(SOURCES
ServiceWorker/ServiceWorkerRegistration.cpp ServiceWorker/ServiceWorkerRegistration.cpp
SRI/SRI.cpp SRI/SRI.cpp
StorageAPI/NavigatorStorage.cpp StorageAPI/NavigatorStorage.cpp
StorageAPI/StorageBottle.cpp
StorageAPI/StorageEndpoint.cpp
StorageAPI/StorageKey.cpp StorageAPI/StorageKey.cpp
StorageAPI/StorageManager.cpp StorageAPI/StorageManager.cpp
StorageAPI/StorageShed.cpp
StorageAPI/StorageShelf.cpp
Streams/AbstractOperations.cpp Streams/AbstractOperations.cpp
Streams/ByteLengthQueuingStrategy.cpp Streams/ByteLengthQueuingStrategy.cpp
Streams/CountQueuingStrategy.cpp Streams/CountQueuingStrategy.cpp

View file

@ -116,6 +116,7 @@
#include <LibWeb/HTML/Scripting/ExceptionReporter.h> #include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h> #include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
#include <LibWeb/HTML/SharedResourceRequest.h> #include <LibWeb/HTML/SharedResourceRequest.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/HTML/TraversableNavigable.h> #include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h> #include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WindowProxy.h> #include <LibWeb/HTML/WindowProxy.h>
@ -537,6 +538,8 @@ void Document::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_top_layer_pending_removals); visitor.visit(m_top_layer_pending_removals);
visitor.visit(m_console_client); visitor.visit(m_console_client);
visitor.visit(m_editing_host_manager); 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 // https://w3c.github.io/selection-api/#dom-document-getselection

View file

@ -760,6 +760,12 @@ public:
GC::Ptr<DOM::Document> container_document() const; 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: protected:
virtual void initialize(JS::Realm&) override; virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override; virtual void visit_edges(Cell::Visitor&) override;
@ -1073,6 +1079,14 @@ private:
// https://w3c.github.io/editing/docs/execCommand/#css-styling-flag // https://w3c.github.io/editing/docs/execCommand/#css-styling-flag
bool m_css_styling_flag { false }; 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<> template<>

View file

@ -752,6 +752,12 @@ struct UnderlyingSource;
namespace Web::StorageAPI { namespace Web::StorageAPI {
class NavigatorStorage; class NavigatorStorage;
class StorageManager; class StorageManager;
class StorageShed;
struct StorageBottle;
struct StorageBucket;
struct StorageEndpoint;
struct StorageShelf;
} }
namespace Web::SVG { namespace Web::SVG {

View file

@ -1,7 +1,7 @@
/* /*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org> * Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -25,15 +25,15 @@ static HashTable<GC::RawRef<Storage>>& all_storages()
return 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) : Bindings::PlatformObject(realm)
, m_type(type) , m_type(type)
, m_quota_bytes(quota_bytes) , m_storage_bottle(move(storage_bottle))
{ {
m_legacy_platform_object_flags = LegacyPlatformObjectFlags { m_legacy_platform_object_flags = LegacyPlatformObjectFlags {
.supports_indexed_properties = true, .supports_indexed_properties = true,
@ -66,18 +66,18 @@ void Storage::finalize()
size_t Storage::length() const size_t Storage::length() const
{ {
// The length getter steps are to return this's map's size. // 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 // https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key
Optional<String> Storage::key(size_t index) Optional<String> Storage::key(size_t index)
{ {
// 1. If index is greater than or equal to this's map's size, then return null. // 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 {}; return {};
// 2. Let keys be the result of running get the keys on this's map. // 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]. // 3. Return keys[index].
return keys[index]; return keys[index];
@ -87,8 +87,8 @@ Optional<String> Storage::key(size_t index)
Optional<String> Storage::get_item(StringView key) const Optional<String> Storage::get_item(StringView key) const
{ {
// 1. If this's map[key] does not exist, then return null. // 1. If this's map[key] does not exist, then return null.
auto it = m_map.find(key); auto it = map().find(key);
if (it == m_map.end()) if (it == map().end())
return {}; return {};
// 2. Return this's map[key]. // 2. Return this's map[key].
@ -108,7 +108,7 @@ WebIDL::ExceptionOr<void> Storage::set_item(String const& key, String const& val
// 3. If this's map[key] exists: // 3. If this's map[key] exists:
auto new_size = m_stored_bytes; 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]. // 1. Set oldValue to this's map[key].
old_value = it->value; old_value = it->value;
@ -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. // 4. If value cannot be stored, then throw a "QuotaExceededError" DOMException exception.
new_size += value.bytes().size() - old_value.value_or(String {}).bytes().size(); new_size += value.bytes().size() - old_value.value_or(String {}).bytes().size();
if (new_size > 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_quota_bytes))); 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. // 5. Set this's map[key] to value.
m_map.set(key, value); map().set(key, value);
m_stored_bytes = new_size; m_stored_bytes = new_size;
// 6. If reorder is true, then reorder this. // 6. If reorder is true, then reorder this.
@ -146,15 +146,15 @@ void Storage::remove_item(String const& key)
{ {
// 1. If this's map[key] does not exist, then return null. // 1. If this's map[key] does not exist, then return null.
// FIXME: Return null? // FIXME: Return null?
auto it = m_map.find(key); auto it = map().find(key);
if (it == m_map.end()) if (it == map().end())
return; return;
// 2. Set oldValue to this's map[key]. // 2. Set oldValue to this's map[key].
auto old_value = it->value; auto old_value = it->value;
// 3. Remove this's map[key]. // 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(); m_stored_bytes = m_stored_bytes - key.bytes().size() - old_value.bytes().size();
// 4. Reorder this. // 4. Reorder this.
@ -168,7 +168,7 @@ void Storage::remove_item(String const& key)
void Storage::clear() void Storage::clear()
{ {
// 1. Clear this's map. // 1. Clear this's map.
m_map.clear(); map().clear();
// 2. Broadcast this with null, null, and null. // 2. Broadcast this with null, null, and null.
broadcast({}, {}, {}); broadcast({}, {}, {});
@ -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. // The supported property names on a Storage object storage are the result of running get the keys on storage's map.
Vector<FlyString> names; Vector<FlyString> names;
names.ensure_capacity(m_map.size()); names.ensure_capacity(map().size());
for (auto const& key : m_map.keys()) for (auto const& key : map().keys())
names.unchecked_append(key); names.unchecked_append(key);
return names; return names;
} }
@ -294,9 +294,9 @@ WebIDL::ExceptionOr<void> Storage::set_value_of_named_property(String const& key
void Storage::dump() const void Storage::dump() const
{ {
dbgln("Storage ({} key(s))", m_map.size()); dbgln("Storage ({} key(s))", map().size());
size_t i = 0; size_t i = 0;
for (auto const& it : m_map) { for (auto const& it : map()) {
dbgln("[{}] \"{}\": \"{}\"", i, it.key, it.value); dbgln("[{}] \"{}\": \"{}\"", i, it.key, it.value);
++i; ++i;
} }

View file

@ -1,6 +1,7 @@
/* /*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -9,6 +10,7 @@
#include <AK/HashMap.h> #include <AK/HashMap.h>
#include <LibWeb/Bindings/PlatformObject.h> #include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/StorageAPI/StorageBottle.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML { namespace Web::HTML {
@ -25,7 +27,7 @@ public:
Session, 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(); ~Storage();
@ -35,14 +37,14 @@ public:
WebIDL::ExceptionOr<void> set_item(String const& key, String const& value); WebIDL::ExceptionOr<void> set_item(String const& key, String const& value);
void remove_item(String const& key); void remove_item(String const& key);
void clear(); void clear();
auto const& map() const { return m_storage_bottle->map; }
auto const& map() const { return m_map; } auto& map() { return m_storage_bottle->map; }
Type type() const { return m_type; } Type type() const { return m_type; }
void dump() const; void dump() const;
private: private:
Storage(JS::Realm&, Type, u64 quota_limit); Storage(JS::Realm&, Type, NonnullRefPtr<StorageAPI::StorageBottle>);
virtual void initialize(JS::Realm&) override; virtual void initialize(JS::Realm&) override;
virtual void finalize() override; virtual void finalize() override;
@ -58,9 +60,8 @@ private:
void reorder(); void reorder();
void broadcast(Optional<String> const& key, Optional<String> const& old_value, Optional<String> const& new_value); void broadcast(Optional<String> const& key, Optional<String> const& old_value, Optional<String> const& new_value);
OrderedHashMap<String, String> m_map;
Type m_type {}; Type m_type {};
u64 m_quota_bytes { 0 }; NonnullRefPtr<StorageAPI::StorageBottle> m_storage_bottle;
u64 m_stored_bytes { 0 }; u64 m_stored_bytes { 0 };
}; };

View file

@ -13,6 +13,7 @@
#include <LibWeb/HTML/VisibilityState.h> #include <LibWeb/HTML/VisibilityState.h>
#include <LibWeb/Page/Page.h> #include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/DisplayListPlayerSkia.h> #include <LibWeb/Painting/DisplayListPlayerSkia.h>
#include <LibWeb/StorageAPI/StorageShed.h>
#include <WebContent/BackingStoreManager.h> #include <WebContent/BackingStoreManager.h>
#ifdef AK_OS_MACOS #ifdef AK_OS_MACOS
@ -107,6 +108,9 @@ public:
RefPtr<Gfx::SkiaBackendContext> skia_backend_context() const { return m_skia_backend_context; } 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: private:
TraversableNavigable(GC::Ref<Page>); TraversableNavigable(GC::Ref<Page>);
@ -142,6 +146,10 @@ private:
// https://html.spec.whatwg.org/multipage/document-sequences.html#system-visibility-state // https://html.spec.whatwg.org/multipage/document-sequences.html#system-visibility-state
VisibilityState m_system_visibility_state { VisibilityState::Hidden }; 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 navigables storage shed holds all session storage data.
StorageAPI::StorageShed m_storage_shed;
GC::Ref<SessionHistoryTraversalQueue> m_session_history_traversal_queue; GC::Ref<SessionHistoryTraversalQueue> m_session_history_traversal_queue;
String m_window_handle; String m_window_handle;

View file

@ -67,6 +67,7 @@
#include <LibWeb/Painting/PaintableBox.h> #include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/RequestIdleCallback/IdleDeadline.h> #include <LibWeb/RequestIdleCallback/IdleDeadline.h>
#include <LibWeb/Selection/Selection.h> #include <LibWeb/Selection/Selection.h>
#include <LibWeb/StorageAPI/StorageBottle.h>
#include <LibWeb/WebIDL/AbstractOperations.h> #include <LibWeb/WebIDL/AbstractOperations.h>
namespace Web::HTML { namespace Web::HTML {
@ -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 // https://html.spec.whatwg.org/multipage/webstorage.html#dom-localstorage
WebIDL::ExceptionOr<GC::Ref<Storage>> Window::local_storage() WebIDL::ExceptionOr<GC::Ref<Storage>> Window::local_storage()
{ {
// See table in: https://storage.spec.whatwg.org/#registered-storage-endpoints auto& realm = this->realm();
constexpr u64 quota_bytes = 5 * MiB;
// FIXME: Implement according to spec. // 1. If this's associated Document's local storage holder is non-null, then return this's associated Document's local storage holder.
static HashMap<URL::Origin, GC::Root<Storage>> local_storage_per_origin; auto& associated_document = this->associated_document();
auto storage = local_storage_per_origin.ensure(associated_document().origin(), [this]() -> GC::Root<Storage> { if (auto storage = associated_document.local_storage_holder())
return Storage::create(realm(), Storage::Type::Local, quota_bytes); return GC::Ref { *storage };
});
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 // https://html.spec.whatwg.org/multipage/webstorage.html#dom-sessionstorage
WebIDL::ExceptionOr<GC::Ref<Storage>> Window::session_storage() WebIDL::ExceptionOr<GC::Ref<Storage>> Window::session_storage()
{ {
// See table in: https://storage.spec.whatwg.org/#registered-storage-endpoints auto& realm = this->realm();
constexpr u64 quota_bytes = 5 * MiB;
// FIXME: Implement according to spec. // 1. If this's associated Document's session storage holder is non-null, then return this's associated Document's session storage holder.
static HashMap<URL::Origin, GC::Root<Storage>> session_storage_per_origin; auto& associated_document = this->associated_document();
auto storage = session_storage_per_origin.ensure(associated_document().origin(), [this]() -> GC::Root<Storage> { if (auto storage = associated_document.session_storage_holder())
return Storage::create(realm(), Storage::Type::Session, quota_bytes); return GC::Ref { *storage };
});
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 // https://html.spec.whatwg.org/multipage/interaction.html#sticky-activation

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/StorageAPI/StorageBottle.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h>
#include <LibWeb/StorageAPI/StorageShed.h>
namespace Web::StorageAPI {
StorageBucket::StorageBucket(StorageType type)
{
// 1. Let bucket be null.
// 2. If type is "local", then set bucket to a new local storage bucket.
// 3. Otherwise:
// 1. Assert: type is "session".
// 2. Set bucket to a new session storage bucket.
// 4. For each endpoint of registered storage endpoints whose types contain type, set buckets bottle map[endpoints identifier] to a new storage bottle whose quota is endpoints quota.
for (auto const& endpoint : StorageEndpoint::registered_endpoints()) {
if (endpoint.type == type)
bottle_map.set(endpoint.identifier, StorageBottle::create(endpoint.quota));
}
// 5. Return bucket.
}
// https://storage.spec.whatwg.org/#obtain-a-storage-bottle-map
RefPtr<StorageBottle> obtain_a_storage_bottle_map(StorageType type, HTML::EnvironmentSettingsObject& environment, StringView identifier)
{
// 1. Let shed be null.
StorageShed* shed = nullptr;
// 2. If type is "local", then set shed to the user agents storage shed.
if (type == StorageType::Local) {
shed = &user_agent_storage_shed();
}
// 3. Otherwise:
else {
// 1. Assert: type is "session".
VERIFY(type == StorageType::Session);
// 2. Set shed to environments global objects associated Documents node navigables traversable navigables storage shed.
shed = &verify_cast<HTML::Window>(environment.global_object()).associated_document().navigable()->traversable_navigable()->storage_shed();
}
// 4. Let shelf be the result of running obtain a storage shelf, with shed, environment, and type.
VERIFY(shed);
auto shelf = shed->obtain_a_storage_shelf(environment, type);
// 5. If shelf is failure, then return failure.
if (!shelf.has_value())
return {};
// 6. Let bucket be shelfs bucket map["default"].
auto& bucket = shelf->bucket_map.get("default"sv).value();
// 7. Let bottle be buckets bottle map[identifier].
auto bottle = bucket.bottle_map.get(identifier).value();
// 8. Let proxyMap be a new storage proxy map whose backing map is bottles map.
// 9. Append proxyMap to bottles proxy map reference set.
// 10. Return proxyMap.
return bottle->proxy();
}
// https://storage.spec.whatwg.org/#obtain-a-session-storage-bottle-map
RefPtr<StorageBottle> obtain_a_session_storage_bottle_map(HTML::EnvironmentSettingsObject& environment, StringView identifier)
{
// To obtain a session storage bottle map, given an environment settings object environment and storage identifier identifier,
// return the result of running obtain a storage bottle map with "session", environment, and identifier.
return obtain_a_storage_bottle_map(StorageType::Session, environment, identifier);
}
// https://storage.spec.whatwg.org/#obtain-a-local-storage-bottle-map
RefPtr<StorageBottle> obtain_a_local_storage_bottle_map(HTML::EnvironmentSettingsObject& environment, StringView identifier)
{
// To obtain a local storage bottle map, given an environment settings object environment and storage identifier identifier,
// return the result of running obtain a storage bottle map with "local", environment, and identifier.
return obtain_a_storage_bottle_map(StorageType::Local, environment, identifier);
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/String.h>
#include <LibWeb/Forward.h>
#include <LibWeb/StorageAPI/StorageType.h>
namespace Web::StorageAPI {
// https://storage.spec.whatwg.org/#storage-bottle
struct StorageBottle : public RefCounted<StorageBottle> {
static NonnullRefPtr<StorageBottle> create(Optional<u64> quota) { return adopt_ref(*new StorageBottle(quota)); }
// A storage bottle has a map, which is initially an empty map
OrderedHashMap<String, String> map;
// A storage bottle also has a proxy map reference set, which is initially an empty set
NonnullRefPtr<StorageBottle> proxy() { return *this; }
// A storage bottle also has a quota, which is null or a number representing a conservative estimate of
// the total amount of bytes it can hold. Null indicates the lack of a limit.
Optional<u64> quota;
private:
explicit StorageBottle(Optional<u64> quota_)
: quota(quota_)
{
}
};
using BottleMap = OrderedHashMap<String, NonnullRefPtr<StorageBottle>>;
// https://storage.spec.whatwg.org/#storage-bucket
// A storage bucket is a place for storage endpoints to store data.
struct StorageBucket {
explicit StorageBucket(StorageType);
// A storage bucket has a bottle map of storage identifiers to storage bottles.
BottleMap bottle_map;
};
RefPtr<StorageBottle> obtain_a_session_storage_bottle_map(HTML::EnvironmentSettingsObject&, StringView storage_identifier);
RefPtr<StorageBottle> obtain_a_local_storage_bottle_map(HTML::EnvironmentSettingsObject&, StringView storage_identifier);
RefPtr<StorageBottle> obtain_a_storage_bottle_map(StorageType, HTML::EnvironmentSettingsObject&, StringView storage_identifier);
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/StorageAPI/StorageEndpoint.h>
namespace Web::StorageAPI {
ReadonlySpan<StorageEndpoint> StorageEndpoint::registered_endpoints()
{
// https://storage.spec.whatwg.org/#registered-storage-endpoints
static auto const endpoints = to_array<StorageEndpoint>({
{ "caches"_string, StorageType::Local, {} },
{ "indexedDB"_string, StorageType::Local, {} },
{ "localStorage"_string, StorageType::Local, 5 * MiB },
{ "serviceWorkerRegistrations"_string, StorageType::Local, {} },
{ "sessionStorage"_string, StorageType::Session, 5 * MiB },
});
return endpoints;
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Array.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Types.h>
#include <LibWeb/StorageAPI/StorageType.h>
namespace Web::StorageAPI {
// https://storage.spec.whatwg.org/#storage-endpoint
//
// A storage endpoint is a local or session storage API that uses the infrastructure defined by this
// standard, most notably storage bottles, to keep track of its storage needs.
struct StorageEndpoint {
// https://storage.spec.whatwg.org/#storage-endpoint-identifier
// A storage endpoint has an identifier, which is a storage identifier.
String identifier;
// https://storage.spec.whatwg.org/#storage-endpoint-types
// A storage endpoint also has types, which is a set of storage types.
// NOTE: We do not implement this as a set as it is not neccessary in the current implementation.
StorageType type;
// https://storage.spec.whatwg.org/#storage-endpoint-quota
// A storage endpoint also has a quota, which is null or a number representing a recommended quota (in bytes) for each storage bottle corresponding to this storage endpoint.
Optional<u64> quota;
static ReadonlySpan<StorageEndpoint> registered_endpoints();
};
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/StorageAPI/StorageShed.h>
namespace Web::StorageAPI {
// https://storage.spec.whatwg.org/#obtain-a-storage-shelf
Optional<StorageShelf&> StorageShed::obtain_a_storage_shelf(HTML::EnvironmentSettingsObject const& environment, StorageType type)
{
// 1. Let key be the result of running obtain a storage key with environment.
auto key = obtain_a_storage_key(environment);
// 2. If key is failure, then return failure.
if (!key.has_value())
return {};
// 3. If shed[key] does not exist, then set shed[key] to the result of running create a storage shelf with type.
// 4. Return shed[key].
return m_data.ensure(key.value(), [type] {
return StorageShelf { type };
});
}
// https://storage.spec.whatwg.org/#user-agent-storage-shed
StorageShed& user_agent_storage_shed()
{
// A user agent holds a storage shed, which is a storage shed. A user agents storage shed holds all local storage data.
// FIXME: Storing this statically in memory is not the correct place or way of doing this!
static StorageShed storage_shed;
return storage_shed;
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <LibWeb/Forward.h>
#include <LibWeb/StorageAPI/StorageKey.h>
#include <LibWeb/StorageAPI/StorageShelf.h>
#include <LibWeb/StorageAPI/StorageType.h>
namespace Web::StorageAPI {
// https://storage.spec.whatwg.org/#storage-shed
// A storage shed is a map of storage keys to storage shelves. It is initially empty.
class StorageShed {
public:
Optional<StorageShelf&> obtain_a_storage_shelf(HTML::EnvironmentSettingsObject const&, StorageType);
private:
OrderedHashMap<StorageKey, StorageShelf> m_data;
};
StorageShed& user_agent_storage_shed();
}

View file

@ -0,0 +1,20 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/StorageAPI/StorageShelf.h>
namespace Web::StorageAPI {
// https://storage.spec.whatwg.org/#create-a-storage-shelf
StorageShelf::StorageShelf(StorageType type)
{
// 1. Let shelf be a new storage shelf.
// 2. Set shelfs bucket map["default"] to the result of running create a storage bucket with type.
bucket_map.set("default"_string, StorageBucket { type });
// 3. Return shelf.
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/String.h>
#include <LibWeb/Forward.h>
#include <LibWeb/StorageAPI/StorageBottle.h>
#include <LibWeb/StorageAPI/StorageType.h>
namespace Web::StorageAPI {
// https://storage.spec.whatwg.org/#storage-shelf
// A storage shelf exists for each storage key within a storage shed. It holds a bucket map, which is a map of strings to storage buckets.
using BucketMap = OrderedHashMap<String, StorageBucket>;
struct StorageShelf {
explicit StorageShelf(StorageType);
BucketMap bucket_map;
};
}

View file

@ -0,0 +1,18 @@
/*
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace Web::StorageAPI {
// https://storage.spec.whatwg.org/#storage-type
// A storage type is "local" or "session".
enum class StorageType {
Local,
Session,
};
}

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass storageArea property test of session event - session event is fired due to an invocation of the setItem() method.

View file

@ -0,0 +1,40 @@
<!DOCTYPE HTML>
<html>
<head>
<title>WebStorage Test: sessionStorage event - storageArea</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
</head>
<body>
<h1>event_session_storageArea</h1>
<div id="log"></div>
<script>
async_test(function(t) {
sessionStorage.clear();
t.add_cleanup(function() { sessionStorage.clear() });
self.fail = t.step_func(function(msg) {
assert_unreached(msg);
t.done();
});
function onStorageEvent(event) {
assert_equals(event.storageArea.length, 1);
var key = event.storageArea.key(0);
var value = event.storageArea.getItem(key);
assert_equals(key, "name");
assert_equals(value, "user1");
t.done();
}
window.addEventListener('storage', t.step_func(onStorageEvent), false);
var el = document.createElement("iframe");
el.setAttribute('id', 'ifrm');
el.setAttribute('src', 'resources/session_set_item_iframe.html');
document.body.appendChild(el);
}, "storageArea property test of session event - session event is fired due to an invocation of the setItem() method.");
</script>
</body>
</html>

View file

@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html>
<body>
<script>
if (('sessionStorage' in window) && window.sessionStorage !== null){
try {
sessionStorage.setItem('name', 'user1');
} catch (e) {
parent.fail('setItem method is failed.');
}
} else {
parent.fail('sessionStorage is not supported.');
}
</script>
</body>
</html>