Skip to main content

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::context::JSContext;
15use js::jsapi::Heap;
16use js::jsval::JSVal;
17use js::rust::{HandleObject, MutableHandleValue};
18use net_traits::http_status::HttpStatus;
19use net_traits::image_cache::{
20    ImageCache, ImageCacheResponseMessage, ImageCacheResult, ImageLoadListener,
21    ImageOrMetadataAvailable, ImageResponse, PendingImageId,
22};
23use net_traits::request::{Destination, RequestBuilder, RequestId};
24use net_traits::{FetchMetadata, FetchResponseMsg, NetworkError, ResourceFetchTiming};
25use pixels::RasterImage;
26use rustc_hash::FxHashSet;
27use script_bindings::cell::DomRefCell;
28use script_bindings::reflector::reflect_dom_object_with_proto_and_cx;
29use servo_url::{ImmutableOrigin, ServoUrl};
30use uuid::Uuid;
31
32use super::bindings::refcounted::{Trusted, TrustedPromise};
33use super::bindings::reflector::DomGlobal;
34use super::performanceresourcetiming::InitiatorType;
35use super::permissionstatus::PermissionStatus;
36use crate::dom::bindings::callback::ExceptionHandling;
37use crate::dom::bindings::codegen::Bindings::NotificationBinding::{
38    NotificationAction, NotificationDirection, NotificationMethods, NotificationOptions,
39    NotificationPermission, NotificationPermissionCallback,
40};
41use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionStatus_Binding::PermissionStatusMethods;
42use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::{
43    PermissionDescriptor, PermissionName, PermissionState,
44};
45use crate::dom::bindings::codegen::UnionTypes::UnsignedLongOrUnsignedLongSequence;
46use crate::dom::bindings::error::{Error, Fallible};
47use crate::dom::bindings::inheritance::Castable;
48use crate::dom::bindings::root::{Dom, DomRoot};
49use crate::dom::bindings::str::{DOMString, USVString};
50use crate::dom::bindings::trace::RootedTraceableBox;
51use crate::dom::bindings::utils::to_frozen_array;
52use crate::dom::csp::{GlobalCspReporting, Violation};
53use crate::dom::eventtarget::EventTarget;
54use crate::dom::globalscope::GlobalScope;
55use crate::dom::permissions::{PermissionAlgorithm, Permissions, descriptor_permission_state};
56use crate::dom::promise::Promise;
57use crate::dom::serviceworkerglobalscope::ServiceWorkerGlobalScope;
58use crate::dom::serviceworkerregistration::ServiceWorkerRegistration;
59use crate::fetch::{RequestWithGlobalScope, create_a_potential_cors_request};
60use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
61use crate::script_runtime::CanGc;
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 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 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 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(cx, global);
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 |cx| {
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__(cx, notification_permission, ExceptionHandling::Report);
431                }
432
433                // Step 3.2.2: Resolve promise with permissionState.
434                promise.resolve_native(cx, &notification_permission);
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
456    /// <https://notifications.spec.whatwg.org/#dom-notification-title>
457    fn Title(&self) -> DOMString {
458        self.title.clone()
459    }
460
461    /// <https://notifications.spec.whatwg.org/#dom-notification-dir>
462    fn Dir(&self) -> NotificationDirection {
463        self.dir
464    }
465
466    /// <https://notifications.spec.whatwg.org/#dom-notification-lang>
467    fn Lang(&self) -> DOMString {
468        self.lang.clone()
469    }
470
471    /// <https://notifications.spec.whatwg.org/#dom-notification-body>
472    fn Body(&self) -> DOMString {
473        self.body.clone()
474    }
475
476    /// <https://notifications.spec.whatwg.org/#dom-notification-tag>
477    fn Tag(&self) -> DOMString {
478        self.tag.clone()
479    }
480
481    /// <https://notifications.spec.whatwg.org/#dom-notification-image>
482    fn Image(&self) -> USVString {
483        // step 1: If there is no this’s notification’s image URL, then return the empty string.
484        // step 2: Return this’s notification’s image URL, serialized.
485        self.image.clone().unwrap_or_default()
486    }
487
488    /// <https://notifications.spec.whatwg.org/#dom-notification-icon>
489    fn Icon(&self) -> USVString {
490        // step 1: If there is no this’s notification’s icon URL, then return the empty string.
491        // step 2: Return this’s notification’s icon URL, serialized.
492        self.icon.clone().unwrap_or_default()
493    }
494
495    /// <https://notifications.spec.whatwg.org/#dom-notification-badge>
496    fn Badge(&self) -> USVString {
497        // step 1: If there is no this’s notification’s badge URL, then return the empty string.
498        // step 2: Return this’s notification’s badge URL, serialized.
499        self.badge.clone().unwrap_or_default()
500    }
501
502    /// <https://notifications.spec.whatwg.org/#dom-notification-renotify>
503    fn Renotify(&self) -> bool {
504        self.renotify
505    }
506
507    /// <https://notifications.spec.whatwg.org/#dom-notification-silent>
508    fn GetSilent(&self) -> Option<bool> {
509        self.silent
510    }
511
512    /// <https://notifications.spec.whatwg.org/#dom-notification-requireinteraction>
513    fn RequireInteraction(&self) -> bool {
514        self.require_interaction
515    }
516
517    /// <https://notifications.spec.whatwg.org/#dom-notification-data>
518    fn Data(&self, mut retval: MutableHandleValue) {
519        retval.set(self.data.get());
520    }
521
522    /// <https://notifications.spec.whatwg.org/#dom-notification-actions>
523    fn Actions(&self, cx: &mut JSContext, retval: MutableHandleValue) {
524        // step 1: Let frozenActions be an empty list of type NotificationAction.
525        let mut frozen_actions: Vec<NotificationAction> = Vec::new();
526
527        // step 2: For each entry of this’s notification’s actions
528        for action in self.actions.iter() {
529            let action = NotificationAction {
530                action: action.name.clone(),
531                title: action.title.clone(),
532                // If entry’s icon URL is non-null,
533                // then set action["icon"] to entry’s icon URL, icon_url, serialized.
534                icon: action.icon_url.clone(),
535            };
536
537            // TODO: step 2.5: Call Object.freeze on action, to prevent accidental mutation by scripts.
538            // step 2.6: Append action to frozenActions.
539            frozen_actions.push(action);
540        }
541
542        // step 3: Return the result of create a frozen array from frozenActions.
543        to_frozen_array(cx, frozen_actions.as_slice(), retval);
544    }
545
546    /// <https://notifications.spec.whatwg.org/#dom-notification-vibrate>
547    fn Vibrate(&self, cx: &mut JSContext, retval: MutableHandleValue) {
548        to_frozen_array(cx, self.vibration_pattern.as_slice(), retval);
549    }
550
551    /// <https://notifications.spec.whatwg.org/#dom-notification-timestamp>
552    fn Timestamp(&self) -> u64 {
553        self.timestamp
554    }
555
556    /// <https://notifications.spec.whatwg.org/#dom-notification-close>
557    fn Close(&self) {
558        // TODO: If notification is a persistent notification and notification was closed by the end user
559        // then fire a service worker notification event named "notificationclose" given notification.
560
561        // If notification is a non-persistent notification
562        // then queue a task to fire an event named close on the Notification object representing notification.
563        if self.serviceworker_registration.is_none() {
564            self.global()
565                .task_manager()
566                .dom_manipulation_task_source()
567                .queue_simple_event(self.upcast(), atom!("close"));
568        }
569    }
570}
571
572/// <https://notifications.spec.whatwg.org/#actions>
573#[derive(JSTraceable, MallocSizeOf)]
574struct Action {
575    id: String,
576    /// <https://notifications.spec.whatwg.org/#action-name>
577    name: DOMString,
578    /// <https://notifications.spec.whatwg.org/#action-title>
579    title: DOMString,
580    /// <https://notifications.spec.whatwg.org/#action-icon-url>
581    icon_url: Option<USVString>,
582    /// <https://notifications.spec.whatwg.org/#action-icon-resource>
583    #[ignore_malloc_size_of = "RasterImage"]
584    #[no_trace]
585    icon_resource: DomRefCell<Option<Arc<RasterImage>>>,
586}
587
588/// <https://notifications.spec.whatwg.org/#create-a-notification-with-a-settings-object>
589fn create_notification_with_settings_object(
590    cx: &mut JSContext,
591    global: &GlobalScope,
592    title: DOMString,
593    options: RootedTraceableBox<NotificationOptions>,
594    proto: Option<HandleObject>,
595) -> Fallible<DomRoot<Notification>> {
596    // step 1: Let origin be settings’s origin.
597    let origin = global.origin().immutable().clone();
598    // step 2: Let baseURL be settings’s API base URL.
599    let base_url = global.api_base_url();
600    // step 3: Let fallbackTimestamp be the number of milliseconds from
601    //         the Unix epoch to settings’s current wall time, rounded to the nearest integer.
602    let fallback_timestamp = SystemTime::now()
603        .duration_since(UNIX_EPOCH)
604        .unwrap_or_default()
605        .as_millis() as u64;
606    // step 4: Return the result of creating a notification given title, options, origin,
607    //         baseURL, and fallbackTimestamp.
608    create_notification(
609        cx,
610        global,
611        title,
612        options,
613        origin,
614        base_url,
615        fallback_timestamp,
616        proto,
617    )
618}
619
620/// <https://notifications.spec.whatwg.org/#create-a-notification
621#[expect(clippy::too_many_arguments)]
622fn create_notification(
623    cx: &mut JSContext,
624    global: &GlobalScope,
625    title: DOMString,
626    options: RootedTraceableBox<NotificationOptions>,
627    origin: ImmutableOrigin,
628    base_url: ServoUrl,
629    fallback_timestamp: u64,
630    proto: Option<HandleObject>,
631) -> Fallible<DomRoot<Notification>> {
632    // If options["silent"] is true and options["vibrate"] exists, then throw a TypeError.
633    if options.silent.is_some() && options.vibrate.is_some() {
634        return Err(Error::Type(
635            c"Can't specify vibration patterns when setting notification to silent.".to_owned(),
636        ));
637    }
638    // If options["renotify"] is true and options["tag"] is the empty string, then throw a TypeError.
639    if options.renotify && options.tag.is_empty() {
640        return Err(Error::Type(
641            c"tag must be set to renotify as an existing notification.".to_owned(),
642        ));
643    }
644
645    Ok(Notification::new(
646        cx,
647        global,
648        title,
649        options,
650        origin,
651        base_url,
652        fallback_timestamp,
653        proto,
654    ))
655}
656
657/// <https://w3c.github.io/vibration/#dfn-validate-and-normalize>
658fn validate_and_normalize_vibration_pattern(
659    pattern: &UnsignedLongOrUnsignedLongSequence,
660) -> Vec<u32> {
661    // Step 1: If pattern is a list, proceed to the next step. Otherwise run the following substeps:
662    let mut pattern: Vec<u32> = match pattern {
663        UnsignedLongOrUnsignedLongSequence::UnsignedLong(value) => {
664            // Step 1.1: Let list be an initially empty list, and add pattern to list.
665            // Step 1.2: Set pattern to list.
666            vec![*value]
667        },
668        UnsignedLongOrUnsignedLongSequence::UnsignedLongSequence(values) => values.clone(),
669    };
670
671    // Step 2: Let max length have the value 10.
672    // Step 3: If the length of pattern is greater than max length, truncate pattern,
673    //         leaving only the first max length entries.
674    pattern.truncate(10);
675
676    // If the length of the pattern is even and not zero then the last entry in the pattern will
677    // have no effect so an implementation can remove it from the pattern at this point.
678    if pattern.len().is_multiple_of(2) && !pattern.is_empty() {
679        pattern.pop();
680    }
681
682    // Step 4: Let max duration have the value 10000.
683    // Step 5: For each entry in pattern whose value is greater than max duration,
684    //         set the entry's value to max duration.
685    pattern.iter_mut().for_each(|entry| {
686        *entry = 10000.min(*entry);
687    });
688
689    // Step 6: Return pattern.
690    pattern
691}
692
693/// <https://notifications.spec.whatwg.org/#get-the-notifications-permission-state>
694fn get_notifications_permission_state(global: &GlobalScope) -> NotificationPermission {
695    let permission_state = descriptor_permission_state(PermissionName::Notifications, Some(global));
696    match permission_state {
697        PermissionState::Granted => NotificationPermission::Granted,
698        PermissionState::Denied => NotificationPermission::Denied,
699        PermissionState::Prompt => NotificationPermission::Default,
700    }
701}
702
703fn request_notification_permission(
704    cx: &mut JSContext,
705    global: &GlobalScope,
706) -> NotificationPermission {
707    let promise = &Promise::new(cx, global);
708    let descriptor = PermissionDescriptor {
709        name: PermissionName::Notifications,
710    };
711    let status = PermissionStatus::new(global, &descriptor, CanGc::from_cx(cx));
712
713    // The implementation of `request_notification_permission` seemed to be synchronous
714    Permissions::permission_request(cx, promise, &descriptor, &status);
715
716    match status.State() {
717        PermissionState::Granted => NotificationPermission::Granted,
718        PermissionState::Denied => NotificationPermission::Denied,
719        // Should only receive "Granted" or "Denied" from the permission request
720        PermissionState::Prompt => NotificationPermission::Default,
721    }
722}
723
724#[derive(Clone, Debug, Eq, Hash, PartialEq)]
725enum ResourceType {
726    Image,
727    Icon,
728    Badge,
729    ActionIcon(String), // action id
730}
731
732struct ResourceFetchListener {
733    /// The ID of the pending image cache for this request.
734    pending_image_id: PendingImageId,
735    /// A reference to the global image cache.
736    image_cache: Arc<dyn ImageCache>,
737    /// The notification instance which makes this request.
738    notification: Trusted<Notification>,
739    /// Request status that indicates whether this request failed, and the reason.
740    status: Result<(), NetworkError>,
741    /// Resource URL of this request.
742    url: ServoUrl,
743}
744
745impl FetchResponseListener for ResourceFetchListener {
746    fn process_request_body(&mut self, _: RequestId) {}
747
748    fn process_response(
749        &mut self,
750        _: &mut js::context::JSContext,
751        request_id: RequestId,
752        metadata: Result<FetchMetadata, NetworkError>,
753    ) {
754        self.image_cache.notify_pending_response(
755            self.pending_image_id,
756            FetchResponseMsg::ProcessResponse(request_id, metadata.clone()),
757        );
758
759        let metadata = metadata.ok().map(|meta| match meta {
760            FetchMetadata::Unfiltered(m) => m,
761            FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
762        });
763
764        let status = metadata
765            .as_ref()
766            .map(|m| m.status.clone())
767            .unwrap_or_else(HttpStatus::new_error);
768
769        self.status = {
770            if status.is_success() {
771                Ok(())
772            } else if status.is_error() {
773                Err(NetworkError::ResourceLoadError(
774                    "No http status code received".to_owned(),
775                ))
776            } else {
777                Err(NetworkError::ResourceLoadError(format!(
778                    "HTTP error code {}",
779                    status.code()
780                )))
781            }
782        };
783    }
784
785    fn process_response_chunk(
786        &mut self,
787        _: &mut js::context::JSContext,
788        request_id: RequestId,
789        payload: Vec<u8>,
790    ) {
791        if self.status.is_ok() {
792            self.image_cache.notify_pending_response(
793                self.pending_image_id,
794                FetchResponseMsg::ProcessResponseChunk(request_id, payload.into()),
795            );
796        }
797    }
798
799    fn process_response_eof(
800        self,
801        cx: &mut JSContext,
802        request_id: RequestId,
803        response: Result<(), NetworkError>,
804        timing: ResourceFetchTiming,
805    ) {
806        self.image_cache.notify_pending_response(
807            self.pending_image_id,
808            FetchResponseMsg::ProcessResponseEOF(request_id, response.clone(), timing.clone()),
809        );
810        network_listener::submit_timing(cx, &self, &response, &timing);
811    }
812
813    fn process_csp_violations(
814        &mut self,
815        cx: &mut js::context::JSContext,
816        _request_id: RequestId,
817        violations: Vec<Violation>,
818    ) {
819        let global = &self.resource_timing_global();
820        global.report_csp_violations(cx, violations, None, None);
821    }
822}
823
824impl ResourceTimingListener for ResourceFetchListener {
825    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
826        (InitiatorType::Other, self.url.clone())
827    }
828
829    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
830        self.notification.root().global()
831    }
832}
833
834impl Notification {
835    fn build_resource_request(&self, url: &ServoUrl) -> RequestBuilder {
836        let global = &self.global();
837        create_a_potential_cors_request(
838            None,
839            url.clone(),
840            Destination::Image,
841            None, // TODO: check which CORS should be used
842            None,
843            global.get_referrer(),
844        )
845        .with_global_scope(global)
846    }
847
848    /// <https://notifications.spec.whatwg.org/#fetch-steps>
849    fn fetch_resources_and_show_when_ready(&self) {
850        let mut pending_requests: Vec<(RequestBuilder, ResourceType)> = vec![];
851        if let Some(image_url) = &self.image &&
852            let Ok(url) = ServoUrl::parse(image_url)
853        {
854            let request = self.build_resource_request(&url);
855            self.pending_request_ids.borrow_mut().insert(request.id);
856            pending_requests.push((request, ResourceType::Image));
857        }
858        if let Some(icon_url) = &self.icon &&
859            let Ok(url) = ServoUrl::parse(icon_url)
860        {
861            let request = self.build_resource_request(&url);
862            self.pending_request_ids.borrow_mut().insert(request.id);
863            pending_requests.push((request, ResourceType::Icon));
864        }
865        if let Some(badge_url) = &self.badge &&
866            let Ok(url) = ServoUrl::parse(badge_url)
867        {
868            let request = self.build_resource_request(&url);
869            self.pending_request_ids.borrow_mut().insert(request.id);
870            pending_requests.push((request, ResourceType::Badge));
871        }
872        for action in self.actions.iter() {
873            if let Some(icon_url) = &action.icon_url &&
874                let Ok(url) = ServoUrl::parse(icon_url)
875            {
876                let request = self.build_resource_request(&url);
877                self.pending_request_ids.borrow_mut().insert(request.id);
878                pending_requests.push((request, ResourceType::ActionIcon(action.id.clone())));
879            }
880        }
881
882        for (request, resource_type) in pending_requests {
883            self.fetch_and_show_when_ready(request, resource_type);
884        }
885    }
886
887    fn fetch_and_show_when_ready(&self, request: RequestBuilder, resource_type: ResourceType) {
888        let global: &GlobalScope = &self.global();
889        let request_id = request.id;
890
891        let cache_result = global.image_cache().get_cached_image_status(
892            request.url.url(),
893            global.origin().immutable().clone(),
894            None, // TODO: check which CORS should be used
895        );
896        match cache_result {
897            ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable {
898                image, ..
899            }) => {
900                let image = image.as_raster_image();
901                if image.is_none() {
902                    warn!("Vector images are not supported in notifications yet");
903                };
904                self.set_resource_and_show_when_ready(request_id, &resource_type, image);
905            },
906            ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable(
907                _,
908                pending_image_id,
909            )) => {
910                self.register_image_cache_callback(request_id, pending_image_id, resource_type);
911            },
912            ImageCacheResult::Pending(pending_image_id) => {
913                self.register_image_cache_callback(request_id, pending_image_id, resource_type);
914            },
915            ImageCacheResult::ReadyForRequest(pending_image_id) => {
916                self.register_image_cache_callback(request_id, pending_image_id, resource_type);
917                self.fetch(pending_image_id, request, global);
918            },
919            ImageCacheResult::FailedToLoadOrDecode => {
920                self.set_resource_and_show_when_ready(request_id, &resource_type, None);
921            },
922        };
923    }
924
925    fn register_image_cache_callback(
926        &self,
927        request_id: RequestId,
928        pending_image_id: PendingImageId,
929        resource_type: ResourceType,
930    ) {
931        let global: &GlobalScope = &self.global();
932        let trusted_this = Trusted::new(self);
933        let task_source = global.task_manager().networking_task_source().to_sendable();
934
935        let callback = Box::new(move |response| {
936            let trusted_this = trusted_this.clone();
937            let resource_type = resource_type.clone();
938            task_source.queue(task!(handle_response: move || {
939                let this = trusted_this.root();
940                let ImageCacheResponseMessage::NotifyPendingImageLoadStatus(status) = response else {
941                    warn!("Received unexpected message from image cache: {response:?}");
942                    return;
943                };
944                this.handle_image_cache_response(request_id, status.response, resource_type);
945            }));
946        });
947
948        global.image_cache().add_listener(ImageLoadListener::new(
949            callback,
950            global.pipeline_id(),
951            pending_image_id,
952        ));
953    }
954
955    fn handle_image_cache_response(
956        &self,
957        request_id: RequestId,
958        response: ImageResponse,
959        resource_type: ResourceType,
960    ) {
961        match response {
962            ImageResponse::Loaded(image, _) => {
963                let image = image.as_raster_image();
964                if image.is_none() {
965                    warn!("Vector images are not yet supported in notification attribute");
966                };
967                self.set_resource_and_show_when_ready(request_id, &resource_type, image);
968            },
969            ImageResponse::FailedToLoadOrDecode => {
970                self.set_resource_and_show_when_ready(request_id, &resource_type, None);
971            },
972            _ => (),
973        };
974    }
975
976    fn set_resource_and_show_when_ready(
977        &self,
978        request_id: RequestId,
979        resource_type: &ResourceType,
980        image: Option<Arc<RasterImage>>,
981    ) {
982        match resource_type {
983            ResourceType::Image => {
984                *self.image_resource.borrow_mut() = image;
985            },
986            ResourceType::Icon => {
987                *self.icon_resource.borrow_mut() = image;
988            },
989            ResourceType::Badge => {
990                *self.badge_resource.borrow_mut() = image;
991            },
992            ResourceType::ActionIcon(id) => {
993                if let Some(action) = self.actions.iter().find(|&action| *action.id == *id) {
994                    *action.icon_resource.borrow_mut() = image;
995                }
996            },
997        }
998
999        let mut pending_requests_id = self.pending_request_ids.borrow_mut();
1000        pending_requests_id.remove(&request_id);
1001
1002        // <https://notifications.spec.whatwg.org/#notification-show-steps>
1003        // step 2: Wait for any fetches to complete and notification’s resources to be set
1004        if pending_requests_id.is_empty() {
1005            self.show();
1006        }
1007    }
1008
1009    fn fetch(
1010        &self,
1011        pending_image_id: PendingImageId,
1012        request: RequestBuilder,
1013        global: &GlobalScope,
1014    ) {
1015        let context = ResourceFetchListener {
1016            pending_image_id,
1017            image_cache: global.image_cache(),
1018            notification: Trusted::new(self),
1019            url: request.url.url(),
1020            status: Ok(()),
1021        };
1022
1023        global.fetch(
1024            request,
1025            context,
1026            global.task_manager().networking_task_source().into(),
1027        );
1028    }
1029}