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