script/dom/
notification.rs

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