mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Propagate through documents a flag that represents if any of the ancestor navigables has a potentially trustworthy origin. The "potentially trustworthy origin" concept appears to have gotten confused in a couple of places and we were instead testing if a URL had "potentially trustworthy" properties. The main test for the ancestor navigables is [mixed-content/nested-iframes](https://github.com/web-platform-tests/wpt/blob/master/mixed-content/nested-iframes.window.js) --- <!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: --> - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes fix #36108 <!-- Either: --> - [X] There are tests for these changes --------- Signed-off-by: Sebastian C <sebsebmc@gmail.com>
1027 lines
40 KiB
Rust
1027 lines
40 KiB
Rust
/* 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::collections::HashSet;
|
||
use std::rc::Rc;
|
||
use std::sync::{Arc, Mutex};
|
||
use std::time::{SystemTime, UNIX_EPOCH};
|
||
|
||
use content_security_policy::Destination;
|
||
use dom_struct::dom_struct;
|
||
use embedder_traits::{
|
||
EmbedderMsg, Notification as EmbedderNotification,
|
||
NotificationAction as EmbedderNotificationAction,
|
||
};
|
||
use ipc_channel::ipc;
|
||
use ipc_channel::router::ROUTER;
|
||
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, ImageCacheResult, ImageOrMetadataAvailable, ImageResponder, ImageResponse,
|
||
PendingImageId, PendingImageResponse, UsePlaceholder,
|
||
};
|
||
use net_traits::request::{RequestBuilder, RequestId};
|
||
use net_traits::{
|
||
FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError, ResourceFetchTiming,
|
||
ResourceTimingType,
|
||
};
|
||
use pixels::Image;
|
||
use servo_url::{ImmutableOrigin, ServoUrl};
|
||
use uuid::Uuid;
|
||
|
||
use super::bindings::cell::DomRefCell;
|
||
use super::bindings::refcounted::{Trusted, TrustedPromise};
|
||
use super::bindings::reflector::DomGlobal;
|
||
use super::performanceresourcetiming::InitiatorType;
|
||
use super::permissionstatus::PermissionStatus;
|
||
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::reflector::reflect_dom_object_with_proto;
|
||
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::eventtarget::EventTarget;
|
||
use crate::dom::globalscope::GlobalScope;
|
||
use crate::dom::permissions::{PermissionAlgorithm, Permissions, descriptor_permission_state};
|
||
use crate::dom::promise::Promise;
|
||
use crate::dom::serviceworkerglobalscope::ServiceWorkerGlobalScope;
|
||
use crate::dom::serviceworkerregistration::ServiceWorkerRegistration;
|
||
use crate::fetch::create_a_potential_cors_request;
|
||
use crate::network_listener::{self, PreInvoke, ResourceTimingListener};
|
||
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
|
||
|
||
// 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<HashSet<RequestId>>,
|
||
/// <https://notifications.spec.whatwg.org/#image-resource>
|
||
#[ignore_malloc_size_of = "Arc"]
|
||
#[no_trace]
|
||
image_resource: DomRefCell<Option<Arc<Image>>>,
|
||
/// <https://notifications.spec.whatwg.org/#icon-resource>
|
||
#[ignore_malloc_size_of = "Arc"]
|
||
#[no_trace]
|
||
icon_resource: DomRefCell<Option<Arc<Image>>>,
|
||
/// <https://notifications.spec.whatwg.org/#badge-resource>
|
||
#[ignore_malloc_size_of = "Arc"]
|
||
#[no_trace]
|
||
badge_resource: DomRefCell<Option<Arc<Image>>>,
|
||
}
|
||
|
||
impl Notification {
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn new(
|
||
global: &GlobalScope,
|
||
title: DOMString,
|
||
options: RootedTraceableBox<NotificationOptions>,
|
||
origin: ImmutableOrigin,
|
||
base_url: ServoUrl,
|
||
fallback_timestamp: u64,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
) -> DomRoot<Self> {
|
||
let notification = reflect_dom_object_with_proto(
|
||
Box::new(Notification::new_inherited(
|
||
global,
|
||
title,
|
||
&options,
|
||
origin,
|
||
base_url,
|
||
fallback_timestamp,
|
||
)),
|
||
global,
|
||
proto,
|
||
can_gc,
|
||
);
|
||
|
||
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 data = Heap::default();
|
||
|
||
let title = title.clone();
|
||
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,
|
||
dir,
|
||
image,
|
||
icon,
|
||
badge,
|
||
lang,
|
||
silent,
|
||
origin,
|
||
vibration_pattern,
|
||
timestamp,
|
||
renotify,
|
||
tag,
|
||
require_interaction,
|
||
actions,
|
||
pending_request_ids: DomRefCell::new(HashSet::new()),
|
||
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 {
|
||
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: action.icon_resource.borrow().clone(),
|
||
})
|
||
.collect(),
|
||
icon_resource: self.icon_resource.borrow().clone(),
|
||
badge_resource: self.badge_resource.borrow().clone(),
|
||
image_resource: self.image_resource.borrow().clone(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl NotificationMethods<crate::DomTypeHolder> for Notification {
|
||
/// <https://notifications.spec.whatwg.org/#constructors>
|
||
fn Constructor(
|
||
global: &GlobalScope,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
title: DOMString,
|
||
options: RootedTraceableBox<NotificationOptions>,
|
||
) -> Fallible<DomRoot<Notification>> {
|
||
// step 1: Check global is a ServiceWorkerGlobalScope
|
||
if global.is::<ServiceWorkerGlobalScope>() {
|
||
return Err(Error::Type(
|
||
"Notification constructor cannot be used in service worker.".to_string(),
|
||
));
|
||
}
|
||
|
||
// step 2: Check options.actions must be empty
|
||
if !options.actions.is_empty() {
|
||
return Err(Error::Type(
|
||
"Actions are only supported for persistent notifications.".to_string(),
|
||
));
|
||
}
|
||
|
||
// step 3: Create a notification with a settings object
|
||
let notification =
|
||
create_notification_with_settings_object(global, title, options, proto, can_gc)?;
|
||
|
||
// 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(
|
||
global: &GlobalScope,
|
||
permission_callback: Option<Rc<NotificationPermissionCallback>>,
|
||
can_gc: CanGc,
|
||
) -> Rc<Promise> {
|
||
// Step 2: Let promise be a new promise in this’s relevant Realm.
|
||
let promise = Promise::new(global, can_gc);
|
||
|
||
// 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(global, can_gc);
|
||
|
||
// 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.clone(), callback.clone());
|
||
}
|
||
|
||
global.task_manager().dom_manipulation_task_source().queue(
|
||
task!(request_permission: move || {
|
||
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__(notification_permission, ExceptionHandling::Report, CanGc::note());
|
||
}
|
||
|
||
// Step 3.2.2: Resolve promise with permissionState.
|
||
promise.resolve_native(¬ification_permission, CanGc::note());
|
||
}),
|
||
);
|
||
|
||
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, _cx: SafeJSContext, mut retval: MutableHandleValue) {
|
||
retval.set(self.data.get());
|
||
}
|
||
/// <https://notifications.spec.whatwg.org/#dom-notification-actions>
|
||
fn Actions(&self, cx: SafeJSContext, can_gc: CanGc, 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(frozen_actions.as_slice(), cx, retval, can_gc);
|
||
}
|
||
/// <https://notifications.spec.whatwg.org/#dom-notification-vibrate>
|
||
fn Vibrate(&self, cx: SafeJSContext, can_gc: CanGc, retval: MutableHandleValue) {
|
||
to_frozen_array(self.vibration_pattern.as_slice(), cx, retval, can_gc);
|
||
}
|
||
/// <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 = "Arc"]
|
||
#[no_trace]
|
||
icon_resource: DomRefCell<Option<Arc<Image>>>,
|
||
}
|
||
|
||
/// <https://notifications.spec.whatwg.org/#create-a-notification-with-a-settings-object>
|
||
fn create_notification_with_settings_object(
|
||
global: &GlobalScope,
|
||
title: DOMString,
|
||
options: RootedTraceableBox<NotificationOptions>,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
) -> 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(
|
||
global,
|
||
title,
|
||
options,
|
||
origin,
|
||
base_url,
|
||
fallback_timestamp,
|
||
proto,
|
||
can_gc,
|
||
)
|
||
}
|
||
|
||
/// <https://notifications.spec.whatwg.org/#create-a-notification
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn create_notification(
|
||
global: &GlobalScope,
|
||
title: DOMString,
|
||
options: RootedTraceableBox<NotificationOptions>,
|
||
origin: ImmutableOrigin,
|
||
base_url: ServoUrl,
|
||
fallback_timestamp: u64,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
) -> 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(
|
||
"Can't specify vibration patterns when setting notification to silent.".to_string(),
|
||
));
|
||
}
|
||
// 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(
|
||
"tag must be set to renotify as an existing notification.".to_string(),
|
||
));
|
||
}
|
||
|
||
Ok(Notification::new(
|
||
global,
|
||
title,
|
||
options,
|
||
origin,
|
||
base_url,
|
||
fallback_timestamp,
|
||
proto,
|
||
can_gc,
|
||
))
|
||
}
|
||
|
||
/// <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() % 2 == 0 && !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(global: &GlobalScope, can_gc: CanGc) -> NotificationPermission {
|
||
let cx = GlobalScope::get_cx();
|
||
let promise = &Promise::new(global, can_gc);
|
||
let descriptor = PermissionDescriptor {
|
||
name: PermissionName::Notifications,
|
||
};
|
||
let status = PermissionStatus::new(global, &descriptor, can_gc);
|
||
|
||
// 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,
|
||
/// Timing data for this resource.
|
||
resource_timing: ResourceFetchTiming,
|
||
}
|
||
|
||
impl FetchResponseListener for ResourceFetchListener {
|
||
fn process_request_body(&mut self, _: RequestId) {}
|
||
fn process_request_eof(&mut self, _: RequestId) {}
|
||
|
||
fn process_response(
|
||
&mut self,
|
||
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::Internal(
|
||
"No http status code received".to_owned(),
|
||
))
|
||
} else {
|
||
Err(NetworkError::Internal(format!(
|
||
"HTTP error code {}",
|
||
status.code()
|
||
)))
|
||
}
|
||
};
|
||
}
|
||
|
||
fn process_response_chunk(&mut self, 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),
|
||
);
|
||
}
|
||
}
|
||
|
||
fn process_response_eof(
|
||
&mut self,
|
||
request_id: RequestId,
|
||
response: Result<ResourceFetchTiming, NetworkError>,
|
||
) {
|
||
self.image_cache.notify_pending_response(
|
||
self.pending_image_id,
|
||
FetchResponseMsg::ProcessResponseEOF(request_id, response),
|
||
);
|
||
}
|
||
|
||
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
|
||
&mut self.resource_timing
|
||
}
|
||
|
||
fn resource_timing(&self) -> &ResourceFetchTiming {
|
||
&self.resource_timing
|
||
}
|
||
|
||
fn submit_resource_timing(&mut self) {
|
||
network_listener::submit_timing(self, CanGc::note())
|
||
}
|
||
}
|
||
|
||
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 PreInvoke for ResourceFetchListener {
|
||
fn should_invoke(&self) -> bool {
|
||
true
|
||
}
|
||
}
|
||
|
||
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(),
|
||
global.insecure_requests_policy(),
|
||
global.has_trustworthy_ancestor_or_current_origin(),
|
||
)
|
||
.origin(global.origin().immutable().clone())
|
||
.pipeline_id(Some(global.pipeline_id()))
|
||
}
|
||
|
||
/// <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 {
|
||
if 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 {
|
||
if 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 {
|
||
if 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 {
|
||
if 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.clone(),
|
||
global.origin().immutable().clone(),
|
||
None, // TODO: check which CORS should be used
|
||
UsePlaceholder::No,
|
||
);
|
||
match cache_result {
|
||
ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable {
|
||
image, ..
|
||
}) => {
|
||
self.set_resource_and_show_when_ready(request_id, &resource_type, Some(image));
|
||
},
|
||
ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable(
|
||
_,
|
||
pending_image_id,
|
||
)) => {
|
||
self.register_image_cache_callback(
|
||
request_id,
|
||
pending_image_id,
|
||
resource_type.clone(),
|
||
);
|
||
},
|
||
ImageCacheResult::Pending(pending_image_id) => {
|
||
self.register_image_cache_callback(
|
||
request_id,
|
||
pending_image_id,
|
||
resource_type.clone(),
|
||
);
|
||
},
|
||
ImageCacheResult::ReadyForRequest(pending_image_id) => {
|
||
self.register_image_cache_callback(
|
||
request_id,
|
||
pending_image_id,
|
||
resource_type.clone(),
|
||
);
|
||
self.fetch(pending_image_id, request, global);
|
||
},
|
||
ImageCacheResult::LoadError => {
|
||
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 (sender, receiver) =
|
||
ipc::channel::<PendingImageResponse>().expect("ipc channel failure");
|
||
|
||
let global: &GlobalScope = &self.global();
|
||
|
||
let trusted_this = Trusted::new(self);
|
||
let resource_type = resource_type.clone();
|
||
let task_source = global.task_manager().networking_task_source().to_sendable();
|
||
|
||
ROUTER.add_typed_route(
|
||
receiver,
|
||
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();
|
||
if let Ok(response) = response {
|
||
this.handle_image_cache_response(request_id, response.response, resource_type);
|
||
} else {
|
||
this.handle_image_cache_response(request_id, ImageResponse::None, resource_type);
|
||
}
|
||
}));
|
||
}),
|
||
);
|
||
|
||
global.image_cache().add_listener(ImageResponder::new(
|
||
sender,
|
||
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, _) => {
|
||
self.set_resource_and_show_when_ready(request_id, &resource_type, Some(image));
|
||
},
|
||
ImageResponse::PlaceholderLoaded(image, _) => {
|
||
self.set_resource_and_show_when_ready(request_id, &resource_type, Some(image));
|
||
},
|
||
ImageResponse::None => {
|
||
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<Image>>,
|
||
) {
|
||
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 = Arc::new(Mutex::new(ResourceFetchListener {
|
||
pending_image_id,
|
||
image_cache: global.image_cache(),
|
||
notification: Trusted::new(self),
|
||
url: request.url.clone(),
|
||
status: Ok(()),
|
||
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
|
||
}));
|
||
|
||
global.fetch(
|
||
request,
|
||
context,
|
||
global.task_manager().networking_task_source().into(),
|
||
);
|
||
}
|
||
}
|