ladybird/Libraries/LibWeb/NotificationsAPI/Notification.cpp
Niccolo Antonelli Dziri bd76078f97 LibWeb: Add constructor for Notification and getter methods
The full constructor for NotificationsAPI::Notification is implemented
along with the getter methods.
It is now possible to call the elements of Notification in JS without
getting undefined but the default values (or the ones passed in
options).

The method `current_wall_time` is added in EnvironmentSettingsObject.

This passes a least a few more tests because of the getter methods
that are created.

https://wpt.live/notifications/constructor-basic.https.html

https://wpt.live/notifications/idlharness.https.any.html
2026-02-13 16:47:42 +00:00

249 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2025, Niccolo Antonelli-Dziri <niccolo.antonelli-dziri@protonmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Math.h>
#include <AK/Time.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/NotificationPrototype.h>
#include <LibWeb/HTML/StructuredSerialize.h>
#include <LibWeb/NotificationsAPI/Notification.h>
#include <LibWeb/ServiceWorker/ServiceWorkerGlobalScope.h>
namespace Web::NotificationsAPI {
GC_DEFINE_ALLOCATOR(Notification);
Notification::Notification(JS::Realm& realm)
: DOM::EventTarget(realm)
{
}
// https://notifications.spec.whatwg.org/#create-a-notification
WebIDL::ExceptionOr<ConceptNotification> Notification::create_a_notification(
JS::Realm& realm,
String const& title,
NotificationOptions const& options,
URL::Origin origin,
URL::URL base_url,
HighResolutionTime::EpochTimeStamp fallback_timestamp)
{
// 1. Let notification be a new notification.
ConceptNotification notification;
// FIXME: 2. If options["silent"] is true and options["vibrate"] exists, then throw a TypeError.
// 3. If options["renotify"] is true and options["tag"] is the empty string, then throw a TypeError.
if (options.renotify && options.tag.is_empty())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "options[\"tag\"] cannot be the empty string when options[\"renotify\"] is set to true."sv };
// 4. Set notifications data to StructuredSerializeForStorage(options["data"]).
notification.data = HTML::structured_serialize_for_storage(realm.vm(), options.data).release_value();
// 5. Set notifications title to title.
notification.title = title;
// 6. Set notifications direction to options["dir"].
notification.direction = options.dir;
// 7. Set notifications language to options["lang"].
notification.language = options.lang;
// 8. Set notifications origin to origin.
notification.origin = move(origin);
// 9. Set notifications body to options["body"].
notification.body = options.body;
// 10. If options["navigate"] exists, then parse it using baseURL, and if that does not return failure,
// set notifications navigation URL to the return value. (Otherwise notifications navigation URL remains null.)
if (options.navigate.has_value()) {
notification.navigation_url = base_url.complete_url(options.navigate.value());
}
// 11. Set notifications tag to options["tag"].
notification.tag = options.tag;
// 12. If options["image"] exists, then parse it using baseURL, and if that does not return failure,
// set notifications image URL to the return value. (Otherwise notifications image URL is not set.)
if (options.image.has_value()) {
notification.image_url = base_url.complete_url(options.image.value());
}
// 13. If options["icon"] exists, then parse it using baseURL, and if that does not return failure,
// set notifications icon URL to the return value. (Otherwise notifications icon URL is not set.)
if (options.icon.has_value()) {
notification.icon_url = base_url.complete_url(options.icon.value());
}
// 14. If options["badge"] exists, then parse it using baseURL, and if that does not return failure,
// set notifications badge URL to the return value. (Otherwise notifications badge URL is not set.)
if (options.badge.has_value()) {
notification.badge_url = base_url.complete_url(options.badge.value());
}
// FIXME: 15. If options["vibrate"] exists, then validate and normalize it and
// set notifications vibration pattern to the return value.
// 16. If options["timestamp"] exists, then set notifications timestamp to the value.
// Otherwise, set notifications timestamp to fallbackTimestamp.
if (options.timestamp.has_value())
notification.timestamp = options.timestamp.value();
else
notification.timestamp = fallback_timestamp;
// 17. Set notifications renotify preference to options["renotify"].
notification.renotify_preference = options.renotify;
// 18. Set notifications silent preference to options["silent"].
notification.silent_preference = options.silent;
// 19. Set notifications require interaction preference to options["requireInteraction"].
notification.require_interaction_preference = options.require_interaction;
// 20. Set notifications actions to « ».
notification.actions = {};
// 21. For each entry in options["actions"], up to the maximum number of actions supported (skip any excess entries):
for (auto const& entry : options.actions) {
// FIXME: stop the loop at the max number of actions supported
// 1. Let action be a new notification action.
ConceptNotification::Action action;
// 2. Set actions name to entry["action"].
action.name = entry.action;
// 3. Set actions title to entry["title"].
action.title = entry.title;
// 4. If entry["navigate"] exists, then parse it using baseURL, and if that does not return failure,
// set actions navigation URL to the return value. (Otherwise actions navigation URL remains null.)
if (entry.navigate.has_value())
action.navigation_url = base_url.complete_url(entry.navigate.value());
// 5. If entry["icon"] exists, then parse it using baseURL, and if that does not return failure,
// set actions icon URL to the return value. (Otherwise actions icon URL remains null.)
if (entry.icon.has_value())
action.icon_url = base_url.complete_url(entry.icon.value());
// 6. Append action to notifications actions.
notification.actions.append(action);
}
// 22. Return notification.
return notification;
}
// https://notifications.spec.whatwg.org/#create-a-notification-with-a-settings-object
WebIDL::ExceptionOr<ConceptNotification> Notification::create_a_notification_with_a_settings_object(
JS::Realm& realm,
String const& title,
NotificationOptions const& options,
GC::Ref<HTML::EnvironmentSettingsObject> settings)
{
// 1. Let origin be settingss origin.
URL::Origin origin = settings->origin();
// 2. Let baseURL be settingss API base URL.
URL::URL base_url = settings->api_base_url();
// 3. Let fallbackTimestamp be the number of milliseconds from the Unix epoch to settingss current wall time,
// rounded to the nearest integer.
auto fallback_timestamp = round_to<HighResolutionTime::EpochTimeStamp>(settings->current_wall_time());
// 4. Return the result of creating a notification given title, options, origin, baseURL, and fallbackTimestamp.
return create_a_notification(realm, title, options, origin, base_url, fallback_timestamp);
}
// https://notifications.spec.whatwg.org/#constructors
WebIDL::ExceptionOr<GC::Ref<Notification>> Notification::construct_impl(
JS::Realm& realm,
String const& title,
NotificationOptions const& options)
{
auto this_notification = realm.create<Notification>(realm);
auto& relevant_settings_object = HTML::relevant_settings_object(this_notification);
auto& relevant_global_object = HTML::relevant_global_object(this_notification);
// 1. If thiss relevant global object is a ServiceWorkerGlobalScope object, then throw a TypeError.
if (is<ServiceWorker::ServiceWorkerGlobalScope>(relevant_global_object))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Thiss relevant global object is a ServiceWorkerGlobalScope object"sv };
// 2. If options["actions"] is not empty, then throw a TypeError.
if (!options.actions.is_empty())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Options `action` is not empty"sv };
// 3. Let notification be the result of creating a notification with a settings object given title, options, and thiss relevant settings object.
ConceptNotification notification = TRY(create_a_notification_with_a_settings_object(realm, title, options, relevant_settings_object));
// 4. Associate this with notification.
this_notification->m_notification = notification;
// FIXME: 5. Run these steps in parallel:
// FIXME: 1. If the result of getting the notifications permission state is not "granted",
// then queue a task to fire an event named error on this, and abort these steps.
// FIXME: 2. Run the notification show steps for notification.
return this_notification;
}
void Notification::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(Notification);
Base::initialize(realm);
}
// https://notifications.spec.whatwg.org/#dom-notification-actions
Vector<NotificationAction> Notification::actions() const
{
// 1. Let frozenActions be an empty list of type NotificationAction.
Vector<NotificationAction> frozen_actions;
frozen_actions.ensure_capacity(m_notification.actions.capacity());
// 2. For each entry of thiss notifications actions:
for (auto const& entry : m_notification.actions) {
// 1. Let action be a new NotificationAction.
NotificationAction action;
// 2. Set action["action"] to entrys name.
action.action = entry.name;
// 3. Set action["title"] to entrys title.
action.title = entry.title;
// 4. If entrys navigation URL is non-null, then set action["navigate"] to entrys navigation URL, serialized.
if (entry.navigation_url.has_value())
action.navigate = entry.navigation_url->serialize();
// 5. If entrys icon URL is non-null, then set action["icon"] to entrys icon URL, serialized.
if (entry.icon_url.has_value())
action.icon = entry.icon_url->serialize();
// FIXME: 6. Call Object.freeze on action, to prevent accidental mutation by scripts.
// 7. Append action to frozenActions.
frozen_actions.append(action);
}
// FIXME: 3. Return the result of create a frozen array from frozenActions.
return frozen_actions;
}
// https://notifications.spec.whatwg.org/#dom-notification-data
JS::Value Notification::data() const
{
// The data getter steps are to return StructuredDeserialize(thiss notifications data, thiss relevant Realm).
// If this throws an exception, then return null.
auto deserialized_data = HTML::structured_deserialize(vm(), m_notification.data, realm());
if (!deserialized_data.is_exception())
return deserialized_data.release_value();
return JS::js_null();
}
}