/* * Copyright (c) 2022, Andreas Kling * Copyright (c) 2023, Luke Wilde * Copyright (c) 2024-2025, Shannon Booth * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include namespace Web::HTML { GC_DEFINE_ALLOCATOR(Storage); static HashTable>& all_storages() { // FIXME: This needs to be stored at the user agent level. static HashTable> storages; return storages; } GC::Ref Storage::create(JS::Realm& realm, Type type, NonnullRefPtr storage_bottle) { return realm.create(realm, type, move(storage_bottle)); } Storage::Storage(JS::Realm& realm, Type type, NonnullRefPtr storage_bottle) : Bindings::PlatformObject(realm) , m_type(type) , m_storage_bottle(move(storage_bottle)) { m_legacy_platform_object_flags = LegacyPlatformObjectFlags { .supports_indexed_properties = true, .supports_named_properties = true, .has_indexed_property_setter = true, .has_named_property_setter = true, .has_named_property_deleter = true, .indexed_property_setter_has_identifier = true, .named_property_setter_has_identifier = true, .named_property_deleter_has_identifier = true, }; for (auto const& item : map()) m_stored_bytes += item.key.byte_count() + item.value.byte_count(); all_storages().set(*this); } Storage::~Storage() = default; void Storage::initialize(JS::Realm& realm) { WEB_SET_PROTOTYPE_FOR_INTERFACE(Storage); Base::initialize(realm); } void Storage::finalize() { all_storages().remove(*this); } // https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-length size_t Storage::length() const { // The length getter steps are to return this's map's size. return map().size(); } // https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key Optional Storage::key(size_t index) { // 1. If index is greater than or equal to this's map's size, then return null. if (index >= map().size()) return {}; // 2. Let keys be the result of running get the keys on this's map. auto keys = map().keys(); // 3. Return keys[index]. return keys[index]; } // https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-getitem Optional Storage::get_item(StringView key) const { // 1. If this's map[key] does not exist, then return null. auto it = map().find(key); if (it == map().end()) return {}; // 2. Return this's map[key]. return it->value; } // https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-setitem WebIDL::ExceptionOr Storage::set_item(String const& key, String const& value) { auto& realm = this->realm(); // 1. Let oldValue be null. Optional old_value; // 2. Let reorder be true. bool reorder = true; // 3. If this's map[key] exists: auto new_size = m_stored_bytes; if (auto it = map().find(key); it != map().end()) { // 1. Set oldValue to this's map[key]. old_value = it->value; // 2. If oldValue is value, then return. if (old_value == value) return {}; // 3. Set reorder to false. reorder = false; } else { new_size += key.bytes().size(); } // 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 (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", *m_storage_bottle->quota))); // 5. Set this's map[key] to value. map().set(key, value); m_stored_bytes = new_size; // 6. If reorder is true, then reorder this. if (reorder) this->reorder(); // 7. Broadcast this with key, oldValue, and value. broadcast(key, old_value, value); return {}; } // https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-removeitem void Storage::remove_item(String const& key) { // 1. If this's map[key] does not exist, then return. 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]. map().remove(it); m_stored_bytes = m_stored_bytes - key.bytes().size() - old_value.bytes().size(); // 4. Reorder this. reorder(); // 5. Broadcast this with key, oldValue, and null. broadcast(key, old_value, {}); } // https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-clear void Storage::clear() { // 1. Clear this's map. map().clear(); // 2. Broadcast this with null, null, and null. broadcast({}, {}, {}); } // https://html.spec.whatwg.org/multipage/webstorage.html#concept-storage-reorder void Storage::reorder() { // To reorder a Storage object storage, reorder storage's map's entries in an implementation-defined manner. // NOTE: This basically means that we're not required to maintain any particular iteration order. } // https://html.spec.whatwg.org/multipage/webstorage.html#concept-storage-broadcast void Storage::broadcast(Optional const& key, Optional const& old_value, Optional const& new_value) { auto& realm = this->realm(); // 1. Let thisDocument be storage's relevant global object's associated Document. auto& relevant_global = relevant_global_object(*this); auto const& this_document = as(relevant_global).associated_document(); // 2. Let url be the serialization of thisDocument's URL. auto url = this_document.url().serialize(); // 3. Let remoteStorages be all Storage objects excluding storage whose: GC::RootVector> remote_storages(heap()); for (auto storage : all_storages()) { if (storage == this) continue; // * type is storage's type if (storage->type() != type()) continue; // * relevant settings object's origin is same origin with storage's relevant settings object's origin. if (!relevant_settings_object(*this).origin().is_same_origin(relevant_settings_object(storage).origin())) continue; // * and, if type is "session", whose relevant settings object's associated Document's node navigable's traversable navigable // is thisDocument's node navigable's traversable navigable. if (type() == Type::Session) { auto& storage_document = *relevant_settings_object(storage).responsible_document(); // NOTE: It is possible the remote storage may have not been fully teared down immediately at the point it's document is made inactive. if (!storage_document.navigable()) continue; VERIFY(this_document.navigable()); if (storage_document.navigable()->traversable_navigable() != this_document.navigable()->traversable_navigable()) continue; } remote_storages.append(storage); } // 4. For each remoteStorage of remoteStorages: queue a global task on the DOM manipulation task source given remoteStorage's relevant // global object to fire an event named storage at remoteStorage's relevant global object, using StorageEvent, with key initialized // to key, oldValue initialized to oldValue, newValue initialized to newValue, url initialized to url, and storageArea initialized to // remoteStorage. for (auto remote_storage : remote_storages) { queue_global_task(Task::Source::DOMManipulation, relevant_global, GC::create_function(heap(), [&realm, key, old_value, new_value, url, remote_storage] { StorageEventInit init; init.key = move(key); init.old_value = move(old_value); init.new_value = move(new_value); init.url = move(url); init.storage_area = remote_storage; as(relevant_global_object(remote_storage)).dispatch_event(StorageEvent::create(realm, EventNames::storage, init)); })); } } Vector 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 names; names.ensure_capacity(map().size()); for (auto const& key : map().keys()) names.unchecked_append(key); return names; } Optional Storage::item_value(size_t index) const { // Handle index as a string since that's our key type auto key = String::number(index); auto value = get_item(key); if (!value.has_value()) return {}; return JS::PrimitiveString::create(vm(), value.release_value()); } JS::Value Storage::named_item_value(FlyString const& name) const { auto value = get_item(name); if (!value.has_value()) // AD-HOC: Spec leaves open to a description at: https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface // However correct behavior expected here: https://github.com/whatwg/html/issues/8684 return JS::js_undefined(); return JS::PrimitiveString::create(vm(), value.release_value()); } WebIDL::ExceptionOr Storage::delete_value(String const& name) { remove_item(name); return DidDeletionFail::NotRelevant; } WebIDL::ExceptionOr Storage::set_value_of_indexed_property(u32 index, JS::Value unconverted_value) { // Handle index as a string since that's our key type auto key = String::number(index); return set_value_of_named_property(key, unconverted_value); } WebIDL::ExceptionOr Storage::set_value_of_named_property(String const& key, JS::Value unconverted_value) { // NOTE: Since PlatformObject does not know the type of value, we must convert it ourselves. // The type of `value` is `DOMString`. auto value = TRY(unconverted_value.to_string(vm())); return set_item(key, value); } void Storage::dump() const { dbgln("Storage ({} key(s))", map().size()); size_t i = 0; for (auto const& it : map()) { dbgln("[{}] \"{}\": \"{}\"", i, it.key, it.value); ++i; } } }