ladybird/Libraries/LibWeb/NotificationsAPI/Notification.cpp

250 lines
10 KiB
C++
Raw Normal View History

/*
* 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 = TRY(HTML::structured_serialize_for_storage(realm.vm(), options.data));
// 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();
}
}