/* * Copyright (c) 2024, Jamie Mansfield * Copyright (c) 2024, Shannon Booth * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include namespace Web::HTML { class BroadcastChannelRepository { public: void register_channel(GC::Root); void unregister_channel(GC::Ref); Vector> const& registered_channels_for_key(StorageAPI::StorageKey) const; private: HashMap>> m_channels; }; void BroadcastChannelRepository::register_channel(GC::Root channel) { auto storage_key = Web::StorageAPI::obtain_a_storage_key_for_non_storage_purposes(relevant_settings_object(*channel)); auto maybe_channels = m_channels.find(storage_key); if (maybe_channels != m_channels.end()) { maybe_channels->value.append(move(channel)); return; } Vector> channels; channels.append(move(channel)); m_channels.set(storage_key, move(channels)); } void BroadcastChannelRepository::unregister_channel(GC::Ref channel) { auto storage_key = Web::StorageAPI::obtain_a_storage_key_for_non_storage_purposes(relevant_settings_object(channel)); auto& relevant_channels = m_channels.get(storage_key).value(); relevant_channels.remove_first_matching([&](auto c) { return c == channel; }); } Vector> const& BroadcastChannelRepository::registered_channels_for_key(StorageAPI::StorageKey key) const { auto maybe_channels = m_channels.get(key); VERIFY(maybe_channels.has_value()); return maybe_channels.value(); } // FIXME: This should not be static, and live at a storage partitioned level of the user agent. static BroadcastChannelRepository s_broadcast_channel_repository; GC_DEFINE_ALLOCATOR(BroadcastChannel); GC::Ref BroadcastChannel::construct_impl(JS::Realm& realm, FlyString const& name) { auto channel = realm.create(realm, name); s_broadcast_channel_repository.register_channel(channel); return channel; } BroadcastChannel::BroadcastChannel(JS::Realm& realm, FlyString const& name) : DOM::EventTarget(realm) , m_channel_name(name) { } void BroadcastChannel::initialize(JS::Realm& realm) { Base::initialize(realm); WEB_SET_PROTOTYPE_FOR_INTERFACE(BroadcastChannel); } // https://html.spec.whatwg.org/multipage/web-messaging.html#eligible-for-messaging bool BroadcastChannel::is_eligible_for_messaging() const { // A BroadcastChannel object is said to be eligible for messaging when its relevant global object is either: auto const& global = relevant_global_object(*this); // * a Window object whose associated Document is fully active, or if (is(global)) return static_cast(global).associated_document().is_fully_active(); // * a WorkerGlobalScope object whose closing flag is false and whose worker is not a suspendable worker. // FIXME: Suspendable worker if (is(global)) return !static_cast(global).is_closing(); return false; } // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-broadcastchannel-postmessage WebIDL::ExceptionOr BroadcastChannel::post_message(JS::Value message) { auto& vm = this->vm(); // 1. If this is not eligible for messaging, then return. if (!is_eligible_for_messaging()) return {}; // 2. If this's closed flag is true, then throw an "InvalidStateError" DOMException. if (m_closed_flag) return WebIDL::InvalidStateError::create(realm(), "BroadcastChannel.postMessage() on a closed channel"_string); // 3. Let serialized be StructuredSerialize(message). Rethrow any exceptions. auto serialized = TRY(structured_serialize(vm, message)); // 4. Let sourceOrigin be this's relevant settings object's origin. auto source_origin = relevant_settings_object(*this).origin(); // 5. Let sourceStorageKey be the result of running obtain a storage key for non-storage purposes with this's relevant settings object. auto source_storage_key = Web::StorageAPI::obtain_a_storage_key_for_non_storage_purposes(relevant_settings_object(*this)); // 6. Let destinations be a list of BroadcastChannel objects that match the following criteria: GC::MarkedVector> destinations(vm.heap()); // * The result of running obtain a storage key for non-storage purposes with their relevant settings object equals sourceStorageKey. auto same_origin_broadcast_channels = s_broadcast_channel_repository.registered_channels_for_key(source_storage_key); for (auto const& channel : same_origin_broadcast_channels) { // * They are eligible for messaging. if (!channel->is_eligible_for_messaging()) continue; // * Their channel name is this's channel name. if (channel->name() != name()) continue; destinations.append(*channel); } // 7. Remove source from destinations. destinations.remove_first_matching([&](auto destination) { return destination == this; }); // FIXME: 8. Sort destinations such that all BroadcastChannel objects whose relevant agents are the same are sorted in creation order, oldest first. // (This does not define a complete ordering. Within this constraint, user agents may sort the list in any implementation-defined manner.) // 9. For each destination in destinations, queue a global task on the DOM manipulation task source given destination's relevant global object to perform the following steps: for (auto destination : destinations) { HTML::queue_global_task(HTML::Task::Source::DOMManipulation, relevant_global_object(destination), GC::create_function(vm.heap(), [&vm, serialized, destination, source_origin] { // 1. If destination's closed flag is true, then abort these steps. if (destination->m_closed_flag) return; // 2. Let targetRealm be destination's relevant realm. auto& target_realm = relevant_realm(destination); // 3. Let data be StructuredDeserialize(serialized, targetRealm). // If this throws an exception, catch it, fire an event named messageerror at destination, using MessageEvent, with the // origin attribute initialized to the serialization of sourceOrigin, and then abort these steps. auto data_or_error = structured_deserialize(vm, serialized, target_realm); if (data_or_error.is_exception()) { MessageEventInit event_init {}; event_init.origin = source_origin.serialize(); auto event = MessageEvent::create(target_realm, HTML::EventNames::messageerror, event_init); event->set_is_trusted(true); destination->dispatch_event(event); return; } // 4. Fire an event named message at destination, using MessageEvent, with the data attribute initialized to data and the // origin attribute initialized to the serialization of sourceOrigin. MessageEventInit event_init {}; event_init.data = data_or_error.release_value(); event_init.origin = source_origin.serialize(); auto event = MessageEvent::create(target_realm, HTML::EventNames::message, event_init); event->set_is_trusted(true); destination->dispatch_event(event); })); } return {}; } // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-broadcastchannel-close void BroadcastChannel::close() { // The close() method steps are to set this's closed flag to true. m_closed_flag = true; s_broadcast_channel_repository.unregister_channel(*this); } // https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessage void BroadcastChannel::set_onmessage(GC::Ptr event_handler) { set_event_handler_attribute(HTML::EventNames::message, event_handler); } // https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessage GC::Ptr BroadcastChannel::onmessage() { return event_handler_attribute(HTML::EventNames::message); } // https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessageerror void BroadcastChannel::set_onmessageerror(GC::Ptr event_handler) { set_event_handler_attribute(HTML::EventNames::messageerror, event_handler); } // https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessageerror GC::Ptr BroadcastChannel::onmessageerror() { return event_handler_attribute(HTML::EventNames::messageerror); } }