Skip to main content

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