/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/serviceworker/notification.rs
1 035 строк
40 KB
Tim van der Lippe
script: Move individual interfaces into subfolders (#47060)
07 авг 2026, 12:10
Не верифицирован
07 авг 2026, 12:10
33036ff
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::rc::Rc; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use dom_struct::dom_struct; use embedder_traits::{ EmbedderMsg, Notification as EmbedderNotification, NotificationAction as EmbedderNotificationAction, }; use js::context::JSContext; use js::jsapi::Heap; use js::jsval::JSVal; use js::rust::{HandleObject, MutableHandleValue}; use net_traits::http_status::HttpStatus; use net_traits::image_cache::{ ImageCache, ImageCacheResponseMessage, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable, ImageResponse, PendingImageId, }; use net_traits::request::{Destination, RequestBuilder, RequestId}; use net_traits::{FetchMetadata, FetchResponseMsg, NetworkError, ResourceFetchTiming}; use pixels::RasterImage; use rustc_hash::FxHashSet; use script_bindings::cell::DomRefCell; use script_bindings::reflector::reflect_dom_object_with_proto; use servo_url::{ImmutableOrigin, ServoUrl}; use uuid::Uuid; use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::codegen::Bindings::NotificationBinding::{ NotificationAction, NotificationDirection, NotificationMethods, NotificationOptions, NotificationPermission, NotificationPermissionCallback, }; use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionStatus_Binding::PermissionStatusMethods; use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::{ PermissionDescriptor, PermissionName, PermissionState, }; use crate::dom::bindings::codegen::UnionTypes::UnsignedLongOrUnsignedLongSequence; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::trace::RootedTraceableBox; use crate::dom::bindings::utils::to_frozen_array; use crate::dom::csp::{GlobalCspReporting, Violation}; use crate::dom::eventtarget::EventTarget; use crate::dom::globalscope::GlobalScope; use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::permissions::{PermissionAlgorithm, Permissions, descriptor_permission_state}; use crate::dom::permissionstatus::PermissionStatus; use crate::dom::promise::Promise; use crate::dom::serviceworkerglobalscope::ServiceWorkerGlobalScope; use crate::dom::serviceworkerregistration::ServiceWorkerRegistration; use crate::fetch::{RequestWithGlobalScope, create_a_potential_cors_request}; use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener}; // TODO: Service Worker API (persistent notification) // https://notifications.spec.whatwg.org/#service-worker-api /// <https://notifications.spec.whatwg.org/#notifications> #[dom_struct] pub(crate) struct Notification { eventtarget: EventTarget, /// <https://notifications.spec.whatwg.org/#service-worker-registration> serviceworker_registration: Option<Dom<ServiceWorkerRegistration>>, /// <https://notifications.spec.whatwg.org/#concept-title> title: DOMString, /// <https://notifications.spec.whatwg.org/#body> body: DOMString, /// <https://notifications.spec.whatwg.org/#data> #[ignore_malloc_size_of = "mozjs"] data: Heap<JSVal>, /// <https://notifications.spec.whatwg.org/#concept-direction> dir: NotificationDirection, /// <https://notifications.spec.whatwg.org/#image-url> image: Option<USVString>, /// <https://notifications.spec.whatwg.org/#icon-url> icon: Option<USVString>, /// <https://notifications.spec.whatwg.org/#badge-url> badge: Option<USVString>, /// <https://notifications.spec.whatwg.org/#concept-language> lang: DOMString, /// <https://notifications.spec.whatwg.org/#silent-preference-flag> silent: Option<bool>, /// <https://notifications.spec.whatwg.org/#tag> tag: DOMString, /// <https://notifications.spec.whatwg.org/#concept-origin> #[no_trace] // ImmutableOrigin is not traceable origin: ImmutableOrigin, /// <https://notifications.spec.whatwg.org/#vibration-pattern> vibration_pattern: Vec<u32>, /// <https://notifications.spec.whatwg.org/#timestamp> timestamp: u64, /// <https://notifications.spec.whatwg.org/#renotify-preference-flag> renotify: bool, /// <https://notifications.spec.whatwg.org/#require-interaction-preference-flag> require_interaction: bool, /// <https://notifications.spec.whatwg.org/#actions> actions: Vec<Action>, /// Pending image, icon, badge, action icon resource request's id #[no_trace] // RequestId is not traceable pending_request_ids: DomRefCell<FxHashSet<RequestId>>, /// <https://notifications.spec.whatwg.org/#image-resource> #[ignore_malloc_size_of = "RasterImage"] #[no_trace] image_resource: DomRefCell<Option<Arc<RasterImage>>>, /// <https://notifications.spec.whatwg.org/#icon-resource> #[ignore_malloc_size_of = "RasterImage"] #[no_trace] icon_resource: DomRefCell<Option<Arc<RasterImage>>>, /// <https://notifications.spec.whatwg.org/#badge-resource> #[ignore_malloc_size_of = "RasterImage"] #[no_trace] badge_resource: DomRefCell<Option<Arc<RasterImage>>>, } impl Notification { #[expect(clippy::too_many_arguments)] pub(crate) fn new( cx: &mut JSContext, global: &GlobalScope, title: DOMString, options: RootedTraceableBox<NotificationOptions>, origin: ImmutableOrigin, base_url: ServoUrl, fallback_timestamp: u64, proto: Option<HandleObject>, ) -> DomRoot<Self> { let notification = reflect_dom_object_with_proto( cx, Box::new(Notification::new_inherited( global, title, &options, origin, base_url, fallback_timestamp, )), global, proto, ); notification.data.set(options.data.get()); notification } /// partial implementation of <https://notifications.spec.whatwg.org/#create-a-notification> fn new_inherited( global: &GlobalScope, title: DOMString, options: &RootedTraceableBox<NotificationOptions>, origin: ImmutableOrigin, base_url: ServoUrl, fallback_timestamp: u64, ) -> Self { // TODO: missing call to https://html.spec.whatwg.org/multipage/#structuredserializeforstorage // may be find in `dom/bindings/structuredclone.rs` let dir = options.dir; let lang = options.lang.clone(); let body = options.body.clone(); let tag = options.tag.clone(); // If options["image"] exists, then parse it using baseURL, and if that does not return failure, // set notification’s image URL to the return value. (Otherwise notification’s image URL is not set.) let image = options.image.as_ref().and_then(|image_url| { ServoUrl::parse_with_base(Some(&base_url), image_url.as_ref()) .map(|url| USVString::from(url.to_string())) .ok() }); // If options["icon"] exists, then parse it using baseURL, and if that does not return failure, // set notification’s icon URL to the return value. (Otherwise notification’s icon URL is not set.) let icon = options.icon.as_ref().and_then(|icon_url| { ServoUrl::parse_with_base(Some(&base_url), icon_url.as_ref()) .map(|url| USVString::from(url.to_string())) .ok() }); // If options["badge"] exists, then parse it using baseURL, and if that does not return failure, // set notification’s badge URL to the return value. (Otherwise notification’s badge URL is not set.) let badge = options.badge.as_ref().and_then(|badge_url| { ServoUrl::parse_with_base(Some(&base_url), badge_url.as_ref()) .map(|url| USVString::from(url.to_string())) .ok() }); // If options["vibrate"] exists, then validate and normalize it and // set notification’s vibration pattern to the return value. let vibration_pattern = match &options.vibrate { Some(pattern) => validate_and_normalize_vibration_pattern(pattern), None => Vec::new(), }; // If options["timestamp"] exists, then set notification’s timestamp to the value. // Otherwise, set notification’s timestamp to fallbackTimestamp. let timestamp = options.timestamp.unwrap_or(fallback_timestamp); let renotify = options.renotify; let silent = options.silent; let require_interaction = options.requireInteraction; // For each entry in options["actions"] // up to the maximum number of actions supported (skip any excess entries): let mut actions: Vec<Action> = Vec::new(); let max_actions = Notification::MaxActions(global); for action in options.actions.iter().take(max_actions as usize) { actions.push(Action { id: Uuid::new_v4().simple().to_string(), name: action.action.clone(), title: action.title.clone(), // If entry["icon"] exists, then parse it using baseURL, and if that does not return failure // set action’s icon URL to the return value. (Otherwise action’s icon URL remains null.) icon_url: action.icon.as_ref().and_then(|icon_url| { ServoUrl::parse_with_base(Some(&base_url), icon_url.as_ref()) .map(|url| USVString::from(url.to_string())) .ok() }), icon_resource: DomRefCell::new(None), }); } Self { eventtarget: EventTarget::new_inherited(), // A non-persistent notification is a notification whose service worker registration is null. serviceworker_registration: None, title, body, data: Heap::default(), dir, image, icon, badge, lang, silent, origin, vibration_pattern, timestamp, renotify, tag, require_interaction, actions, pending_request_ids: DomRefCell::new(Default::default()), image_resource: DomRefCell::new(None), icon_resource: DomRefCell::new(None), badge_resource: DomRefCell::new(None), } } /// <https://notifications.spec.whatwg.org/#notification-show-steps> fn show(&self) { // step 3: set shown to false let shown = false; // TODO: step 4: Let oldNotification be the notification in the list of notifications // whose tag is not the empty string and is notification’s tag, // and whose origin is same origin with notification’s origin, // if any, and null otherwise. // TODO: step 5: If oldNotification is non-null, then: // TODO: step 5.1: Handle close events with oldNotification. // TODO: step 5.2: If the notification platform supports replacement, then: // TODO: step 5.2.1: Replace oldNotification with notification, in the list of notifications. // TODO: step 5.2.2: Set shown to true. // TODO: step 5.3: Otherwise, remove oldNotification from the list of notifications. // step 6: If shown is false, then: if !shown { // TODO: step 6.1: Append notification to the list of notifications. // step 6.2: Display notification on the device self.global() .send_to_embedder(EmbedderMsg::ShowNotification( self.global().webview_id(), self.to_embedder_notification(), )); } // TODO: step 7: If shown is false or oldNotification is non-null, // and notification’s renotify preference is true, // then run the alert steps for notification. // step 8: If notification is a non-persistent notification, // then queue a task to fire an event named show on // the Notification object representing notification. if self.serviceworker_registration.is_none() { self.global() .task_manager() .dom_manipulation_task_source() .queue_simple_event(self.upcast(), atom!("show")); } } /// Create an [`embedder_traits::Notification`]. fn to_embedder_notification(&self) -> EmbedderNotification { let icon_resource = self .icon_resource .borrow() .as_ref() .map(|image| image.to_shared()); EmbedderNotification { title: self.title.to_string(), body: self.body.to_string(), tag: self.tag.to_string(), language: self.lang.to_string(), require_interaction: self.require_interaction, silent: self.silent, icon_url: self .icon .as_ref() .and_then(|icon| ServoUrl::parse(icon).ok()), badge_url: self .badge .as_ref() .and_then(|badge| ServoUrl::parse(badge).ok()), image_url: self .image .as_ref() .and_then(|image| ServoUrl::parse(image).ok()), actions: self .actions .iter() .map(|action| EmbedderNotificationAction { name: action.name.to_string(), title: action.title.to_string(), icon_url: action .icon_url .as_ref() .and_then(|icon| ServoUrl::parse(icon).ok()), icon_resource: icon_resource.clone(), }) .collect(), icon_resource, badge_resource: self .badge_resource .borrow() .as_ref() .map(|image| image.to_shared()), image_resource: self .image_resource .borrow() .as_ref() .map(|image| image.to_shared()), } } } impl NotificationMethods<crate::DomTypeHolder> for Notification { /// <https://notifications.spec.whatwg.org/#constructors> fn Constructor( cx: &mut JSContext, global: &GlobalScope, proto: Option<HandleObject>, title: DOMString, options: RootedTraceableBox<NotificationOptions>, ) -> Fallible<DomRoot<Notification>> { // step 1: Check global is a ServiceWorkerGlobalScope if global.is::<ServiceWorkerGlobalScope>() { return Err(Error::Type( c"Notification constructor cannot be used in service worker.".to_owned(), )); } // step 2: Check options.actions must be empty if !options.actions.is_empty() { return Err(Error::Type( c"Actions are only supported for persistent notifications.".to_owned(), )); } // step 3: Create a notification with a settings object let notification = create_notification_with_settings_object(cx, global, title, options, proto)?; // TODO: Run step 5.1, 5.2 in parallel // step 5.1: If the result of getting the notifications permission state is not "granted", // then queue a task to fire an event named error on this, and abort these steps. let permission_state = get_notifications_permission_state(global); if permission_state != NotificationPermission::Granted { global .task_manager() .dom_manipulation_task_source() .queue_simple_event(notification.upcast(), atom!("error")); // TODO: abort steps } else { // step 5.2: Run the notification show steps for notification // <https://notifications.spec.whatwg.org/#notification-show-steps> // step 1: Run the fetch steps for notification. notification.fetch_resources_and_show_when_ready(); } Ok(notification) } /// <https://notifications.spec.whatwg.org/#dom-notification-permission> fn GetPermission(global: &GlobalScope) -> Fallible<NotificationPermission> { Ok(get_notifications_permission_state(global)) } /// <https://notifications.spec.whatwg.org/#dom-notification-requestpermission> fn RequestPermission( cx: &mut JSContext, global: &GlobalScope, permission_callback: Option<Rc<NotificationPermissionCallback>>, ) -> Rc<Promise> { // Step 2: Let promise be a new promise in this’s relevant Realm. let promise = Promise::new(cx, global); // TODO: Step 3: Run these steps in parallel: // Step 3.1: Let permissionState be the result of requesting permission to use "notifications". let notification_permission = request_notification_permission(cx, global); // Step 3.2: Queue a global task on the DOM manipulation task source given global to run these steps: let trusted_promise = TrustedPromise::new(promise.clone()); let uuid = Uuid::new_v4().simple().to_string(); let uuid_ = uuid.clone(); if let Some(callback) = permission_callback { global.add_notification_permission_request_callback(uuid, callback); } global.task_manager().dom_manipulation_task_source().queue( task!(request_permission: move |cx| { let promise = trusted_promise.root(); let global = promise.global(); // Step 3.2.1: If deprecatedCallback is given, // then invoke deprecatedCallback with « permissionState » and "report". if let Some(callback) = global.remove_notification_permission_request_callback(uuid_) { let _ = callback.Call__(cx, notification_permission, ExceptionHandling::Report); } // Step 3.2.2: Resolve promise with permissionState. promise.resolve_native(cx, ¬ification_permission); }), ); promise } // <https://notifications.spec.whatwg.org/#dom-notification-onclick> event_handler!(click, GetOnclick, SetOnclick); // <https://notifications.spec.whatwg.org/#dom-notification-onshow> event_handler!(show, GetOnshow, SetOnshow); // <https://notifications.spec.whatwg.org/#dom-notification-onerror> event_handler!(error, GetOnerror, SetOnerror); // <https://notifications.spec.whatwg.org/#dom-notification-onclose> event_handler!(close, GetOnclose, SetOnclose); /// <https://notifications.spec.whatwg.org/#maximum-number-of-actions> fn MaxActions(_global: &GlobalScope) -> u32 { // TODO: determine the maximum number of actions 2 } /// <https://notifications.spec.whatwg.org/#dom-notification-title> fn Title(&self) -> DOMString { self.title.clone() } /// <https://notifications.spec.whatwg.org/#dom-notification-dir> fn Dir(&self) -> NotificationDirection { self.dir } /// <https://notifications.spec.whatwg.org/#dom-notification-lang> fn Lang(&self) -> DOMString { self.lang.clone() } /// <https://notifications.spec.whatwg.org/#dom-notification-body> fn Body(&self) -> DOMString { self.body.clone() } /// <https://notifications.spec.whatwg.org/#dom-notification-tag> fn Tag(&self) -> DOMString { self.tag.clone() } /// <https://notifications.spec.whatwg.org/#dom-notification-image> fn Image(&self) -> USVString { // step 1: If there is no this’s notification’s image URL, then return the empty string. // step 2: Return this’s notification’s image URL, serialized. self.image.clone().unwrap_or_default() } /// <https://notifications.spec.whatwg.org/#dom-notification-icon> fn Icon(&self) -> USVString { // step 1: If there is no this’s notification’s icon URL, then return the empty string. // step 2: Return this’s notification’s icon URL, serialized. self.icon.clone().unwrap_or_default() } /// <https://notifications.spec.whatwg.org/#dom-notification-badge> fn Badge(&self) -> USVString { // step 1: If there is no this’s notification’s badge URL, then return the empty string. // step 2: Return this’s notification’s badge URL, serialized. self.badge.clone().unwrap_or_default() } /// <https://notifications.spec.whatwg.org/#dom-notification-renotify> fn Renotify(&self) -> bool { self.renotify } /// <https://notifications.spec.whatwg.org/#dom-notification-silent> fn GetSilent(&self) -> Option<bool> { self.silent } /// <https://notifications.spec.whatwg.org/#dom-notification-requireinteraction> fn RequireInteraction(&self) -> bool { self.require_interaction } /// <https://notifications.spec.whatwg.org/#dom-notification-data> fn Data(&self, mut retval: MutableHandleValue) { retval.set(self.data.get()); } /// <https://notifications.spec.whatwg.org/#dom-notification-actions> fn Actions(&self, cx: &mut JSContext, retval: MutableHandleValue) { // step 1: Let frozenActions be an empty list of type NotificationAction. let mut frozen_actions: Vec<NotificationAction> = Vec::new(); // step 2: For each entry of this’s notification’s actions for action in self.actions.iter() { let action = NotificationAction { action: action.name.clone(), title: action.title.clone(), // If entry’s icon URL is non-null, // then set action["icon"] to entry’s icon URL, icon_url, serialized. icon: action.icon_url.clone(), }; // TODO: step 2.5: Call Object.freeze on action, to prevent accidental mutation by scripts. // step 2.6: Append action to frozenActions. frozen_actions.push(action); } // step 3: Return the result of create a frozen array from frozenActions. to_frozen_array(cx, frozen_actions.as_slice(), retval); } /// <https://notifications.spec.whatwg.org/#dom-notification-vibrate> fn Vibrate(&self, cx: &mut JSContext, retval: MutableHandleValue) { to_frozen_array(cx, self.vibration_pattern.as_slice(), retval); } /// <https://notifications.spec.whatwg.org/#dom-notification-timestamp> fn Timestamp(&self) -> u64 { self.timestamp } /// <https://notifications.spec.whatwg.org/#dom-notification-close> fn Close(&self) { // TODO: If notification is a persistent notification and notification was closed by the end user // then fire a service worker notification event named "notificationclose" given notification. // If notification is a non-persistent notification // then queue a task to fire an event named close on the Notification object representing notification. if self.serviceworker_registration.is_none() { self.global() .task_manager() .dom_manipulation_task_source() .queue_simple_event(self.upcast(), atom!("close")); } } } /// <https://notifications.spec.whatwg.org/#actions> #[derive(JSTraceable, MallocSizeOf)] struct Action { id: String, /// <https://notifications.spec.whatwg.org/#action-name> name: DOMString, /// <https://notifications.spec.whatwg.org/#action-title> title: DOMString, /// <https://notifications.spec.whatwg.org/#action-icon-url> icon_url: Option<USVString>, /// <https://notifications.spec.whatwg.org/#action-icon-resource> #[ignore_malloc_size_of = "RasterImage"] #[no_trace] icon_resource: DomRefCell<Option<Arc<RasterImage>>>, } /// <https://notifications.spec.whatwg.org/#create-a-notification-with-a-settings-object> fn create_notification_with_settings_object( cx: &mut JSContext, global: &GlobalScope, title: DOMString, options: RootedTraceableBox<NotificationOptions>, proto: Option<HandleObject>, ) -> Fallible<DomRoot<Notification>> { // step 1: Let origin be settings’s origin. let origin = global.origin().immutable().clone(); // step 2: Let baseURL be settings’s API base URL. let base_url = global.api_base_url(); // step 3: Let fallbackTimestamp be the number of milliseconds from // the Unix epoch to settings’s current wall time, rounded to the nearest integer. let fallback_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64; // step 4: Return the result of creating a notification given title, options, origin, // baseURL, and fallbackTimestamp. create_notification( cx, global, title, options, origin, base_url, fallback_timestamp, proto, ) } /// <https://notifications.spec.whatwg.org/#create-a-notification #[expect(clippy::too_many_arguments)] fn create_notification( cx: &mut JSContext, global: &GlobalScope, title: DOMString, options: RootedTraceableBox<NotificationOptions>, origin: ImmutableOrigin, base_url: ServoUrl, fallback_timestamp: u64, proto: Option<HandleObject>, ) -> Fallible<DomRoot<Notification>> { // If options["silent"] is true and options["vibrate"] exists, then throw a TypeError. if options.silent.is_some() && options.vibrate.is_some() { return Err(Error::Type( c"Can't specify vibration patterns when setting notification to silent.".to_owned(), )); } // If options["renotify"] is true and options["tag"] is the empty string, then throw a TypeError. if options.renotify && options.tag.is_empty() { return Err(Error::Type( c"tag must be set to renotify as an existing notification.".to_owned(), )); } Ok(Notification::new( cx, global, title, options, origin, base_url, fallback_timestamp, proto, )) } /// <https://w3c.github.io/vibration/#dfn-validate-and-normalize> fn validate_and_normalize_vibration_pattern( pattern: &UnsignedLongOrUnsignedLongSequence, ) -> Vec<u32> { // Step 1: If pattern is a list, proceed to the next step. Otherwise run the following substeps: let mut pattern: Vec<u32> = match pattern { UnsignedLongOrUnsignedLongSequence::UnsignedLong(value) => { // Step 1.1: Let list be an initially empty list, and add pattern to list. // Step 1.2: Set pattern to list. vec![*value] }, UnsignedLongOrUnsignedLongSequence::UnsignedLongSequence(values) => values.clone(), }; // Step 2: Let max length have the value 10. // Step 3: If the length of pattern is greater than max length, truncate pattern, // leaving only the first max length entries. pattern.truncate(10); // If the length of the pattern is even and not zero then the last entry in the pattern will // have no effect so an implementation can remove it from the pattern at this point. if pattern.len().is_multiple_of(2) && !pattern.is_empty() { pattern.pop(); } // Step 4: Let max duration have the value 10000. // Step 5: For each entry in pattern whose value is greater than max duration, // set the entry's value to max duration. pattern.iter_mut().for_each(|entry| { *entry = 10000.min(*entry); }); // Step 6: Return pattern. pattern } /// <https://notifications.spec.whatwg.org/#get-the-notifications-permission-state> fn get_notifications_permission_state(global: &GlobalScope) -> NotificationPermission { let permission_state = descriptor_permission_state(PermissionName::Notifications, Some(global)); match permission_state { PermissionState::Granted => NotificationPermission::Granted, PermissionState::Denied => NotificationPermission::Denied, PermissionState::Prompt => NotificationPermission::Default, } } fn request_notification_permission( cx: &mut JSContext, global: &GlobalScope, ) -> NotificationPermission { let promise = &Promise::new(cx, global); let descriptor = PermissionDescriptor { name: PermissionName::Notifications, }; let status = PermissionStatus::new(cx, global, &descriptor); // The implementation of `request_notification_permission` seemed to be synchronous Permissions::permission_request(cx, promise, &descriptor, &status); match status.State() { PermissionState::Granted => NotificationPermission::Granted, PermissionState::Denied => NotificationPermission::Denied, // Should only receive "Granted" or "Denied" from the permission request PermissionState::Prompt => NotificationPermission::Default, } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] enum ResourceType { Image, Icon, Badge, ActionIcon(String), // action id } struct ResourceFetchListener { /// The ID of the pending image cache for this request. pending_image_id: PendingImageId, /// A reference to the global image cache. image_cache: Arc<dyn ImageCache>, /// The notification instance which makes this request. notification: Trusted<Notification>, /// Request status that indicates whether this request failed, and the reason. status: Result<(), NetworkError>, /// Resource URL of this request. url: ServoUrl, } impl FetchResponseListener for ResourceFetchListener { fn process_request_body(&mut self, _: RequestId) {} fn process_response( &mut self, _: &mut js::context::JSContext, request_id: RequestId, metadata: Result<FetchMetadata, NetworkError>, ) { self.image_cache.notify_pending_response( self.pending_image_id, FetchResponseMsg::ProcessResponse(request_id, metadata.clone()), ); let metadata = metadata.ok().map(|meta| match meta { FetchMetadata::Unfiltered(m) => m, FetchMetadata::Filtered { unsafe_, .. } => unsafe_, }); let status = metadata .as_ref() .map(|m| m.status.clone()) .unwrap_or_else(HttpStatus::new_error); self.status = { if status.is_success() { Ok(()) } else if status.is_error() { Err(NetworkError::ResourceLoadError( "No http status code received".to_owned(), )) } else { Err(NetworkError::ResourceLoadError(format!( "HTTP error code {}", status.code() ))) } }; } fn process_response_chunk( &mut self, _: &mut js::context::JSContext, request_id: RequestId, payload: Vec<u8>, ) { if self.status.is_ok() { self.image_cache.notify_pending_response( self.pending_image_id, FetchResponseMsg::ProcessResponseChunk(request_id, payload.into()), ); } } fn process_response_eof( self, cx: &mut JSContext, request_id: RequestId, response: Result<(), NetworkError>, timing: ResourceFetchTiming, ) { self.image_cache.notify_pending_response( self.pending_image_id, FetchResponseMsg::ProcessResponseEOF(request_id, response.clone(), timing.clone()), ); network_listener::submit_timing(cx, &self, &response, &timing); } fn process_csp_violations( &mut self, cx: &mut js::context::JSContext, _request_id: RequestId, violations: Vec<Violation>, ) { let global = &self.resource_timing_global(); global.report_csp_violations(cx, violations, None, None); } fn process_content_length(&mut self, request_id: RequestId, size: usize) { self.image_cache.notify_pending_response( self.pending_image_id, FetchResponseMsg::ProcessContentLength(request_id, size), ); } } impl ResourceTimingListener for ResourceFetchListener { fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { (InitiatorType::Other, self.url.clone()) } fn resource_timing_global(&self) -> DomRoot<GlobalScope> { self.notification.root().global() } } impl Notification { fn build_resource_request(&self, url: &ServoUrl) -> RequestBuilder { let global = &self.global(); create_a_potential_cors_request( None, url.clone(), Destination::Image, None, // TODO: check which CORS should be used None, global.get_referrer(), ) .with_global_scope(global) } /// <https://notifications.spec.whatwg.org/#fetch-steps> fn fetch_resources_and_show_when_ready(&self) { let mut pending_requests: Vec<(RequestBuilder, ResourceType)> = vec![]; if let Some(image_url) = &self.image && let Ok(url) = ServoUrl::parse(image_url) { let request = self.build_resource_request(&url); self.pending_request_ids.borrow_mut().insert(request.id); pending_requests.push((request, ResourceType::Image)); } if let Some(icon_url) = &self.icon && let Ok(url) = ServoUrl::parse(icon_url) { let request = self.build_resource_request(&url); self.pending_request_ids.borrow_mut().insert(request.id); pending_requests.push((request, ResourceType::Icon)); } if let Some(badge_url) = &self.badge && let Ok(url) = ServoUrl::parse(badge_url) { let request = self.build_resource_request(&url); self.pending_request_ids.borrow_mut().insert(request.id); pending_requests.push((request, ResourceType::Badge)); } for action in self.actions.iter() { if let Some(icon_url) = &action.icon_url && let Ok(url) = ServoUrl::parse(icon_url) { let request = self.build_resource_request(&url); self.pending_request_ids.borrow_mut().insert(request.id); pending_requests.push((request, ResourceType::ActionIcon(action.id.clone()))); } } for (request, resource_type) in pending_requests { self.fetch_and_show_when_ready(request, resource_type); } } fn fetch_and_show_when_ready(&self, request: RequestBuilder, resource_type: ResourceType) { let global: &GlobalScope = &self.global(); let request_id = request.id; let cache_result = global.image_cache().get_cached_image_status( request.url.url(), global.origin().immutable().clone(), None, // TODO: check which CORS should be used ); match cache_result { ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable { image, .. }) => { let image = image.as_raster_image(); if image.is_none() { warn!("Vector images are not supported in notifications yet"); }; self.set_resource_and_show_when_ready(request_id, &resource_type, image); }, ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable( _, pending_image_id, )) => { self.register_image_cache_callback(request_id, pending_image_id, resource_type); }, ImageCacheResult::Pending(pending_image_id) => { self.register_image_cache_callback(request_id, pending_image_id, resource_type); }, ImageCacheResult::ReadyForRequest(pending_image_id) => { self.register_image_cache_callback(request_id, pending_image_id, resource_type); self.fetch(pending_image_id, request, global); }, ImageCacheResult::FailedToLoadOrDecode => { self.set_resource_and_show_when_ready(request_id, &resource_type, None); }, }; } fn register_image_cache_callback( &self, request_id: RequestId, pending_image_id: PendingImageId, resource_type: ResourceType, ) { let global: &GlobalScope = &self.global(); let trusted_this = Trusted::new(self); let task_source = global.task_manager().networking_task_source().to_sendable(); let callback = Box::new(move |response| { let trusted_this = trusted_this.clone(); let resource_type = resource_type.clone(); task_source.queue(task!(handle_response: move || { let this = trusted_this.root(); let ImageCacheResponseMessage::NotifyPendingImageLoadStatus(status) = response else { warn!("Received unexpected message from image cache: {response:?}"); return; }; this.handle_image_cache_response(request_id, status.response, resource_type); })); }); global.image_cache().add_listener(ImageLoadListener::new( callback, global.pipeline_id(), pending_image_id, )); } fn handle_image_cache_response( &self, request_id: RequestId, response: ImageResponse, resource_type: ResourceType, ) { match response { ImageResponse::Loaded(image, _) => { let image = image.as_raster_image(); if image.is_none() { warn!("Vector images are not yet supported in notification attribute"); }; self.set_resource_and_show_when_ready(request_id, &resource_type, image); }, ImageResponse::FailedToLoadOrDecode => { self.set_resource_and_show_when_ready(request_id, &resource_type, None); }, _ => (), }; } fn set_resource_and_show_when_ready( &self, request_id: RequestId, resource_type: &ResourceType, image: Option<Arc<RasterImage>>, ) { match resource_type { ResourceType::Image => { *self.image_resource.borrow_mut() = image; }, ResourceType::Icon => { *self.icon_resource.borrow_mut() = image; }, ResourceType::Badge => { *self.badge_resource.borrow_mut() = image; }, ResourceType::ActionIcon(id) => { if let Some(action) = self.actions.iter().find(|&action| *action.id == *id) { *action.icon_resource.borrow_mut() = image; } }, } let mut pending_requests_id = self.pending_request_ids.borrow_mut(); pending_requests_id.remove(&request_id); // <https://notifications.spec.whatwg.org/#notification-show-steps> // step 2: Wait for any fetches to complete and notification’s resources to be set if pending_requests_id.is_empty() { self.show(); } } fn fetch( &self, pending_image_id: PendingImageId, request: RequestBuilder, global: &GlobalScope, ) { let context = ResourceFetchListener { pending_image_id, image_cache: global.image_cache(), notification: Trusted::new(self), url: request.url.url(), status: Ok(()), }; global.fetch( request, context, global.task_manager().networking_task_source().into(), ); } }