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