script/dom/
notification.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::rc::Rc;
6use std::sync::Arc;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use dom_struct::dom_struct;
10use embedder_traits::{
11    EmbedderMsg, Notification as EmbedderNotification,
12    NotificationAction as EmbedderNotificationAction,
13};
14use js::jsapi::Heap;
15use js::jsval::JSVal;
16use js::rust::{HandleObject, MutableHandleValue};
17use net_traits::http_status::HttpStatus;
18use net_traits::image_cache::{
19    ImageCache, ImageCacheResponseMessage, ImageCacheResult, ImageLoadListener,
20    ImageOrMetadataAvailable, ImageResponse, PendingImageId,
21};
22use net_traits::request::{Destination, RequestBuilder, RequestId};
23use net_traits::{FetchMetadata, FetchResponseMsg, NetworkError, ResourceFetchTiming};
24use pixels::RasterImage;
25use rustc_hash::FxHashSet;
26use servo_url::{ImmutableOrigin, ServoUrl};
27use uuid::Uuid;
28
29use super::bindings::cell::DomRefCell;
30use super::bindings::refcounted::{Trusted, TrustedPromise};
31use super::bindings::reflector::DomGlobal;
32use super::performanceresourcetiming::InitiatorType;
33use super::permissionstatus::PermissionStatus;
34use crate::dom::bindings::callback::ExceptionHandling;
35use crate::dom::bindings::codegen::Bindings::NotificationBinding::{
36    NotificationAction, NotificationDirection, NotificationMethods, NotificationOptions,
37    NotificationPermission, NotificationPermissionCallback,
38};
39use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionStatus_Binding::PermissionStatusMethods;
40use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::{
41    PermissionDescriptor, PermissionName, PermissionState,
42};
43use crate::dom::bindings::codegen::UnionTypes::UnsignedLongOrUnsignedLongSequence;
44use crate::dom::bindings::error::{Error, Fallible};
45use crate::dom::bindings::inheritance::Castable;
46use crate::dom::bindings::reflector::reflect_dom_object_with_proto_and_cx;
47use crate::dom::bindings::root::{Dom, DomRoot};
48use crate::dom::bindings::str::{DOMString, USVString};
49use crate::dom::bindings::trace::RootedTraceableBox;
50use crate::dom::bindings::utils::to_frozen_array;
51use crate::dom::csp::{GlobalCspReporting, Violation};
52use crate::dom::eventtarget::EventTarget;
53use crate::dom::globalscope::GlobalScope;
54use crate::dom::permissions::{PermissionAlgorithm, Permissions, descriptor_permission_state};
55use crate::dom::promise::Promise;
56use crate::dom::serviceworkerglobalscope::ServiceWorkerGlobalScope;
57use crate::dom::serviceworkerregistration::ServiceWorkerRegistration;
58use crate::fetch::{RequestWithGlobalScope, create_a_potential_cors_request};
59use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
60use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
61
62// TODO: Service Worker API (persistent notification)
63// https://notifications.spec.whatwg.org/#service-worker-api
64
65/// <https://notifications.spec.whatwg.org/#notifications>
66#[dom_struct]
67pub(crate) struct Notification {
68    eventtarget: EventTarget,
69    /// <https://notifications.spec.whatwg.org/#service-worker-registration>
70    serviceworker_registration: Option<Dom<ServiceWorkerRegistration>>,
71    /// <https://notifications.spec.whatwg.org/#concept-title>
72    title: DOMString,
73    /// <https://notifications.spec.whatwg.org/#body>
74    body: DOMString,
75    /// <https://notifications.spec.whatwg.org/#data>
76    #[ignore_malloc_size_of = "mozjs"]
77    data: Heap<JSVal>,
78    /// <https://notifications.spec.whatwg.org/#concept-direction>
79    dir: NotificationDirection,
80    /// <https://notifications.spec.whatwg.org/#image-url>
81    image: Option<USVString>,
82    /// <https://notifications.spec.whatwg.org/#icon-url>
83    icon: Option<USVString>,
84    /// <https://notifications.spec.whatwg.org/#badge-url>
85    badge: Option<USVString>,
86    /// <https://notifications.spec.whatwg.org/#concept-language>
87    lang: DOMString,
88    /// <https://notifications.spec.whatwg.org/#silent-preference-flag>
89    silent: Option<bool>,
90    /// <https://notifications.spec.whatwg.org/#tag>
91    tag: DOMString,
92    /// <https://notifications.spec.whatwg.org/#concept-origin>
93    #[no_trace] // ImmutableOrigin is not traceable
94    origin: ImmutableOrigin,
95    /// <https://notifications.spec.whatwg.org/#vibration-pattern>
96    vibration_pattern: Vec<u32>,
97    /// <https://notifications.spec.whatwg.org/#timestamp>
98    timestamp: u64,
99    /// <https://notifications.spec.whatwg.org/#renotify-preference-flag>
100    renotify: bool,
101    /// <https://notifications.spec.whatwg.org/#require-interaction-preference-flag>
102    require_interaction: bool,
103    /// <https://notifications.spec.whatwg.org/#actions>
104    actions: Vec<Action>,
105    /// Pending image, icon, badge, action icon resource request's id
106    #[no_trace] // RequestId is not traceable
107    pending_request_ids: DomRefCell<FxHashSet<RequestId>>,
108    /// <https://notifications.spec.whatwg.org/#image-resource>
109    #[ignore_malloc_size_of = "RasterImage"]
110    #[no_trace]
111    image_resource: DomRefCell<Option<Arc<RasterImage>>>,
112    /// <https://notifications.spec.whatwg.org/#icon-resource>
113    #[ignore_malloc_size_of = "RasterImage"]
114    #[no_trace]
115    icon_resource: DomRefCell<Option<Arc<RasterImage>>>,
116    /// <https://notifications.spec.whatwg.org/#badge-resource>
117    #[ignore_malloc_size_of = "RasterImage"]
118    #[no_trace]
119    badge_resource: DomRefCell<Option<Arc<RasterImage>>>,
120}
121
122impl Notification {
123    #[expect(clippy::too_many_arguments)]
124    pub(crate) fn new(
125        cx: &mut js::context::JSContext,
126        global: &GlobalScope,
127        title: DOMString,
128        options: RootedTraceableBox<NotificationOptions>,
129        origin: ImmutableOrigin,
130        base_url: ServoUrl,
131        fallback_timestamp: u64,
132        proto: Option<HandleObject>,
133    ) -> DomRoot<Self> {
134        let notification = reflect_dom_object_with_proto_and_cx(
135            Box::new(Notification::new_inherited(
136                global,
137                title,
138                &options,
139                origin,
140                base_url,
141                fallback_timestamp,
142            )),
143            global,
144            proto,
145            cx,
146        );
147
148        notification.data.set(options.data.get());
149
150        notification
151    }
152
153    /// partial implementation of <https://notifications.spec.whatwg.org/#create-a-notification>
154    fn new_inherited(
155        global: &GlobalScope,
156        title: DOMString,
157        options: &RootedTraceableBox<NotificationOptions>,
158        origin: ImmutableOrigin,
159        base_url: ServoUrl,
160        fallback_timestamp: u64,
161    ) -> Self {
162        // TODO: missing call to https://html.spec.whatwg.org/multipage/#structuredserializeforstorage
163        // may be find in `dom/bindings/structuredclone.rs`
164
165        let dir = options.dir;
166        let lang = options.lang.clone();
167        let body = options.body.clone();
168        let tag = options.tag.clone();
169
170        // If options["image"] exists, then parse it using baseURL, and if that does not return failure,
171        // set notification’s image URL to the return value. (Otherwise notification’s image URL is not set.)
172        let image = options.image.as_ref().and_then(|image_url| {
173            ServoUrl::parse_with_base(Some(&base_url), image_url.as_ref())
174                .map(|url| USVString::from(url.to_string()))
175                .ok()
176        });
177        // If options["icon"] exists, then parse it using baseURL, and if that does not return failure,
178        // set notification’s icon URL to the return value. (Otherwise notification’s icon URL is not set.)
179        let icon = options.icon.as_ref().and_then(|icon_url| {
180            ServoUrl::parse_with_base(Some(&base_url), icon_url.as_ref())
181                .map(|url| USVString::from(url.to_string()))
182                .ok()
183        });
184        // If options["badge"] exists, then parse it using baseURL, and if that does not return failure,
185        // set notification’s badge URL to the return value. (Otherwise notification’s badge URL is not set.)
186        let badge = options.badge.as_ref().and_then(|badge_url| {
187            ServoUrl::parse_with_base(Some(&base_url), badge_url.as_ref())
188                .map(|url| USVString::from(url.to_string()))
189                .ok()
190        });
191        // If options["vibrate"] exists, then validate and normalize it and
192        // set notification’s vibration pattern to the return value.
193        let vibration_pattern = match &options.vibrate {
194            Some(pattern) => validate_and_normalize_vibration_pattern(pattern),
195            None => Vec::new(),
196        };
197        // If options["timestamp"] exists, then set notification’s timestamp to the value.
198        // Otherwise, set notification’s timestamp to fallbackTimestamp.
199        let timestamp = options.timestamp.unwrap_or(fallback_timestamp);
200        let renotify = options.renotify;
201        let silent = options.silent;
202        let require_interaction = options.requireInteraction;
203
204        // For each entry in options["actions"]
205        // up to the maximum number of actions supported (skip any excess entries):
206        let mut actions: Vec<Action> = Vec::new();
207        let max_actions = Notification::MaxActions(global);
208        for action in options.actions.iter().take(max_actions as usize) {
209            actions.push(Action {
210                id: Uuid::new_v4().simple().to_string(),
211                name: action.action.clone(),
212                title: action.title.clone(),
213                // If entry["icon"] exists, then parse it using baseURL, and if that does not return failure
214                // set action’s icon URL to the return value. (Otherwise action’s icon URL remains null.)
215                icon_url: action.icon.as_ref().and_then(|icon_url| {
216                    ServoUrl::parse_with_base(Some(&base_url), icon_url.as_ref())
217                        .map(|url| USVString::from(url.to_string()))
218                        .ok()
219                }),
220                icon_resource: DomRefCell::new(None),
221            });
222        }
223
224        Self {
225            eventtarget: EventTarget::new_inherited(),
226            // A non-persistent notification is a notification whose service worker registration is null.
227            serviceworker_registration: None,
228            title,
229            body,
230            data: Heap::default(),
231            dir,
232            image,
233            icon,
234            badge,
235            lang,
236            silent,
237            origin,
238            vibration_pattern,
239            timestamp,
240            renotify,
241            tag,
242            require_interaction,
243            actions,
244            pending_request_ids: DomRefCell::new(Default::default()),
245            image_resource: DomRefCell::new(None),
246            icon_resource: DomRefCell::new(None),
247            badge_resource: DomRefCell::new(None),
248        }
249    }
250
251    /// <https://notifications.spec.whatwg.org/#notification-show-steps>
252    fn show(&self) {
253        // step 3: set shown to false
254        let shown = false;
255
256        // TODO: step 4: Let oldNotification be the notification in the list of notifications
257        //               whose tag is not the empty string and is notification’s tag,
258        //               and whose origin is same origin with notification’s origin,
259        //               if any, and null otherwise.
260
261        // TODO: step 5: If oldNotification is non-null, then:
262        // TODO:   step 5.1: Handle close events with oldNotification.
263        // TODO:   step 5.2: If the notification platform supports replacement, then:
264        // TODO:     step 5.2.1: Replace oldNotification with notification, in the list of notifications.
265        // TODO:     step 5.2.2: Set shown to true.
266        // TODO:   step 5.3: Otherwise, remove oldNotification from the list of notifications.
267
268        // step 6: If shown is false, then:
269        if !shown {
270            // TODO: step 6.1: Append notification to the list of notifications.
271            // step 6.2: Display notification on the device
272            self.global()
273                .send_to_embedder(EmbedderMsg::ShowNotification(
274                    self.global().webview_id(),
275                    self.to_embedder_notification(),
276                ));
277        }
278
279        // TODO: step 7: If shown is false or oldNotification is non-null,
280        //               and notification’s renotify preference is true,
281        //               then run the alert steps for notification.
282
283        // step 8: If notification is a non-persistent notification,
284        //         then queue a task to fire an event named show on
285        //         the Notification object representing notification.
286        if self.serviceworker_registration.is_none() {
287            self.global()
288                .task_manager()
289                .dom_manipulation_task_source()
290                .queue_simple_event(self.upcast(), atom!("show"));
291        }
292    }
293
294    /// Create an [`embedder_traits::Notification`].
295    fn to_embedder_notification(&self) -> EmbedderNotification {
296        let icon_resource = self
297            .icon_resource
298            .borrow()
299            .as_ref()
300            .map(|image| image.to_shared());
301        EmbedderNotification {
302            title: self.title.to_string(),
303            body: self.body.to_string(),
304            tag: self.tag.to_string(),
305            language: self.lang.to_string(),
306            require_interaction: self.require_interaction,
307            silent: self.silent,
308            icon_url: self
309                .icon
310                .as_ref()
311                .and_then(|icon| ServoUrl::parse(icon).ok()),
312            badge_url: self
313                .badge
314                .as_ref()
315                .and_then(|badge| ServoUrl::parse(badge).ok()),
316            image_url: self
317                .image
318                .as_ref()
319                .and_then(|image| ServoUrl::parse(image).ok()),
320            actions: self
321                .actions
322                .iter()
323                .map(|action| EmbedderNotificationAction {
324                    name: action.name.to_string(),
325                    title: action.title.to_string(),
326                    icon_url: action
327                        .icon_url
328                        .as_ref()
329                        .and_then(|icon| ServoUrl::parse(icon).ok()),
330                    icon_resource: icon_resource.clone(),
331                })
332                .collect(),
333            icon_resource,
334            badge_resource: self
335                .badge_resource
336                .borrow()
337                .as_ref()
338                .map(|image| image.to_shared()),
339            image_resource: self
340                .image_resource
341                .borrow()
342                .as_ref()
343                .map(|image| image.to_shared()),
344        }
345    }
346}
347
348impl NotificationMethods<crate::DomTypeHolder> for Notification {
349    /// <https://notifications.spec.whatwg.org/#constructors>
350    fn Constructor(
351        cx: &mut js::context::JSContext,
352        global: &GlobalScope,
353        proto: Option<HandleObject>,
354        title: DOMString,
355        options: RootedTraceableBox<NotificationOptions>,
356    ) -> Fallible<DomRoot<Notification>> {
357        // step 1: Check global is a ServiceWorkerGlobalScope
358        if global.is::<ServiceWorkerGlobalScope>() {
359            return Err(Error::Type(
360                c"Notification constructor cannot be used in service worker.".to_owned(),
361            ));
362        }
363
364        // step 2: Check options.actions must be empty
365        if !options.actions.is_empty() {
366            return Err(Error::Type(
367                c"Actions are only supported for persistent notifications.".to_owned(),
368            ));
369        }
370
371        // step 3: Create a notification with a settings object
372        let notification =
373            create_notification_with_settings_object(cx, global, title, options, proto)?;
374
375        // TODO: Run step 5.1, 5.2 in parallel
376        // step 5.1: If the result of getting the notifications permission state is not "granted",
377        //           then queue a task to fire an event named error on this, and abort these steps.
378        let permission_state = get_notifications_permission_state(global);
379        if permission_state != NotificationPermission::Granted {
380            global
381                .task_manager()
382                .dom_manipulation_task_source()
383                .queue_simple_event(notification.upcast(), atom!("error"));
384            // TODO: abort steps
385        } else {
386            // step 5.2: Run the notification show steps for notification
387            // <https://notifications.spec.whatwg.org/#notification-show-steps>
388            // step 1: Run the fetch steps for notification.
389            notification.fetch_resources_and_show_when_ready();
390        }
391
392        Ok(notification)
393    }
394
395    /// <https://notifications.spec.whatwg.org/#dom-notification-permission>
396    fn GetPermission(global: &GlobalScope) -> Fallible<NotificationPermission> {
397        Ok(get_notifications_permission_state(global))
398    }
399
400    /// <https://notifications.spec.whatwg.org/#dom-notification-requestpermission>
401    fn RequestPermission(
402        cx: &mut js::context::JSContext,
403        global: &GlobalScope,
404        permission_callback: Option<Rc<NotificationPermissionCallback>>,
405    ) -> Rc<Promise> {
406        // Step 2: Let promise be a new promise in this’s relevant Realm.
407        let promise = Promise::new(global, CanGc::from_cx(cx));
408
409        // TODO: Step 3: Run these steps in parallel:
410        // Step 3.1: Let permissionState be the result of requesting permission to use "notifications".
411        let notification_permission = request_notification_permission(cx, global);
412
413        // Step 3.2: Queue a global task on the DOM manipulation task source given global to run these steps:
414        let trusted_promise = TrustedPromise::new(promise.clone());
415        let uuid = Uuid::new_v4().simple().to_string();
416        let uuid_ = uuid.clone();
417
418        if let Some(callback) = permission_callback {
419            global.add_notification_permission_request_callback(uuid, callback);
420        }
421
422        global.task_manager().dom_manipulation_task_source().queue(
423            task!(request_permission: move || {
424                let promise = trusted_promise.root();
425                let global = promise.global();
426
427                // Step 3.2.1: If deprecatedCallback is given,
428                //             then invoke deprecatedCallback with « permissionState » and "report".
429                if let Some(callback) = global.remove_notification_permission_request_callback(uuid_) {
430                    let _ = callback.Call__(notification_permission, ExceptionHandling::Report, CanGc::note());
431                }
432
433                // Step 3.2.2: Resolve promise with permissionState.
434                promise.resolve_native(&notification_permission, CanGc::note());
435            }),
436        );
437
438        promise
439    }
440
441    // <https://notifications.spec.whatwg.org/#dom-notification-onclick>
442    event_handler!(click, GetOnclick, SetOnclick);
443    // <https://notifications.spec.whatwg.org/#dom-notification-onshow>
444    event_handler!(show, GetOnshow, SetOnshow);
445    // <https://notifications.spec.whatwg.org/#dom-notification-onerror>
446    event_handler!(error, GetOnerror, SetOnerror);
447    // <https://notifications.spec.whatwg.org/#dom-notification-onclose>
448    event_handler!(close, GetOnclose, SetOnclose);
449
450    /// <https://notifications.spec.whatwg.org/#maximum-number-of-actions>
451    fn MaxActions(_global: &GlobalScope) -> u32 {
452        // TODO: determine the maximum number of actions
453        2
454    }
455    /// <https://notifications.spec.whatwg.org/#dom-notification-title>
456    fn Title(&self) -> DOMString {
457        self.title.clone()
458    }
459    /// <https://notifications.spec.whatwg.org/#dom-notification-dir>
460    fn Dir(&self) -> NotificationDirection {
461        self.dir
462    }
463    /// <https://notifications.spec.whatwg.org/#dom-notification-lang>
464    fn Lang(&self) -> DOMString {
465        self.lang.clone()
466    }
467    /// <https://notifications.spec.whatwg.org/#dom-notification-body>
468    fn Body(&self) -> DOMString {
469        self.body.clone()
470    }
471    /// <https://notifications.spec.whatwg.org/#dom-notification-tag>
472    fn Tag(&self) -> DOMString {
473        self.tag.clone()
474    }
475    /// <https://notifications.spec.whatwg.org/#dom-notification-image>
476    fn Image(&self) -> USVString {
477        // step 1: If there is no this’s notification’s image URL, then return the empty string.
478        // step 2: Return this’s notification’s image URL, serialized.
479        self.image.clone().unwrap_or_default()
480    }
481    /// <https://notifications.spec.whatwg.org/#dom-notification-icon>
482    fn Icon(&self) -> USVString {
483        // step 1: If there is no this’s notification’s icon URL, then return the empty string.
484        // step 2: Return this’s notification’s icon URL, serialized.
485        self.icon.clone().unwrap_or_default()
486    }
487    /// <https://notifications.spec.whatwg.org/#dom-notification-badge>
488    fn Badge(&self) -> USVString {
489        // step 1: If there is no this’s notification’s badge URL, then return the empty string.
490        // step 2: Return this’s notification’s badge URL, serialized.
491        self.badge.clone().unwrap_or_default()
492    }
493    /// <https://notifications.spec.whatwg.org/#dom-notification-renotify>
494    fn Renotify(&self) -> bool {
495        self.renotify
496    }
497    /// <https://notifications.spec.whatwg.org/#dom-notification-silent>
498    fn GetSilent(&self) -> Option<bool> {
499        self.silent
500    }
501    /// <https://notifications.spec.whatwg.org/#dom-notification-requireinteraction>
502    fn RequireInteraction(&self) -> bool {
503        self.require_interaction
504    }
505    /// <https://notifications.spec.whatwg.org/#dom-notification-data>
506    fn Data(&self, _cx: SafeJSContext, mut retval: MutableHandleValue) {
507        retval.set(self.data.get());
508    }
509    /// <https://notifications.spec.whatwg.org/#dom-notification-actions>
510    fn Actions(&self, cx: SafeJSContext, can_gc: CanGc, retval: MutableHandleValue) {
511        // step 1: Let frozenActions be an empty list of type NotificationAction.
512        let mut frozen_actions: Vec<NotificationAction> = Vec::new();
513
514        // step 2: For each entry of this’s notification’s actions
515        for action in self.actions.iter() {
516            let action = NotificationAction {
517                action: action.name.clone(),
518                title: action.title.clone(),
519                // If entry’s icon URL is non-null,
520                // then set action["icon"] to entry’s icon URL, icon_url, serialized.
521                icon: action.icon_url.clone(),
522            };
523
524            // TODO: step 2.5: Call Object.freeze on action, to prevent accidental mutation by scripts.
525            // step 2.6: Append action to frozenActions.
526            frozen_actions.push(action);
527        }
528
529        // step 3: Return the result of create a frozen array from frozenActions.
530        to_frozen_array(frozen_actions.as_slice(), cx, retval, can_gc);
531    }
532    /// <https://notifications.spec.whatwg.org/#dom-notification-vibrate>
533    fn Vibrate(&self, cx: SafeJSContext, can_gc: CanGc, retval: MutableHandleValue) {
534        to_frozen_array(self.vibration_pattern.as_slice(), cx, retval, can_gc);
535    }
536    /// <https://notifications.spec.whatwg.org/#dom-notification-timestamp>
537    fn Timestamp(&self) -> u64 {
538        self.timestamp
539    }
540    /// <https://notifications.spec.whatwg.org/#dom-notification-close>
541    fn Close(&self) {
542        // TODO: If notification is a persistent notification and notification was closed by the end user
543        // then fire a service worker notification event named "notificationclose" given notification.
544
545        // If notification is a non-persistent notification
546        // then queue a task to fire an event named close on the Notification object representing notification.
547        if self.serviceworker_registration.is_none() {
548            self.global()
549                .task_manager()
550                .dom_manipulation_task_source()
551                .queue_simple_event(self.upcast(), atom!("close"));
552        }
553    }
554}
555
556/// <https://notifications.spec.whatwg.org/#actions>
557#[derive(JSTraceable, MallocSizeOf)]
558struct Action {
559    id: String,
560    /// <https://notifications.spec.whatwg.org/#action-name>
561    name: DOMString,
562    /// <https://notifications.spec.whatwg.org/#action-title>
563    title: DOMString,
564    /// <https://notifications.spec.whatwg.org/#action-icon-url>
565    icon_url: Option<USVString>,
566    /// <https://notifications.spec.whatwg.org/#action-icon-resource>
567    #[ignore_malloc_size_of = "RasterImage"]
568    #[no_trace]
569    icon_resource: DomRefCell<Option<Arc<RasterImage>>>,
570}
571
572/// <https://notifications.spec.whatwg.org/#create-a-notification-with-a-settings-object>
573fn create_notification_with_settings_object(
574    cx: &mut js::context::JSContext,
575    global: &GlobalScope,
576    title: DOMString,
577    options: RootedTraceableBox<NotificationOptions>,
578    proto: Option<HandleObject>,
579) -> Fallible<DomRoot<Notification>> {
580    // step 1: Let origin be settings’s origin.
581    let origin = global.origin().immutable().clone();
582    // step 2: Let baseURL be settings’s API base URL.
583    let base_url = global.api_base_url();
584    // step 3: Let fallbackTimestamp be the number of milliseconds from
585    //         the Unix epoch to settings’s current wall time, rounded to the nearest integer.
586    let fallback_timestamp = SystemTime::now()
587        .duration_since(UNIX_EPOCH)
588        .unwrap_or_default()
589        .as_millis() as u64;
590    // step 4: Return the result of creating a notification given title, options, origin,
591    //         baseURL, and fallbackTimestamp.
592    create_notification(
593        cx,
594        global,
595        title,
596        options,
597        origin,
598        base_url,
599        fallback_timestamp,
600        proto,
601    )
602}
603
604/// <https://notifications.spec.whatwg.org/#create-a-notification
605#[expect(clippy::too_many_arguments)]
606fn create_notification(
607    cx: &mut js::context::JSContext,
608    global: &GlobalScope,
609    title: DOMString,
610    options: RootedTraceableBox<NotificationOptions>,
611    origin: ImmutableOrigin,
612    base_url: ServoUrl,
613    fallback_timestamp: u64,
614    proto: Option<HandleObject>,
615) -> Fallible<DomRoot<Notification>> {
616    // If options["silent"] is true and options["vibrate"] exists, then throw a TypeError.
617    if options.silent.is_some() && options.vibrate.is_some() {
618        return Err(Error::Type(
619            c"Can't specify vibration patterns when setting notification to silent.".to_owned(),
620        ));
621    }
622    // If options["renotify"] is true and options["tag"] is the empty string, then throw a TypeError.
623    if options.renotify && options.tag.is_empty() {
624        return Err(Error::Type(
625            c"tag must be set to renotify as an existing notification.".to_owned(),
626        ));
627    }
628
629    Ok(Notification::new(
630        cx,
631        global,
632        title,
633        options,
634        origin,
635        base_url,
636        fallback_timestamp,
637        proto,
638    ))
639}
640
641/// <https://w3c.github.io/vibration/#dfn-validate-and-normalize>
642fn validate_and_normalize_vibration_pattern(
643    pattern: &UnsignedLongOrUnsignedLongSequence,
644) -> Vec<u32> {
645    // Step 1: If pattern is a list, proceed to the next step. Otherwise run the following substeps:
646    let mut pattern: Vec<u32> = match pattern {
647        UnsignedLongOrUnsignedLongSequence::UnsignedLong(value) => {
648            // Step 1.1: Let list be an initially empty list, and add pattern to list.
649            // Step 1.2: Set pattern to list.
650            vec![*value]
651        },
652        UnsignedLongOrUnsignedLongSequence::UnsignedLongSequence(values) => values.clone(),
653    };
654
655    // Step 2: Let max length have the value 10.
656    // Step 3: If the length of pattern is greater than max length, truncate pattern,
657    //         leaving only the first max length entries.
658    pattern.truncate(10);
659
660    // If the length of the pattern is even and not zero then the last entry in the pattern will
661    // have no effect so an implementation can remove it from the pattern at this point.
662    if pattern.len() % 2 == 0 && !pattern.is_empty() {
663        pattern.pop();
664    }
665
666    // Step 4: Let max duration have the value 10000.
667    // Step 5: For each entry in pattern whose value is greater than max duration,
668    //         set the entry's value to max duration.
669    pattern.iter_mut().for_each(|entry| {
670        *entry = 10000.min(*entry);
671    });
672
673    // Step 6: Return pattern.
674    pattern
675}
676
677/// <https://notifications.spec.whatwg.org/#get-the-notifications-permission-state>
678fn get_notifications_permission_state(global: &GlobalScope) -> NotificationPermission {
679    let permission_state = descriptor_permission_state(PermissionName::Notifications, Some(global));
680    match permission_state {
681        PermissionState::Granted => NotificationPermission::Granted,
682        PermissionState::Denied => NotificationPermission::Denied,
683        PermissionState::Prompt => NotificationPermission::Default,
684    }
685}
686
687fn request_notification_permission(
688    cx: &mut js::context::JSContext,
689    global: &GlobalScope,
690) -> NotificationPermission {
691    let promise = &Promise::new(global, CanGc::from_cx(cx));
692    let descriptor = PermissionDescriptor {
693        name: PermissionName::Notifications,
694    };
695    let status = PermissionStatus::new(global, &descriptor, CanGc::from_cx(cx));
696
697    // The implementation of `request_notification_permission` seemed to be synchronous
698    Permissions::permission_request(cx, promise, &descriptor, &status);
699
700    match status.State() {
701        PermissionState::Granted => NotificationPermission::Granted,
702        PermissionState::Denied => NotificationPermission::Denied,
703        // Should only receive "Granted" or "Denied" from the permission request
704        PermissionState::Prompt => NotificationPermission::Default,
705    }
706}
707
708#[derive(Clone, Debug, Eq, Hash, PartialEq)]
709enum ResourceType {
710    Image,
711    Icon,
712    Badge,
713    ActionIcon(String), // action id
714}
715
716struct ResourceFetchListener {
717    /// The ID of the pending image cache for this request.
718    pending_image_id: PendingImageId,
719    /// A reference to the global image cache.
720    image_cache: Arc<dyn ImageCache>,
721    /// The notification instance which makes this request.
722    notification: Trusted<Notification>,
723    /// Request status that indicates whether this request failed, and the reason.
724    status: Result<(), NetworkError>,
725    /// Resource URL of this request.
726    url: ServoUrl,
727}
728
729impl FetchResponseListener for ResourceFetchListener {
730    fn process_request_body(&mut self, _: RequestId) {}
731    fn process_request_eof(&mut self, _: RequestId) {}
732
733    fn process_response(
734        &mut self,
735        request_id: RequestId,
736        metadata: Result<FetchMetadata, NetworkError>,
737    ) {
738        self.image_cache.notify_pending_response(
739            self.pending_image_id,
740            FetchResponseMsg::ProcessResponse(request_id, metadata.clone()),
741        );
742
743        let metadata = metadata.ok().map(|meta| match meta {
744            FetchMetadata::Unfiltered(m) => m,
745            FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
746        });
747
748        let status = metadata
749            .as_ref()
750            .map(|m| m.status.clone())
751            .unwrap_or_else(HttpStatus::new_error);
752
753        self.status = {
754            if status.is_success() {
755                Ok(())
756            } else if status.is_error() {
757                Err(NetworkError::ResourceLoadError(
758                    "No http status code received".to_owned(),
759                ))
760            } else {
761                Err(NetworkError::ResourceLoadError(format!(
762                    "HTTP error code {}",
763                    status.code()
764                )))
765            }
766        };
767    }
768
769    fn process_response_chunk(&mut self, request_id: RequestId, payload: Vec<u8>) {
770        if self.status.is_ok() {
771            self.image_cache.notify_pending_response(
772                self.pending_image_id,
773                FetchResponseMsg::ProcessResponseChunk(request_id, payload.into()),
774            );
775        }
776    }
777
778    fn process_response_eof(
779        self,
780        cx: &mut js::context::JSContext,
781        request_id: RequestId,
782        response: Result<(), NetworkError>,
783        timing: ResourceFetchTiming,
784    ) {
785        self.image_cache.notify_pending_response(
786            self.pending_image_id,
787            FetchResponseMsg::ProcessResponseEOF(request_id, response.clone(), timing.clone()),
788        );
789        network_listener::submit_timing(&self, &response, &timing, CanGc::from_cx(cx));
790    }
791
792    fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
793        let global = &self.resource_timing_global();
794        global.report_csp_violations(violations, None, None);
795    }
796}
797
798impl ResourceTimingListener for ResourceFetchListener {
799    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
800        (InitiatorType::Other, self.url.clone())
801    }
802
803    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
804        self.notification.root().global()
805    }
806}
807
808impl Notification {
809    fn build_resource_request(&self, url: &ServoUrl) -> RequestBuilder {
810        let global = &self.global();
811        create_a_potential_cors_request(
812            None,
813            url.clone(),
814            Destination::Image,
815            None, // TODO: check which CORS should be used
816            None,
817            global.get_referrer(),
818        )
819        .with_global_scope(global)
820    }
821
822    /// <https://notifications.spec.whatwg.org/#fetch-steps>
823    fn fetch_resources_and_show_when_ready(&self) {
824        let mut pending_requests: Vec<(RequestBuilder, ResourceType)> = vec![];
825        if let Some(image_url) = &self.image {
826            if let Ok(url) = ServoUrl::parse(image_url) {
827                let request = self.build_resource_request(&url);
828                self.pending_request_ids.borrow_mut().insert(request.id);
829                pending_requests.push((request, ResourceType::Image));
830            }
831        }
832        if let Some(icon_url) = &self.icon {
833            if let Ok(url) = ServoUrl::parse(icon_url) {
834                let request = self.build_resource_request(&url);
835                self.pending_request_ids.borrow_mut().insert(request.id);
836                pending_requests.push((request, ResourceType::Icon));
837            }
838        }
839        if let Some(badge_url) = &self.badge {
840            if let Ok(url) = ServoUrl::parse(badge_url) {
841                let request = self.build_resource_request(&url);
842                self.pending_request_ids.borrow_mut().insert(request.id);
843                pending_requests.push((request, ResourceType::Badge));
844            }
845        }
846        for action in self.actions.iter() {
847            if let Some(icon_url) = &action.icon_url {
848                if let Ok(url) = ServoUrl::parse(icon_url) {
849                    let request = self.build_resource_request(&url);
850                    self.pending_request_ids.borrow_mut().insert(request.id);
851                    pending_requests.push((request, ResourceType::ActionIcon(action.id.clone())));
852                }
853            }
854        }
855
856        for (request, resource_type) in pending_requests {
857            self.fetch_and_show_when_ready(request, resource_type);
858        }
859    }
860
861    fn fetch_and_show_when_ready(&self, request: RequestBuilder, resource_type: ResourceType) {
862        let global: &GlobalScope = &self.global();
863        let request_id = request.id;
864
865        let cache_result = global.image_cache().get_cached_image_status(
866            request.url.clone(),
867            global.origin().immutable().clone(),
868            None, // TODO: check which CORS should be used
869        );
870        match cache_result {
871            ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable {
872                image, ..
873            }) => {
874                let image = image.as_raster_image();
875                if image.is_none() {
876                    warn!("Vector images are not supported in notifications yet");
877                };
878                self.set_resource_and_show_when_ready(request_id, &resource_type, image);
879            },
880            ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable(
881                _,
882                pending_image_id,
883            )) => {
884                self.register_image_cache_callback(request_id, pending_image_id, resource_type);
885            },
886            ImageCacheResult::Pending(pending_image_id) => {
887                self.register_image_cache_callback(request_id, pending_image_id, resource_type);
888            },
889            ImageCacheResult::ReadyForRequest(pending_image_id) => {
890                self.register_image_cache_callback(request_id, pending_image_id, resource_type);
891                self.fetch(pending_image_id, request, global);
892            },
893            ImageCacheResult::FailedToLoadOrDecode => {
894                self.set_resource_and_show_when_ready(request_id, &resource_type, None);
895            },
896        };
897    }
898
899    fn register_image_cache_callback(
900        &self,
901        request_id: RequestId,
902        pending_image_id: PendingImageId,
903        resource_type: ResourceType,
904    ) {
905        let global: &GlobalScope = &self.global();
906        let trusted_this = Trusted::new(self);
907        let task_source = global.task_manager().networking_task_source().to_sendable();
908
909        let callback = Box::new(move |response| {
910            let trusted_this = trusted_this.clone();
911            let resource_type = resource_type.clone();
912            task_source.queue(task!(handle_response: move || {
913                let this = trusted_this.root();
914                let ImageCacheResponseMessage::NotifyPendingImageLoadStatus(status) = response else {
915                    warn!("Received unexpected message from image cache: {response:?}");
916                    return;
917                };
918                this.handle_image_cache_response(request_id, status.response, resource_type);
919            }));
920        });
921
922        global.image_cache().add_listener(ImageLoadListener::new(
923            callback,
924            global.pipeline_id(),
925            pending_image_id,
926        ));
927    }
928
929    fn handle_image_cache_response(
930        &self,
931        request_id: RequestId,
932        response: ImageResponse,
933        resource_type: ResourceType,
934    ) {
935        match response {
936            ImageResponse::Loaded(image, _) => {
937                let image = image.as_raster_image();
938                if image.is_none() {
939                    warn!("Vector images are not yet supported in notification attribute");
940                };
941                self.set_resource_and_show_when_ready(request_id, &resource_type, image);
942            },
943            ImageResponse::FailedToLoadOrDecode => {
944                self.set_resource_and_show_when_ready(request_id, &resource_type, None);
945            },
946            _ => (),
947        };
948    }
949
950    fn set_resource_and_show_when_ready(
951        &self,
952        request_id: RequestId,
953        resource_type: &ResourceType,
954        image: Option<Arc<RasterImage>>,
955    ) {
956        match resource_type {
957            ResourceType::Image => {
958                *self.image_resource.borrow_mut() = image;
959            },
960            ResourceType::Icon => {
961                *self.icon_resource.borrow_mut() = image;
962            },
963            ResourceType::Badge => {
964                *self.badge_resource.borrow_mut() = image;
965            },
966            ResourceType::ActionIcon(id) => {
967                if let Some(action) = self.actions.iter().find(|&action| *action.id == *id) {
968                    *action.icon_resource.borrow_mut() = image;
969                }
970            },
971        }
972
973        let mut pending_requests_id = self.pending_request_ids.borrow_mut();
974        pending_requests_id.remove(&request_id);
975
976        // <https://notifications.spec.whatwg.org/#notification-show-steps>
977        // step 2: Wait for any fetches to complete and notification’s resources to be set
978        if pending_requests_id.is_empty() {
979            self.show();
980        }
981    }
982
983    fn fetch(
984        &self,
985        pending_image_id: PendingImageId,
986        request: RequestBuilder,
987        global: &GlobalScope,
988    ) {
989        let context = ResourceFetchListener {
990            pending_image_id,
991            image_cache: global.image_cache(),
992            notification: Trusted::new(self),
993            url: request.url.clone(),
994            status: Ok(()),
995        };
996
997        global.fetch(
998            request,
999            context,
1000            global.task_manager().networking_task_source().into(),
1001        );
1002    }
1003}