Skip to main content

script/dom/
notification.rs

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