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